diff --git a/.github/workflows/code_analysis.yml b/.github/workflows/code_analysis.yml index b4882e778d..c944c9a1bc 100644 --- a/.github/workflows/code_analysis.yml +++ b/.github/workflows/code_analysis.yml @@ -38,11 +38,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -53,7 +53,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -67,4 +67,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/pytest-auth.yml b/.github/workflows/pytest-auth.yml index 6bbd6b0795..e285dd6593 100644 --- a/.github/workflows/pytest-auth.yml +++ b/.github/workflows/pytest-auth.yml @@ -36,17 +36,17 @@ jobs: # Each step will be run in order of listing. steps: # checkout the volttron repository and set current direectory to it - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 # setup the python environment for the operating system - name: Set up Python ${{matrix.os}} ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} # Run the specified tests and save the results to a unique file that can be archived for later analysis. - name: Run pytest on ${{ matrix.python-version }}, ${{ matrix.os }} - uses: volttron/volttron-build-action@v4 + uses: volttron/volttron-build-action@v5 with: python_version: ${{ matrix.python-version }} os: ${{ matrix.os }} @@ -55,20 +55,20 @@ jobs: # Archive the results from the pytest to storage. - name: Archive test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: always() with: name: pytest-report path: output/test-auth-${{matrix.os}}-${{ matrix.python-version }}-results.xml - + # - name: Publish Unit Test Results # uses: EnricoMi/publish-unit-test-result-action@v1.5 # if: always() # with: # github_token: ${{ secrets.WORKFLOW_ACCESS_TOKEN }} # files: output/test-testutils*.xml - - + + #-cov=com --cov-report=xml --cov-report=html # pytest tests.py --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html # - name: Lint with flake8 diff --git a/.github/workflows/pytest-dbutils-backup_db.yml b/.github/workflows/pytest-dbutils-backup_db.yml index 5094808140..94fb96e338 100644 --- a/.github/workflows/pytest-dbutils-backup_db.yml +++ b/.github/workflows/pytest-dbutils-backup_db.yml @@ -46,11 +46,11 @@ jobs: steps: # checkout the volttron repository and set current directory to it - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 # setup the python environment for the operating system - name: Set up Python ${{matrix.os}} ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -60,7 +60,7 @@ jobs: # Run the specified tests and save the results to a unique file that can be archived for later analysis. - name: Run pytest on ${{ matrix.python-version }}, ${{ matrix.os }} - uses: volttron/volttron-build-action@v4 + uses: volttron/volttron-build-action@v5 timeout-minutes: 600 with: python_version: ${{ matrix.python-version }} @@ -70,7 +70,7 @@ jobs: # Archive the results from the pytest to storage. - name: Archive test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: always() with: name: pytest-report diff --git a/.github/workflows/pytest-dbutils-influxdbfuncts.yml b/.github/workflows/pytest-dbutils-influxdbfuncts.yml index 74b34d8386..2ce37659e3 100644 --- a/.github/workflows/pytest-dbutils-influxdbfuncts.yml +++ b/.github/workflows/pytest-dbutils-influxdbfuncts.yml @@ -35,12 +35,12 @@ jobs: steps: # checkout the volttron repository and set current directory to it - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 # Attempt to restore the cache from the build-dependency-cache workflow if present then # the output value steps.check_files.outputs.files_exists will be set (see the next step for usage) - name: Set up Python ${{matrix.os}} ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -50,7 +50,7 @@ jobs: # Run the specified tests and save the results to a unique file that can be archived for later analysis. - name: Run pytest on ${{ matrix.python-version }}, ${{ matrix.os }} - uses: volttron/volttron-build-action@v4 + uses: volttron/volttron-build-action@v5 timeout-minutes: 600 with: python_version: ${{ matrix.python-version }} @@ -60,7 +60,7 @@ jobs: # Archive the results from the pytest to storage. - name: Archive test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: always() with: name: pytest-report diff --git a/.github/workflows/pytest-dbutils-mysqlfuncts.yml b/.github/workflows/pytest-dbutils-mysqlfuncts.yml index 3feab4f759..4219c12f1a 100644 --- a/.github/workflows/pytest-dbutils-mysqlfuncts.yml +++ b/.github/workflows/pytest-dbutils-mysqlfuncts.yml @@ -35,12 +35,12 @@ jobs: steps: # checkout the volttron repository and set current directory to it - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 # Attempt to restore the cache from the build-dependency-cache workflow if present then # the output value steps.check_files.outputs.files_exists will be set (see the next step for usage) - name: Set up Python ${{matrix.os}} ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -50,7 +50,7 @@ jobs: # Run the specified tests and save the results to a unique file that can be archived for later analysis. - name: Run pytest on ${{ matrix.python-version }}, ${{ matrix.os }} - uses: volttron/volttron-build-action@v4 + uses: volttron/volttron-build-action@v5 timeout-minutes: 600 with: python_version: ${{ matrix.python-version }} @@ -60,7 +60,7 @@ jobs: # Archive the results from the pytest to storage. - name: Archive test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: always() with: name: pytest-report diff --git a/.github/workflows/pytest-dbutils-postgresqlfuncts.yml b/.github/workflows/pytest-dbutils-postgresqlfuncts.yml index 27d4e44e9a..ce833e6888 100644 --- a/.github/workflows/pytest-dbutils-postgresqlfuncts.yml +++ b/.github/workflows/pytest-dbutils-postgresqlfuncts.yml @@ -35,12 +35,12 @@ jobs: steps: # checkout the volttron repository and set current directory to it - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 # Attempt to restore the cache from the build-dependency-cache workflow if present then # the output value steps.check_files.outputs.files_exists will be set (see the next step for usage) - name: Set up Python ${{matrix.os}} ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -50,7 +50,7 @@ jobs: # Run the specified tests and save the results to a unique file that can be archived for later analysis. - name: Run pytest on ${{ matrix.python-version }}, ${{ matrix.os }} - uses: volttron/volttron-build-action@v4 + uses: volttron/volttron-build-action@v5 timeout-minutes: 600 with: python_version: ${{ matrix.python-version }} @@ -60,7 +60,7 @@ jobs: # Archive the results from the pytest to storage. - name: Archive test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: always() with: name: pytest-report diff --git a/.github/workflows/pytest-dbutils-sqlitefuncts.yml b/.github/workflows/pytest-dbutils-sqlitefuncts.yml index 9849b05a1c..8a6969308e 100644 --- a/.github/workflows/pytest-dbutils-sqlitefuncts.yml +++ b/.github/workflows/pytest-dbutils-sqlitefuncts.yml @@ -35,12 +35,12 @@ jobs: steps: # checkout the volttron repository and set current directory to it - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 # Attempt to restore the cache from the build-dependency-cache workflow if present then # the output value steps.check_files.outputs.files_exists will be set (see the next step for usage) - name: Set up Python ${{matrix.os}} ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -51,7 +51,7 @@ jobs: # Run the specified tests and save the results to a unique file that can be archived for later analysis. - name: Run pytest on ${{ matrix.python-version }}, ${{ matrix.os }} - uses: volttron/volttron-build-action@v4 + uses: volttron/volttron-build-action@v5 timeout-minutes: 600 with: python_version: ${{ matrix.python-version }} @@ -60,7 +60,7 @@ jobs: test_output_suffix: ${{ env.OUTPUT_SUFFIX }} - name: Archive test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: always() with: name: pytest-report diff --git a/.github/workflows/pytest-dbutils-timescaldbfuncts.yml b/.github/workflows/pytest-dbutils-timescaldbfuncts.yml index fe0ed71700..8a82239a96 100644 --- a/.github/workflows/pytest-dbutils-timescaldbfuncts.yml +++ b/.github/workflows/pytest-dbutils-timescaldbfuncts.yml @@ -35,12 +35,12 @@ jobs: steps: # checkout the volttron repository and set current directory to it - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 # Attempt to restore the cache from the build-dependency-cache workflow if present then # the output value steps.check_files.outputs.files_exists will be set (see the next step for usage) - name: Set up Python ${{matrix.os}} ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -50,7 +50,7 @@ jobs: # Run the specified tests and save the results to a unique file that can be archived for later analysis. - name: Run pytest on ${{ matrix.python-version }}, ${{ matrix.os }} - uses: volttron/volttron-build-action@v4 + uses: volttron/volttron-build-action@v5 timeout-minutes: 600 with: python_version: ${{ matrix.python-version }} @@ -60,7 +60,7 @@ jobs: # Archive the results from the pytest to storage. - name: Archive test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: always() with: name: pytest-report diff --git a/.github/workflows/pytest-miscellaneous-tests.yml b/.github/workflows/pytest-miscellaneous-tests.yml index 7fdf36ed5b..e1756d414b 100644 --- a/.github/workflows/pytest-miscellaneous-tests.yml +++ b/.github/workflows/pytest-miscellaneous-tests.yml @@ -44,17 +44,17 @@ jobs: # Each step will be run in order of listing. steps: # Checkout the volttron repository and set current direectory to it - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 # Setup the python environment for the operating system - name: Set up Python ${{matrix.os}} ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} # Run the specified tests and save the results to a unique file that can be archived for later analysis - name: Run certs test on ${{ matrix.python-version }}, ${{ matrix.os }} - uses: volttron/volttron-build-action@v4 + uses: volttron/volttron-build-action@v5 with: python_version: ${{ matrix.python-version }} os: ${{ matrix.os }} @@ -62,7 +62,7 @@ jobs: test_output_suffix: misc - name: Run core agent test on ${{ matrix.python-version }}, ${{ matrix.os }} - uses: volttron/volttron-build-action@v4 + uses: volttron/volttron-build-action@v5 with: python_version: ${{ matrix.python-version }} os: ${{ matrix.os }} @@ -70,7 +70,7 @@ jobs: test_output_suffix: misc - name: Run packaging test on ${{ matrix.python-version }}, ${{ matrix.os }} - uses: volttron/volttron-build-action@v4 + uses: volttron/volttron-build-action@v5 with: python_version: ${{ matrix.python-version }} os: ${{ matrix.os }} @@ -78,7 +78,7 @@ jobs: test_output_suffix: misc - name: Run platform init test on ${{ matrix.python-version }}, ${{ matrix.os }} - uses: volttron/volttron-build-action@v4 + uses: volttron/volttron-build-action@v5 with: python_version: ${{ matrix.python-version }} os: ${{ matrix.os }} @@ -86,7 +86,7 @@ jobs: test_output_suffix: misc - name: Run sqlite3 test on ${{ matrix.python-version }}, ${{ matrix.os }} - uses: volttron/volttron-build-action@v4 + uses: volttron/volttron-build-action@v5 with: python_version: ${{ matrix.python-version }} os: ${{ matrix.os }} @@ -95,7 +95,7 @@ jobs: # Archive the results from the pytest to storage. - name: Archive test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: always() with: name: pytest-report diff --git a/.github/workflows/pytest-testutils.yml b/.github/workflows/pytest-testutils.yml index fc66ffc531..a1c2923458 100644 --- a/.github/workflows/pytest-testutils.yml +++ b/.github/workflows/pytest-testutils.yml @@ -34,18 +34,18 @@ jobs: steps: # checkout the volttron repository and set current directory to it - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 # Attempt to restore the cache from the build-dependency-cache workflow if present then # the output value steps.check_files.outputs.files_exists will be set (see the next step for usage) - name: Set up Python ${{matrix.os}} ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} # Run the specified tests and save the results to a unique file that can be archived for later analysis. - name: Run pytest on ${{ matrix.python-version }}, ${{ matrix.os }} - uses: volttron/volttron-build-action@v4 + uses: volttron/volttron-build-action@v5 with: python_version: ${{ matrix.python-version }} os: ${{ matrix.os }} @@ -54,7 +54,7 @@ jobs: # Archive the results from the pytest to storage. - name: Archive test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: always() with: name: pytest-report diff --git a/.github/workflows/pytest-vctl.yml b/.github/workflows/pytest-vctl.yml index 22b2f165c1..a36a881740 100644 --- a/.github/workflows/pytest-vctl.yml +++ b/.github/workflows/pytest-vctl.yml @@ -45,18 +45,18 @@ jobs: # Each step will be run in order of listing. steps: # checkout the volttron repository and set current directory to it - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 # Attempt to restore the cache from the build-dependency-cache workflow if present then # the output value steps.check_files.outputs.files_exists will be set (see the next step for usage) - name: Set up Python ${{matrix.os}} ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} # Run the specified tests and save the results to a unique file that can be archived for later analysis. - name: Run pytest on ${{ matrix.python-version }}, ${{ matrix.os }} - uses: volttron/volttron-build-action@v4 + uses: volttron/volttron-build-action@v5 with: python_version: ${{ matrix.python-version }} os: ${{ matrix.os }} @@ -65,7 +65,7 @@ jobs: # Archive the results from the pytest to storage. - name: Archive test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: always() with: name: pytest-report diff --git a/.github/workflows/pytest-web.yml b/.github/workflows/pytest-web.yml index 89045fe67a..36fcb8905f 100644 --- a/.github/workflows/pytest-web.yml +++ b/.github/workflows/pytest-web.yml @@ -44,17 +44,17 @@ jobs: # Each step will be run in order of listing. steps: # checkout the volttron repository and set current direectory to it - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 # setup the python environment for the operating system - name: Set up Python ${{matrix.os}} ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} # Run the specified tests and save the results to a unique file that can be archived for later analysis. - name: Run pytest on ${{ matrix.python-version }}, ${{ matrix.os }} - uses: volttron/volttron-build-action@v4 + uses: volttron/volttron-build-action@v5 with: python_version: ${{ matrix.python-version }} os: ${{ matrix.os }} @@ -63,7 +63,7 @@ jobs: # Archive the results from the pytest to storage. - name: Archive test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: always() with: name: pytest-report diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000..40ba57ee61 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,95 @@ +# This file is a template, and might need editing before it works on your project. +# You can copy and paste this template into a new `.gitlab-ci.yml` file. +# You should not add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword. +# +# To contribute improvements to CI/CD templates, please follow the Development guide at: +# https://docs.gitlab.com/ee/development/cicd/templates.html +# This specific template is located at: +# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Bash.gitlab-ci.yml + +# See https://docs.gitlab.com/ee/ci/yaml/index.html for all available options + +# you can delete this line if you're not using Docker +# image: busybox:latest + +stages: + - build + - test + +.parallel-tests: + parallel: + matrix: + - TEST: + - services/core/ActuatorAgent/tests + - services/core/DataMover/tests/ + - services/core/DNP3OutstationAgent/tests + - services/core/OpenADRVenAgent/tests + - services/core/PlatformDriverAgent/tests + - services/core/SQLHistorian/tests + - services/core/VolttronCentral/tests + - services/core/VolttronCentralPlatform/tests + - services/core/WeatherDotGov/tests + - services/ops + - volttrontesting/gevent/yield_test.py + - volttrontesting/platform/auth_tests + - volttrontesting/platform/control_tests + - volttrontesting/platform/dbutils + - volttrontesting/platform/web + - volttrontesting/platform/test_basehistorian.py + - volttrontesting/platform/test_connection.py + - volttrontesting/platform/test_core_agent.py + - volttrontesting/platform/test_instance_setup.py + - volttrontesting/platform/test_keystore.py + - volttrontesting/platform/test_packaging.py + - volttrontesting/platform/test_platform_init.py + - volttrontesting/platform/test_platform_rmq.py + - volttrontesting/platform/test_platform_web.py + - volttrontesting/platform/test_rmq_platform_shutdown.py + - volttrontesting/platform/test_sqlite3_fix.py + - volttrontesting/services/historian + - volttrontesting/services/aggregate_historian + - volttrontesting/services/tagging + - volttrontesting/services/weather + - volttrontesting/services/test_pubsub_service.py + - volttrontesting/subsystems + - volttrontesting/testutils + - volttrontesting/zmq + +build 20.04: + stage: build + tags: + - ubuntu2004 + + before_script: + #- killall -9 volttron beam.smp python + - rm -rf dist ~/.volttron ~/.volttron_instances + - rm -rf /tmp/tmp* + - rm -rf ~/rabbitmq_server + + script: + - python3 bootstrap.py --all + - source env/bin/activate + - python3 bootstrap.py --rabbitmq + - echo "BUILD_DIR_20_04=`pwd`" >> build.env + - echo "$BUILD_DIR_20_04" + - echo `pwd` + + artifacts: + reports: + dotenv: build.env + +test 20.04: + stage: test + needs: [build 20.04] + variables: + GIT_CHECKOUT: "false" + tags: + - ubuntu2004 + extends: .parallel-tests + script: + - cd $BUILD_DIR_20_04 + - echo `pwd` + - source env/bin/activate + - pytest $TEST + + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c4456b33e0..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -language: python2.7 - -# Each array entry will execute 1 job. -env: - - NUM_PROCESSES=10 CI="travis" - -services: - - docker - -script: ci-integration/run-test-docker.sh - diff --git a/README.md b/README.md index 7f888a3744..ab37ef8781 100644 --- a/README.md +++ b/README.md @@ -113,36 +113,42 @@ You can deactivate the environment at any time by running `deactivate`. #### Steps for RabbitMQ -##### 1. Install Erlang version 24 packages +##### 1. Install Erlang version 25 packages -For RabbitMQ based VOLTTRON, some RabbitMQ specific software packages must be installed. - -###### On Debian based systems and CentOS 6/7 - -If you are running an Debian or CentOS system, you can install the RabbitMQ dependencies by running the rabbit - dependencies script, passing in the OS name and appropriate distribution as parameters. The following are supported: - -- `debian focal` (for Ubuntu 20.04) +###### Install Erlang pre-requisites +```shell +sudo apt-get update +sudo apt-get install -y gnupg apt-transport-https libsctp1 libncurses5 +``` +Please note there could be other pre-requisites that erlang requires based on the version of Erlang and OS. If there are other pre-requisites required, +install of erlang should fail with appropriate error message. -- `debian bionic` (for Ubuntu 18.04) +###### Purge previous versions of Erlang +```shell +sudo apt-get purge -yf erlang-base +``` -- `debian stretch` (for Debian Stretch) +###### Install Erlang -- `debian buster` (for Debian Buster) +Download and install ErlangOTP from [Erlang Solutions](https://www.erlang-solutions.com/downloads/). +RMQ uses components - ssl, public_key, asn1, and crypto. These are by default included in the OTP +RabbitMQ 3.9.29 is compatible with Erlang versions 24.3.4.2 to 25.2. VOLTTRON was tested with Erlang version 25.2-1 -- `raspbian buster` (for Raspbian/Raspberry Pi OS buster) +Example: -Example command: +On Ubuntu 22.04: -```sh -./scripts/rabbit_dependencies.sh debian xenial +```shell +wget https://binaries2.erlang-solutions.com/ubuntu/pool/contrib/e/esl-erlang/esl-erlang_25.2-1~ubuntu~jammy_amd64.deb +sudo dpkg -i esl-erlang_25.2-1~ubuntu~jammy_amd64.deb ``` -###### Alternatively +On Ubuntu 20.04: +```shell +wget https://binaries2.erlang-solutions.com/ubuntu/pool/contrib/e/esl-erlang/esl-erlang_25.2-1~ubuntu~focal_amd64.deb +sudo dpkg -i esl-erlang_25.2-1~ubuntu~focal_amd64.deb +``` -You can download and install Erlang from [Erlang Solutions](https://www.erlang-solutions.com/resources/download.html). -Please include OTP/components - ssl, public_key, asn1, and crypto. -Also lock your version of Erlang using the [yum-plugin-versionlock](https://access.redhat.com/solutions/98873) ##### 2. Configure hostname @@ -155,9 +161,14 @@ connect to empd (port 4369) on ." Note: RabbitMQ startup error would s and not in RabbitMQ logs (/var/log/rabbitmq/rabbitmq@hostname.log) ##### 3. Bootstrap +Remove older version of rabbitmq_server directory if you are upgrading from a older version. +Defaults to /rabbitmq_server/rabbitmq_server-3.9.7 + +Run the rabbitmq boostrap command within an activated VOLTTRON environment ```sh cd volttron +source env/bin/activate python3 bootstrap.py --rabbitmq [optional install directory. defaults to /rabbitmq_server] ``` @@ -175,7 +186,7 @@ it needs to be set to the RabbitMQ installation directory (default path is `/rabbitmq_server/rabbitmq_server-`) ```sh -echo 'export RABBITMQ_HOME=$HOME/rabbitmq_server/rabbitmq_server-3.9.7'|sudo tee --append ~/.bashrc +echo 'export RABBITMQ_HOME=$HOME/rabbitmq_server/rabbitmq_server-3.9.29'|sudo tee --append ~/.bashrc source ~/.bashrc $RABBITMQ_HOME/sbin/rabbitmqctl status @@ -232,7 +243,7 @@ Your VOLTTRON_HOME currently set to: /home/vdev/new_vhome2 Is this the volttron you are attempting to setup? [Y]: Creating rmq config yml -RabbitMQ server home: [/home/vdev/rabbitmq_server/rabbitmq_server-3.9.7]: +RabbitMQ server home: [/home/vdev/rabbitmq_server/rabbitmq_server-3.9.29]: Fully qualified domain name of the system: [cs_cbox.pnl.gov]: Enable SSL Authentication: [Y]: @@ -252,7 +263,7 @@ AMQPS (SSL) port RabbitMQ address: [5671]: https port for the RabbitMQ management plugin: [15671]: INFO:rmq_setup.pyc:Starting rabbitmq server Warning: PID file not written; -detached was passed. -INFO:rmq_setup.pyc:**Started rmq server at /home/vdev/rabbitmq_server/rabbitmq_server-3.9.7 +INFO:rmq_setup.pyc:**Started rmq server at /home/vdev/rabbitmq_server/rabbitmq_server-3.9.29 INFO:requests.packages.urllib3.connectionpool:Starting new HTTP connection (1): localhost INFO:requests.packages.urllib3.connectionpool:Starting new HTTP connection (1): localhost INFO:requests.packages.urllib3.connectionpool:Starting new HTTP connection (1): localhost @@ -266,7 +277,7 @@ INFO:requests.packages.urllib3.connectionpool:Starting new HTTP connection (1): INFO:requests.packages.urllib3.connectionpool:Starting new HTTP connection (1): localhost INFO:rmq_setup.pyc:**Stopped rmq server Warning: PID file not written; -detached was passed. -INFO:rmq_setup.pyc:**Started rmq server at /home/vdev/rabbitmq_server/rabbitmq_server-3.9.7 +INFO:rmq_setup.pyc:**Started rmq server at /home/vdev/rabbitmq_server/rabbitmq_server-3.9.29 INFO:rmq_setup.pyc: ####################### diff --git a/bootstrap.py b/bootstrap.py index 7e66288328..a20364c355 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -86,7 +86,7 @@ _WINDOWS = sys.platform.startswith('win') default_rmq_dir = os.path.join(os.path.expanduser("~"), "rabbitmq_server") -rmq_version = "3.9.7" +rmq_version = "3.9.29" rabbitmq_server = f"rabbitmq_server-{rmq_version}" @@ -242,7 +242,7 @@ def main(argv=sys.argv): # Python3 for life! if sys.version_info.major < 3 or sys.version_info.minor < 6: - sys.stderr.write('error: Python >= 3.6 is required\n') + sys.stderr.write('error: Python >= 3.8 is required\n') sys.exit(1) # Build the parser diff --git a/ci-integration/run-tests.sh b/ci-integration/run-tests.sh index 9266b0b883..e0ede38058 100755 --- a/ci-integration/run-tests.sh +++ b/ci-integration/run-tests.sh @@ -46,12 +46,12 @@ echo "bootstrapping RABBITMQ" python bootstrap.py --rabbitmq --market echo "rabbitmq status" -"$HOME/rabbitmq_server/rabbitmq_server-3.9.7/sbin/rabbitmqctl" status +"$HOME/rabbitmq_server/rabbitmq_server-3.9.29/sbin/rabbitmqctl" status echo "TestDirs" for dir in $testdirs; do echo "*********TESTDIR: $dir" - py.test -s -v "$dir" + pytest -s -v "$dir" tmp_code=$? exit_code=$tmp_code @@ -79,7 +79,7 @@ for dir in $splitdirs; do if [ -d "${D}" ]; then echo "*********SPLITDIR: $D" - py.test -s -v "${D}" + pytest -s -v "${D}" tmp_code=$? if [ $tmp_code -ne 0 ]; then if [ $tmp_code -ne 5 ]; then @@ -100,7 +100,7 @@ for dir in $filedirs; do for testfile in "$dir"/*.py; do echo "Using testfile: $testfile" if [ "$testfile" != "volttrontesting/platform/packaging-tests.py" ]; then - py.test -s -v "$testfile" + pytest -s -v "$testfile" tmp_code=$? exit_code=$tmp_code diff --git a/ci-integration/virtualization/Dockerfile b/ci-integration/virtualization/Dockerfile index 58346d044b..18af962aac 100644 --- a/ci-integration/virtualization/Dockerfile +++ b/ci-integration/virtualization/Dockerfile @@ -31,8 +31,8 @@ RUN chmod +x /startup/entrypoint.sh && \ USER $VOLTTRON_USER RUN mkdir $RMQ_ROOT RUN set -eux \ - && wget -P $VOLTTRON_USER_HOME https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.9.7/rabbitmq-server-generic-unix-3.9.7.tar.xz \ - && tar -xf $VOLTTRON_USER_HOME/rabbitmq-server-generic-unix-3.9.7.tar.xz --directory $RMQ_ROOT \ + && wget -P $VOLTTRON_USER_HOME https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.9.29/rabbitmq-server-generic-unix-3.9.29.tar.xz \ + && tar -xf $VOLTTRON_USER_HOME/rabbitmq-server-generic-unix-3.9.29.tar.xz --directory $RMQ_ROOT \ && $RMQ_HOME/sbin/rabbitmq-plugins enable rabbitmq_management rabbitmq_federation rabbitmq_federation_management rabbitmq_shovel rabbitmq_shovel_management rabbitmq_auth_mechanism_ssl rabbitmq_trust_store RUN python3 -m pip install gevent-pika --user ############################################ diff --git a/deprecated/Darksky/tests/test_darksky.py b/deprecated/Darksky/tests/test_darksky.py index a4a1b93b24..424ee83f21 100644 --- a/deprecated/Darksky/tests/test_darksky.py +++ b/deprecated/Darksky/tests/test_darksky.py @@ -365,11 +365,10 @@ def test_success_forecast(volttron_instance, cleanup_cache, weather, query_agent num_records = cursor.fetchone()[0] if service_name == service: assert num_records is records_amount * len(locations) + elif identity == 'platform.darksky_perf': + assert num_records is 0 else: - if identity == 'platform.darksky_perf': - assert num_records is 0 - else: - assert num_records is records_amount * len(locations) + assert num_records is records_amount * len(locations) assert len(query_data) == len(locations) diff --git a/deprecated/MongodbHistorian/scripts/count_data_by_topic_pattern.py b/deprecated/MongodbHistorian/scripts/count_data_by_topic_pattern.py index 872d39ac6b..6078fb22c2 100644 --- a/deprecated/MongodbHistorian/scripts/count_data_by_topic_pattern.py +++ b/deprecated/MongodbHistorian/scripts/count_data_by_topic_pattern.py @@ -21,5 +21,5 @@ count = count + db.data.find( {"topic_id":x['_id'], "ts":{"$gte":s_dt, "$lt":e_dt}}).count() -print (count) -print ("time taken: {}".format(datetime.datetime.now()-start)) \ No newline at end of file +print(count) +print("time taken: {}".format(datetime.datetime.now() - start)) diff --git a/deprecated/MongodbHistorian/scripts/rollup_data_by_time.py b/deprecated/MongodbHistorian/scripts/rollup_data_by_time.py index c351d4713d..52514f00e4 100644 --- a/deprecated/MongodbHistorian/scripts/rollup_data_by_time.py +++ b/deprecated/MongodbHistorian/scripts/rollup_data_by_time.py @@ -236,7 +236,7 @@ def execute_batch(table_type, bulk, count, topic_id, topic_name): "bulk execute of {} data for {}:{}.\nnumber of op sent to " "bulk execute ({}) does not match nModified count".format( table_type, topic_id, topic_name, count)) - print ("bulk execute result {}".format(result)) + print("bulk execute result {}".format(result)) errors = True except BulkWriteError as ex: print(str(ex.details)) @@ -338,7 +338,7 @@ def init_hourly_data(db, data_collection, start_dt, end_dt): if __name__ == '__main__': start = datetime.utcnow() - print ("Starting rollup of data from {} to {}. current time: {}".format( + print("Starting rollup of data from {} to {}. current time: {}".format( start_date, end_date, start)) pool = Pool(size=10) @@ -370,13 +370,13 @@ def init_hourly_data(db, data_collection, start_dt, end_dt): source_db = connect_mongodb(local_source_params) s_dt = datetime.strptime(start_date, '%d%b%YT%H:%M:%S.%f') e_dt = datetime.strptime(end_date, '%d%b%YT%H:%M:%S.%f') - print ("Starting init of tables") + print("Starting init of tables") init_start = datetime.utcnow() init_daily_data(source_db, source_tables['data_table'], s_dt, e_dt) - print ("Total time for init of daily data " + print("Total time for init of daily data " "between {} and {} : {} " "".format(start_date, end_date, datetime.utcnow() - init_start)) @@ -385,7 +385,7 @@ def init_hourly_data(db, data_collection, start_dt, end_dt): source_tables['data_table'], s_dt, e_dt) - print ("Total time for init of hourly data " + print("Total time for init of hourly data " "between {} and {} : {} " "".format(start_date, end_date, datetime.utcnow() - init_start)) @@ -419,7 +419,7 @@ def init_hourly_data(db, data_collection, start_dt, end_dt): print("Exception processing data: {}".format(e.args)) finally: pool.kill() - print ("Total time for roll up of data : {}".format( + print("Total time for roll up of data : {}".format( datetime.utcnow() - start)) if log: log.close() diff --git a/deprecated/MongodbHistorian/scripts/rollup_data_using_groupby.py b/deprecated/MongodbHistorian/scripts/rollup_data_using_groupby.py index b67b1f9c55..9f8d603f71 100644 --- a/deprecated/MongodbHistorian/scripts/rollup_data_using_groupby.py +++ b/deprecated/MongodbHistorian/scripts/rollup_data_using_groupby.py @@ -32,13 +32,13 @@ def connect_mongodb(connection_params): - print ("setup mongodb") + print("setup mongodb") mongo_conn_str = 'mongodb://{user}:{passwd}@{host}:{port}/{database}' if connection_params.get('authSource'): mongo_conn_str = mongo_conn_str+ '?authSource={authSource}' params = connection_params mongo_conn_str = mongo_conn_str.format(**params) - print (mongo_conn_str) + print(mongo_conn_str) mongo_client = pymongo.MongoClient(mongo_conn_str) db = mongo_client[connection_params['database']] return db diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3.py b/deprecated/OldDnp3/OldDnp3Driver/PlatformDriverAgent/platform_driver/interfaces/dnp3.py similarity index 99% rename from services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3.py rename to deprecated/OldDnp3/OldDnp3Driver/PlatformDriverAgent/platform_driver/interfaces/dnp3.py index 93e1690465..90334d57ed 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3.py +++ b/deprecated/OldDnp3/OldDnp3Driver/PlatformDriverAgent/platform_driver/interfaces/dnp3.py @@ -35,7 +35,7 @@ from datetime import datetime, timedelta import logging -from . import BaseInterface, BaseRegister, BasicRevert +from services.core.PlatformDriverAgent.platform_driver.interfaces import BaseInterface, BaseRegister, BasicRevert _log = logging.getLogger(__name__) type_mapping = {"string": str, diff --git a/services/core/PlatformDriverAgent/tests/test_dnp3_driver.py b/deprecated/OldDnp3/OldDnp3Driver/PlatformDriverAgent/tests/test_dnp3_driver.py similarity index 95% rename from services/core/PlatformDriverAgent/tests/test_dnp3_driver.py rename to deprecated/OldDnp3/OldDnp3Driver/PlatformDriverAgent/tests/test_dnp3_driver.py index f0a12f4dfb..44fc8d0b06 100644 --- a/services/core/PlatformDriverAgent/tests/test_dnp3_driver.py +++ b/deprecated/OldDnp3/OldDnp3Driver/PlatformDriverAgent/tests/test_dnp3_driver.py @@ -80,7 +80,7 @@ def agent(request, volttron_instance): test_agent = volttron_instance.build_agent() def update_config(agent_id, name, value, cfg_type): - test_agent.vip.rpc.call('config.store', 'manage_store', agent_id, name, value, config_type=cfg_type) + test_agent.vip.rpc.call('config.store', 'set_config', agent_id, name, value, config_type=cfg_type) capabilities = {'edit_config_store': {'identity': PLATFORM_DRIVER}} volttron_instance.add_capabilities(test_agent.core.publickey, capabilities) @@ -95,7 +95,7 @@ def update_config(agent_id, name, value, cfg_type): # Build and start PlatformDriverAgent - test_agent.vip.rpc.call('config.store', 'manage_delete_store', PLATFORM_DRIVER) + test_agent.vip.rpc.call('config.store', 'delete_store', PLATFORM_DRIVER) platform_uuid = volttron_instance.install_agent(agent_dir=get_services_core("PlatformDriverAgent"), config_file={}, diff --git a/deprecated/OldDnp3/OldDnp3Driver/dnp3-driver.rst b/deprecated/OldDnp3/OldDnp3Driver/dnp3-driver.rst new file mode 100644 index 0000000000..d35c51e056 --- /dev/null +++ b/deprecated/OldDnp3/OldDnp3Driver/dnp3-driver.rst @@ -0,0 +1,89 @@ +.. _DNP3-Driver: + +=========== +DNP3 Driver +=========== + +VOLTTRON's DNP3 driver enables the use of `DNP3 `_ (Distributed Network Protocol) +communications, reading and writing points via a DNP3 Outstation. + +In order to use a DNP3 driver to read and write point data, VOLTTRON's DNP3 Agent must also +be configured and running. All communication between the VOLTTRON Outstation and a +DNP3 Master happens through the DNP3 Agent. + +For information about the DNP3 Agent, please see the :ref:`DNP3 Platform Specification `. + + +Requirements +============ + +The DNP3 driver requires the PyDNP3 package. This package can be installed in an activated environment with: + +.. code-block:: bash + + pip install pydnp3 + + +Driver Configuration +==================== + +There is one argument for the "driver_config" section of the DNP3 driver configuration file: + + - **dnp3_agent_id** - ID of VOLTTRON's DNP3Agent. + +Here is a sample DNP3 driver configuration file: + +.. code-block:: json + + { + "driver_config": { + "dnp3_agent_id": "dnp3agent" + }, + "campus": "campus", + "building": "building", + "unit": "dnp3", + "driver_type": "dnp3", + "registry_config": "config://dnp3.csv", + "interval": 15, + "timezone": "US/Pacific", + "heart_beat_point": "Heartbeat" + } + +A sample DNP3 driver configuration file can be found in the VOLTTRON repository +in ``services/core/PlatformDriverAgent/example_configurations/test_dnp3.config``. + + +DNP3 Registry Configuration File +================================ + +The driver's registry configuration file, a `CSV `_ file, +specifies which DNP3 points the driver will read and/or write. Each row configures a single DNP3 point. + +The following columns are required for each row: + + - **Volttron Point Name** - The name used by the VOLTTRON platform and agents to refer to the point. + - **Group** - The point's DNP3 group number. + - **Index** - The point's index number within its DNP3 data type (which is derived from its DNP3 group number). + - **Scaling** - A factor by which to multiply point values. + - **Units** - Point value units. + - **Writable** - TRUE or FALSE, indicating whether the point can be written by the driver (FALSE = read-only). + +Consult the **DNP3 data dictionary** for a point's Group and Index values. Point +definitions in the data dictionary are by agreement between the DNP3 Outstation and Master. +The VOLTTRON DNP3Agent loads the data dictionary of point definitions from the JSON file +at "point_definitions_path" in the DNP3Agent's config file. + +A sample data dictionary is available in ``services/core/DNP3Agent/dnp3/mesa_points.config``. + +Point definitions in the DNP3 driver's registry should look something like this: + +.. csv-table:: DNP3 + :header: Volttron Point Name,Group,Index,Scaling,Units,Writable + + DCHD.WTgt,41,65,1.0,NA,FALSE + DCHD.WTgt-In,30,90,1.0,NA,TRUE + DCHD.WinTms,41,66,1.0,NA,FALSE + DCHD.RmpTms,41,67,1.0,NA,FALSE + +A sample DNP3 driver registry configuration file is available +in ``services/core/PlatformDriverAgent/example_configurations/dnp3.csv``. diff --git a/deprecated/OldDnp3/OldDnp3Driverexamples/configurations/drivers/dnp3.csv b/deprecated/OldDnp3/OldDnp3Driverexamples/configurations/drivers/dnp3.csv new file mode 100644 index 0000000000..571d2e9a8e --- /dev/null +++ b/deprecated/OldDnp3/OldDnp3Driverexamples/configurations/drivers/dnp3.csv @@ -0,0 +1,13 @@ +Volttron Point Name,Group,Index,Scaling,Units,Writable +DCHD.WTgt,41,65,1.0,NA,FALSE +DCHD.WTgt-In,30,90,1.0,NA,TRUE +DCHD.WinTms,41,66,1.0,NA,FALSE +DCHD.RmpTms,41,67,1.0,NA,FALSE +DCHD.RevtTms,41,68,1.0,NA,FALSE +DCHD.RmpUpRte,41,69,1.0,NA,FALSE +DCHD.RmpDnRte,41,70,1.0,NA,FALSE +DCHD.ChaRmpUpRte,41,71,1.0,NA,FALSE +DCHD.ChaRmpDnRte,41,72,1.0,NA,FALSE +DCHD.ModPrty,41,9,1.0,NA,FALSE +DCHD.VArAct,41,10,1.0,NA,FALSE +DCHD.ModEna,12,5,1.0,NA,FALSE diff --git a/deprecated/OldDnp3/OldDnp3Driverexamples/configurations/drivers/test_dnp3.config b/deprecated/OldDnp3/OldDnp3Driverexamples/configurations/drivers/test_dnp3.config new file mode 100644 index 0000000000..7296eae4bc --- /dev/null +++ b/deprecated/OldDnp3/OldDnp3Driverexamples/configurations/drivers/test_dnp3.config @@ -0,0 +1,13 @@ +{ + "driver_config": { + "dnp3_agent_id": "dnp3agent" + }, + "campus": "campus", + "building": "building", + "unit": "dnp3", + "driver_type": "dnp3", + "registry_config": "config://dnp3.csv", + "interval": 15, + "timezone": "US/Pacific", + "heart_beat_point": "Heartbeat" +} \ No newline at end of file diff --git a/docs/.readthedocs.yaml b/docs/.readthedocs.yaml new file mode 100644 index 0000000000..8915bd8d0d --- /dev/null +++ b/docs/.readthedocs.yaml @@ -0,0 +1,29 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.10" + # You can also specify other tool versions: + # nodejs: "19" + # rust: "1.64" + # golang: "1.19" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/source/conf.py + +# If using Sphinx, optionally build your docs in additional formats such as PDF +# formats: +# - pdf + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt index 84232c5b90..e4de85564c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -27,3 +27,4 @@ zmq ply psutil ws4py +Jinja2==3.1.2 diff --git a/docs/source/agent-framework/driver-framework/dnp3-driver/dnp3-driver.rst b/docs/source/agent-framework/driver-framework/dnp3-driver/dnp3-driver.rst index d35c51e056..8aa7b03839 100644 --- a/docs/source/agent-framework/driver-framework/dnp3-driver/dnp3-driver.rst +++ b/docs/source/agent-framework/driver-framework/dnp3-driver/dnp3-driver.rst @@ -7,21 +7,18 @@ DNP3 Driver VOLTTRON's DNP3 driver enables the use of `DNP3 `_ (Distributed Network Protocol) communications, reading and writing points via a DNP3 Outstation. -In order to use a DNP3 driver to read and write point data, VOLTTRON's DNP3 Agent must also -be configured and running. All communication between the VOLTTRON Outstation and a -DNP3 Master happens through the DNP3 Agent. - -For information about the DNP3 Agent, please see the :ref:`DNP3 Platform Specification `. - +In order to use a DNP3 driver to read and write point data, a server component (i.e., Outstation) must also +be configured and running. Requirements ============ -The DNP3 driver requires the PyDNP3 package. This package can be installed in an activated environment with: +The DNP3 driver requires the `dnp3-python `_ package, a wrapper on Pydnp3 package. +This package can be installed in an activated environment with: .. code-block:: bash - pip install pydnp3 + pip install dnp3-python Driver Configuration @@ -29,24 +26,23 @@ Driver Configuration There is one argument for the "driver_config" section of the DNP3 driver configuration file: - - **dnp3_agent_id** - ID of VOLTTRON's DNP3Agent. - Here is a sample DNP3 driver configuration file: .. code-block:: json { - "driver_config": { - "dnp3_agent_id": "dnp3agent" - }, - "campus": "campus", - "building": "building", - "unit": "dnp3", - "driver_type": "dnp3", - "registry_config": "config://dnp3.csv", - "interval": 15, - "timezone": "US/Pacific", - "heart_beat_point": "Heartbeat" + "driver_config": {"master_ip": "0.0.0.0", "outstation_ip": "127.0.0.1", + "master_id": 2, "outstation_id": 1, + "port": 20000}, + "registry_config":"config://udd-Dnp3.csv", + "driver_type": "udd_dnp3", + "interval": 5, + "timezone": "UTC", + "campus": "campus-vm", + "building": "building-vm", + "unit": "Dnp3", + "publish_depth_first_all": true, + "heart_beat_point": "random_bool" } A sample DNP3 driver configuration file can be found in the VOLTTRON repository @@ -63,6 +59,7 @@ The following columns are required for each row: - **Volttron Point Name** - The name used by the VOLTTRON platform and agents to refer to the point. - **Group** - The point's DNP3 group number. + - **Variation** - THe permit negotiated exchange of data formatted, i.e., data type. - **Index** - The point's index number within its DNP3 data type (which is derived from its DNP3 group number). - **Scaling** - A factor by which to multiply point values. - **Units** - Point value units. @@ -78,12 +75,16 @@ A sample data dictionary is available in ``services/core/DNP3Agent/dnp3/mesa_poi Point definitions in the DNP3 driver's registry should look something like this: .. csv-table:: DNP3 - :header: Volttron Point Name,Group,Index,Scaling,Units,Writable + :header: Point Name,Volttron Point Name,Group,Variation,Index,Scaling,Units,Writable,Notes + + AnalogInput_index0,AnalogInput_index0,30,6,0,1,NA,FALSE,Double Analogue input without status + BinaryInput_index0,BinaryInput_index0,1,2,0,1,NA,FALSE,Single bit binary input with status + AnalogOutput_index0,AnalogOutput_index0,40,4,0,1,NA,TRUE,Double-precision floating point with flags + BinaryOutput_index0,BinaryOutput_index0,10,2,0,1,NA,TRUE,Binary Output with flags - DCHD.WTgt,41,65,1.0,NA,FALSE - DCHD.WTgt-In,30,90,1.0,NA,TRUE - DCHD.WinTms,41,66,1.0,NA,FALSE - DCHD.RmpTms,41,67,1.0,NA,FALSE A sample DNP3 driver registry configuration file is available in ``services/core/PlatformDriverAgent/example_configurations/dnp3.csv``. + +For more information about Group Variation definition, please refer to `dnp3.Variation +`_. diff --git a/docs/source/conf.py b/docs/source/conf.py index 87aafc3224..fe69affe91 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -16,11 +16,11 @@ # import sys # sys.path.insert(0, os.path.abspath('.')) -import subprocess -import sys -import os from glob import glob from mock import Mock as MagicMock +import os +import subprocess +import sys import yaml from volttron.platform.agent.utils import execute_command diff --git a/docs/source/deploying-volttron/multi-platform/multi-platform-multi-bus.rst b/docs/source/deploying-volttron/multi-platform/multi-platform-multi-bus.rst index e58f9ed497..e01563c1c6 100644 --- a/docs/source/deploying-volttron/multi-platform/multi-platform-multi-bus.rst +++ b/docs/source/deploying-volttron/multi-platform/multi-platform-multi-bus.rst @@ -83,7 +83,7 @@ Platform agent, SQL historian agent and a Listener agent. The following shows an Is this the volttron you are attempting to setup? [Y]: What type of message bus (rmq/zmq)? [zmq]: rmq Name of this volttron instance: [volttron1]: central - RabbitMQ server home: [/home/user/rabbitmq_server/rabbitmq_server-3.9.7]: + RabbitMQ server home: [/home/user/rabbitmq_server/rabbitmq_server-3.9.29]: Fully qualified domain name of the system: [central]: Would you like to create a new self signed root CAcertificate for this instance: [Y]: @@ -95,7 +95,7 @@ Platform agent, SQL historian agent and a Listener agent. The following shows an Organization Unit: volttron Do you want to use default values for RabbitMQ home, ports, and virtual host: [Y]: 2020-04-13 13:29:36,347 rmq_setup.py INFO: Starting RabbitMQ server - 2020-04-13 13:29:46,528 rmq_setup.py INFO: Rmq server at /home/user/rabbitmq_server/rabbitmq_server-3.9.7 is running at + 2020-04-13 13:29:46,528 rmq_setup.py INFO: Rmq server at /home/user/rabbitmq_server/rabbitmq_server-3.9.29 is running at 2020-04-13 13:29:46,554 volttron.utils.rmq_mgmt DEBUG: Creating new VIRTUAL HOST: volttron 2020-04-13 13:29:46,582 volttron.utils.rmq_mgmt DEBUG: Create READ, WRITE and CONFIGURE permissions for the user: central-admin Create new exchange: volttron, {'durable': True, 'type': 'topic', 'arguments': {'alternate-exchange': 'undeliverable'}} @@ -108,7 +108,7 @@ Platform agent, SQL historian agent and a Listener agent. The following shows an 2020-04-13 13:29:46,601 rmq_setup.py INFO: Creating root ca with the following info: {'C': 'US', 'ST': 'WA', 'L': 'Richland', 'O': 'PNNL', 'OU': 'VOLTTRON', 'CN': 'central-root-ca'} Created CA cert 2020-04-13 13:29:49,668 rmq_setup.py INFO: **Stopped rmq server - 2020-04-13 13:30:00,556 rmq_setup.py INFO: Rmq server at /home/user/rabbitmq_server/rabbitmq_server-3.9.7 is running at + 2020-04-13 13:30:00,556 rmq_setup.py INFO: Rmq server at /home/user/rabbitmq_server/rabbitmq_server-3.9.29 is running at 2020-04-13 13:30:00,557 rmq_setup.py INFO: ####################### @@ -443,7 +443,7 @@ name is set to "collector2". Is this the volttron you are attempting to setup? [Y]: What type of message bus (rmq/zmq)? [zmq]: rmq Name of this volttron instance: [volttron1]: collector2 - RabbitMQ server home: [/home/user/rabbitmq_server/rabbitmq_server-3.9.7]: + RabbitMQ server home: [/home/user/rabbitmq_server/rabbitmq_server-3.9.29]: Fully qualified domain name of the system: [node-rmq]: Would you like to create a new self signed root CA certificate for this instance: [Y]: @@ -455,7 +455,7 @@ name is set to "collector2". Organization Unit: volttron Do you want to use default values for RabbitMQ home, ports, and virtual host: [Y]: 2020-04-13 13:29:36,347 rmq_setup.py INFO: Starting RabbitMQ server - 2020-04-13 13:29:46,528 rmq_setup.py INFO: Rmq server at /home/user/rabbitmq_server/rabbitmq_server-3.9.7 is running at + 2020-04-13 13:29:46,528 rmq_setup.py INFO: Rmq server at /home/user/rabbitmq_server/rabbitmq_server-3.9.29 is running at 2020-04-13 13:29:46,554 volttron.utils.rmq_mgmt DEBUG: Creating new VIRTUAL HOST: volttron 2020-04-13 13:29:46,582 volttron.utils.rmq_mgmt DEBUG: Create READ, WRITE and CONFIGURE permissions for the user: collector2-admin Create new exchange: volttron, {'durable': True, 'type': 'topic', 'arguments': {'alternate-exchange': 'undeliverable'}} @@ -468,7 +468,7 @@ name is set to "collector2". 2020-04-13 13:29:46,601 rmq_setup.py INFO: Creating root ca with the following info: {'C': 'US', 'ST': 'WA', 'L': 'Richland', 'O': 'PNNL', 'OU': 'VOLTTRON', 'CN': 'collector2-root-ca'} Created CA cert 2020-04-13 13:29:49,668 rmq_setup.py INFO: **Stopped rmq server - 2020-04-13 13:30:00,556 rmq_setup.py INFO: Rmq server at /home/user/rabbitmq_server/rabbitmq_server-3.9.7 is running at + 2020-04-13 13:30:00,556 rmq_setup.py INFO: Rmq server at /home/user/rabbitmq_server/rabbitmq_server-3.9.29 is running at 2020-04-13 13:30:00,557 rmq_setup.py INFO: ####################### diff --git a/docs/source/deploying-volttron/secure-deployment-considerations.rst b/docs/source/deploying-volttron/secure-deployment-considerations.rst index 7d3719a333..ba5b97408e 100644 --- a/docs/source/deploying-volttron/secure-deployment-considerations.rst +++ b/docs/source/deploying-volttron/secure-deployment-considerations.rst @@ -179,3 +179,23 @@ system files outside of VOLTTRON_HOME For more information, refer to :ref:`Agent Isolation Mode: Running Users as unique Unix user`. +Non-Auth Implementation +======================= + +There may be some use-cases, such as simulating deployments or agent development, where security is not a consideration. +In these cases, it is possible to disable VOLTTRON's authentication and authorization, stripping away the security layer from the +VIP messagebus and simplifying agent connection and RPC communication. Since this is not ideal for any deployment, this can only +be done by manually modifying the volttron configuration file. + +Within the config file located within VOLTTRON_HOME, the allow-auth option must be added and set to False. + +.. code-block:: console + + [volttron] + message-bus = zmq + vip-address = tcp://127.0.0.1:22916 + instance-name = volttron1 + allow-auth = False + +In simulation environments where multiple volttron instances are used, it is important to ensure that auth settings are +the same across all instances. diff --git a/docs/source/developing-volttron/developing-agents/agent-development.rst b/docs/source/developing-volttron/developing-agents/agent-development.rst index 69b87b78ca..92cec9a8f7 100644 --- a/docs/source/developing-volttron/developing-agents/agent-development.rst +++ b/docs/source/developing-volttron/developing-agents/agent-development.rst @@ -918,7 +918,7 @@ VOLTTRON uses *pytest* as a framework for executing tests. All unit tests shoul For instructions on writing unit and integration tests with *pytest*, refer to the :ref:`Writing Agent Tests ` documentation. -*pytest* is not installed with the distribution by default. To install py.test and it's dependencies execute the +*pytest* is not installed with the distribution by default. To install pytest and it's dependencies execute the following: .. code-block:: bash diff --git a/docs/source/developing-volttron/developing-agents/example-agents/scheduler-example-agent.rst b/docs/source/developing-volttron/developing-agents/example-agents/scheduler-example-agent.rst index 83817ae8e8..36939d3535 100644 --- a/docs/source/developing-volttron/developing-agents/example-agents/scheduler-example-agent.rst +++ b/docs/source/developing-volttron/developing-agents/example-agents/scheduler-example-agent.rst @@ -41,7 +41,7 @@ The agent listens to schedule announcements from the actuator and then issues a @PubSub.subscribe('pubsub', topics.ACTUATOR_SCHEDULE_ANNOUNCE(campus='campus', building='building',unit='unit')) def actuate(self, peer, sender, bus, topic, headers, message): - print ("response:",topic,headers,message) + print("response:", topic, headers, message) if headers[headers_mod.REQUESTER_ID] != agent_id: return '''Match the announce for our fake device with our ID diff --git a/docs/source/developing-volttron/development-environment/pycharm/index.rst b/docs/source/developing-volttron/development-environment/pycharm/index.rst index c2044d04d0..1001ff61eb 100644 --- a/docs/source/developing-volttron/development-environment/pycharm/index.rst +++ b/docs/source/developing-volttron/development-environment/pycharm/index.rst @@ -6,7 +6,7 @@ Pycharm Development Environment Pycharm is an IDE dedicated to developing python projects. It provides coding assistance and easy access to debugging tools as well as integration with -py.test. It is a popular tool for working with VOLTTRON. +pytest. It is a popular tool for working with VOLTTRON. Jetbrains provides a free community version that can be downloaded from https://www.jetbrains.com/pycharm/ @@ -98,8 +98,8 @@ top level source directory; git will ignore changes to these files. Testing an Agent ---------------- -Agent tests written in py.test can be run simply by right-clicking the tests -directory and selecting `Run 'py.test in tests`, so long as the root directory +Agent tests written in pytest can be run simply by right-clicking the tests +directory and selecting `Run 'pytest in tests`, so long as the root directory is set as the VOLTTRON source root. |Run Tests| diff --git a/docs/source/index.rst b/docs/source/index.rst index 6bdc1ac7a2..83b1b4b283 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -150,6 +150,15 @@ at our bi-weekly office-hours and on Slack. To be invited to office-hours or sla tutorials/quick-start + +.. toctree:: + :caption: Tutorials + :hidden: + :titlesonly: + :maxdepth: 1 + + tutorials/quick-start + Indices and tables ================== diff --git a/docs/source/introduction/platform-install.rst b/docs/source/introduction/platform-install.rst index c17e1f0d74..5b77d8ae82 100644 --- a/docs/source/introduction/platform-install.rst +++ b/docs/source/introduction/platform-install.rst @@ -194,37 +194,40 @@ Step 2 - Install Erlang packages For RabbitMQ based VOLTTRON, some of the RabbitMQ specific software packages have to be installed. +Install Erlang pre-requisites ++++++++++++++++++++++++++++++ +.. code-block:: bash + sudo apt-get update + sudo apt-get install -y gnupg apt-transport-https libsctp1 libncurses5 -On Debian based systems and CentOS 8 -"""""""""""""""""""""""""""""""""""" +Please note there could be other pre-requisites that erlang requires based on the version of Erlang and OS. If there are other pre-requisites required, install of erlang should fail with appropriate error message. -If you are running a Debian or CentOS 8 system, you can install the RabbitMQ dependencies by running the -"rabbit_dependencies.sh" script, passing in the OS name and appropriate distribution as parameters. The -following are supported: +Purge previous versions of Erlang ++++++++++++++++++++++++++++++++++ -* `debian bionic` (for Ubuntu 18.04) +.. code-block:: bash -* `debian focal` (for Ubuntu 20.04) + sudo apt-get purge -yf erlang-base +Install Erlang +++++++++++++++ -Example command: +Download and install ErlangOTP from [Erlang Solutions](https://www.erlang-solutions.com/downloads/). +RMQ uses components - ssl, public_key, asn1, and crypto. These are by default included in the OTP +RabbitMQ 3.9.29 is compatible with Erlang versions 24.3.4.2 to 25.2. VOLTTRON was tested with Erlang version 25.2-1 +Example: Ubuntu 22.04 .. code-block:: bash - ./scripts/rabbit_dependencies.sh debian xenial - + wget https://binaries2.erlang-solutions.com/ubuntu/pool/contrib/e/esl-erlang/esl-erlang_25.2-1~ubuntu~jammy_amd64.deb + sudo dpkg -i esl-erlang_25.2-1~ubuntu~jammy_amd64.deb -Alternatively -""""""""""""" -You can download and install Erlang from `Erlang Solutions `_. -Please include OTP/components - ssl, public_key, asn1, and crypto. -Also lock your version of Erlang using the `yum-plugin-versionlock `_. +Example: Ubuntu 20.04 +.. code-block:: bash -.. note:: - Currently VOLTTRON only officially supports specific versions of Erlang for each operating system: - * 1:24.1.7-1 for Debian - * 24.2-1.el8 for CentOS 8 + wget https://binaries2.erlang-solutions.com/ubuntu/pool/contrib/e/esl-erlang/esl-erlang_25.2-1~ubuntu~focal_amd64.deb + sudo dpkg -i esl-erlang_25.2-1~ubuntu~focal_amd64.deb Step 3 - Configure hostname @@ -246,10 +249,14 @@ to connect to empd (port 4369) on ." Step 4 - Bootstrap the environment ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Remove older version of rabbitmq_server directory if you are upgrading from a older version. Defaults to +/rabbitmq_server/rabbitmq_server-3.9.7 +Run the rabbitmq bootstrap step from within an VOLTTRON activated environment. .. code-block:: bash cd volttron + source env/bin/activate python3 bootstrap.py --rabbitmq [optional install directory. defaults to /rabbitmq_server] This will build the platform and create a virtual Python environment and dependencies for RabbitMQ. It also installs @@ -272,11 +279,11 @@ Thus, you can use $RABBITMQ_HOME to see if the RabbitMQ server is installed by c .. note:: The `RABBITMQ_HOME` environment variable can be set in ~/.bashrc. If doing so, it needs to be set to the RabbitMQ - installation directory (default path is `/rabbitmq_server/rabbitmq_server-3.9.7`) + installation directory (default path is `/rabbitmq_server/rabbitmq_server-3.9.29`) .. code-block:: bash - echo 'export RABBITMQ_HOME=$HOME/rabbitmq_server/rabbitmq_server-3.9.7'|sudo tee --append ~/.bashrc + echo 'export RABBITMQ_HOME=$HOME/rabbitmq_server/rabbitmq_server-3.9.29'|sudo tee --append ~/.bashrc source ~/.bashrc $RABBITMQ_HOME/sbin/rabbitmqctl status @@ -334,7 +341,7 @@ prompts for necessary details. Is this the volttron you are attempting to setup? [Y]: Creating rmq config yml - RabbitMQ server home: [/home/vdev/rabbitmq_server/rabbitmq_server-3.9.7]: + RabbitMQ server home: [/home/vdev/rabbitmq_server/rabbitmq_server-3.9.29]: Fully qualified domain name of the system: [cs_cbox.pnl.gov]: Enable SSL Authentication: [Y]: @@ -354,7 +361,7 @@ prompts for necessary details. https port for the RabbitMQ management plugin: [15671]: INFO:rmq_setup.pyc:Starting rabbitmq server Warning: PID file not written; -detached was passed. - INFO:rmq_setup.pyc:**Started rmq server at /home/vdev/rabbitmq_server/rabbitmq_server-3.9.7 + INFO:rmq_setup.pyc:**Started rmq server at /home/vdev/rabbitmq_server/rabbitmq_server-3.9.29 INFO:requests.packages.urllib3.connectionpool:Starting new HTTP connection (1): localhost INFO:requests.packages.urllib3.connectionpool:Starting new HTTP connection (1): localhost INFO:requests.packages.urllib3.connectionpool:Starting new HTTP connection (1): localhost @@ -368,7 +375,7 @@ prompts for necessary details. INFO:requests.packages.urllib3.connectionpool:Starting new HTTP connection (1): localhost INFO:rmq_setup.pyc:**Stopped rmq server Warning: PID file not written; -detached was passed. - INFO:rmq_setup.pyc:**Started rmq server at /home/vdev/rabbitmq_server/rabbitmq_server-3.9.7 + INFO:rmq_setup.pyc:**Started rmq server at /home/vdev/rabbitmq_server/rabbitmq_server-3.9.29 INFO:rmq_setup.pyc: ####################### diff --git a/docs/source/platform-features/config-store/agent-configuration-store.rst b/docs/source/platform-features/config-store/agent-configuration-store.rst index fbf55ecd3c..aea72e2ac6 100644 --- a/docs/source/platform-features/config-store/agent-configuration-store.rst +++ b/docs/source/platform-features/config-store/agent-configuration-store.rst @@ -312,36 +312,66 @@ Platform RPC Methods -------------------- -Methods for Agents -^^^^^^^^^^^^^^^^^^ - -Agent methods that change configurations do not trigger any callbacks unless trigger_callback is True. - -**set_config(config_name, contents, trigger_callback=False)** - Change/create a configuration file on the platform. +**set_config(identity, config_name, contents, config_type="raw", trigger_callback=True, send_update=True)** - +Change/create a configuration on the platform for an agent with the specified identity. Requires the +authorization capability 'edit_config_store'. By default agents have access to edit only their own config store entries. + +**manage_store(identity, config_name, contents, config_type="raw", trigger_callback=True, send_update=True)** - +Deprecated method. Please use set_config instead. Will be removed in VOLTTRON version 10. +Change/create a configuration on the platform for an agent with the specified identity. Requires the +authorization capability 'edit_config_store'. By default agents have access to edit only their own config store entries. + +**delete_config(identity, config_name, trigger_callback=True, send_update=True)** - Delete a configuration for an +agent with the specified identity. Requires the authorization capability 'edit_config_store'. By default agents have +access to edit only their own config store entries. + +**manage_delete_config(identity, config_name, trigger_callback=True, send_update=True)** - +Deprecated method. Please use delete_config instead. Will be removed in VOLTTRON version 10. +Delete a configuration for an agent with the specified identity. Requires the authorization capability +'edit_config_store'. By default agents have access to edit only their own config store entries. + +**delete_store(identity)** - Delete all configurations for an agent with the specified identity. Requires the +authorization capability 'edit_config_store'. By default agents have access to edit only their own config store entries. +Calls the agent's update_config with the action `DELETE_ALL` and no configuration name. -**get_configs()** - Get all of the configurations for an Agent. +**manage_delete_store(identity)** - +Deprecated method. Please use delete_store instead. Will be removed in VOLTTRON version 10. +Delete all configurations for an agent with the specified identity. Requires the +authorization capability 'edit_config_store'. By default agents have access to edit only their own config store entries. +Calls the agent's update_config with the action `DELETE_ALL` and no configuration name. -**delete_config(config_name, trigger_callback=False)** - Delete a configuration. +**list_configs(identity)** - Get a list of configurations for an agent with the specified identity. +**manage_list_configs(identity)** - +Deprecated method. Please use list_configs instead. Will be removed in VOLTTRON version 10. +Get a list of configurations for an agent with the specified identity. -Methods for Management -^^^^^^^^^^^^^^^^^^^^^^ +**list_stores()** - Get a list of all the agents with configurations. -**manage_store_config(identity, config_name, contents, config_type="raw")** - Change/create a configuration on the -platform for an agent with the specified identity +**manage_list_stores()** - +Deprecated method. Please use list_stores instead. Will be removed in VOLTTRON version 10. +Get a list of all the agents with configurations. -**manage_delete_config(identity, config_name)** - Delete a configuration for an agent with the specified identity. -Calls the agent's update_config with the action `DELETE_ALL` and no configuration name. -**manage_delete_store(identity)** - Delete all configurations for a :term:`VIP Identity`. +**get_config(identity, config_name, raw=True)** - Get the contents of a configuration file. If raw is set to +`True` this function will return the original file, otherwise it will return the parsed representation of the file. -**manage_list_config(identity)** - Get a list of configurations for an agent with the specified identity. +**manage_get_config(identity, config_name, raw=True)** - +Deprecated method. Please use get_config instead. Will be removed in VOLTTRON version 10. +Get the contents of a configuration file. If raw is set to `True` this function will return the original file, +otherwise it will return the parsed representation of the file. -**manage_get_config(identity, config_name, raw=True)** - Get the contents of a configuration file. If raw is set to -`True` this function will return the original file, otherwise it will return the parsed representation of the file. +**initialize_configs(identity)** - Called by an Agent at startup to trigger initial configuration state push. +Requires the authorization capability 'edit_config_store'. By default agents have access to edit only their own +config store entries. -**manage_list_stores()** - Get a list of all the agents with configurations. +**get_metadata(identity, config_name)** - Get the metadata of configuration named *config_name* of agent +identified by *identity*. Returns the type(json, csv, raw) of the configuration, modified date and actual content +**manage_get_metadata(identity, config_name)** - +Deprecated method. Please use get_metadata instead. Will be removed in VOLTTRON version 10. +Get the metadata of configuration named *config_name* of agent +identified by *identity*. Returns the type(json, csv, raw) of the configuration, modified date and actual content Direct Call Methods ^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/platform-features/message-bus/rabbitmq/rabbitmq-ssl-auth.rst b/docs/source/platform-features/message-bus/rabbitmq/rabbitmq-ssl-auth.rst index 5549b6beaf..37fc7efdc3 100644 --- a/docs/source/platform-features/message-bus/rabbitmq/rabbitmq-ssl-auth.rst +++ b/docs/source/platform-features/message-bus/rabbitmq/rabbitmq-ssl-auth.rst @@ -14,7 +14,7 @@ configurations can be seen by running the following command: .. code-block:: bash - cat ~/rabbitmq_server/rabbitmq_server-3.9.7/etc/rabbitmq/rabbitmq.conf + cat ~/rabbitmq_server/rabbitmq_server-3.9.29/etc/rabbitmq/rabbitmq.conf The configurations required to enable SSL: @@ -78,8 +78,8 @@ To configure RabbitMQ-VOLTTRON to use SSL based authentication, we need to add S # defaults to true ssl: 'true' - # defaults to ~/rabbitmq_server/rabbbitmq_server-3.9.7 - rmq-home: "~/rabbitmq_server/rabbitmq_server-3.9.7" + # defaults to ~/rabbitmq_server/rabbbitmq_server-3.9.29 + rmq-home: "~/rabbitmq_server/rabbitmq_server-3.9.29" The parameters of interest for SSL based configuration are diff --git a/docs/source/platform-features/message-bus/rabbitmq/rabbitmq-volttron.rst b/docs/source/platform-features/message-bus/rabbitmq/rabbitmq-volttron.rst index 249b382cd7..5d29152ee1 100644 --- a/docs/source/platform-features/message-bus/rabbitmq/rabbitmq-volttron.rst +++ b/docs/source/platform-features/message-bus/rabbitmq/rabbitmq-volttron.rst @@ -56,8 +56,8 @@ Path: `$VOLTTRON_HOME/rabbitmq_config.yml` # defaults to true ssl: 'true' - # defaults to ~/rabbitmq_server/rabbbitmq_server-3.9.7 - rmq-home: "~/rabbitmq_server/rabbitmq_server-3.9.7" + # defaults to ~/rabbitmq_server/rabbbitmq_server-3.9.29 + rmq-home: "~/rabbitmq_server/rabbitmq_server-3.9.29" Each VOLTTRON instance resides within a RabbitMQ virtual host. The name of the virtual host needs to be unique per VOLTTRON instance if there are multiple virtual instances within a single host/machine. The hostname needs to be able diff --git a/examples/CAgent/c_agent/agent.py b/examples/CAgent/c_agent/agent.py index 0d44579ff5..d0c952732c 100644 --- a/examples/CAgent/c_agent/agent.py +++ b/examples/CAgent/c_agent/agent.py @@ -40,8 +40,8 @@ from ctypes import CDLL, cdll, c_float from datetime import datetime import logging -import sys import os +import sys from volttron.platform.vip.agent import Agent, Core, PubSub, compat from volttron.platform.agent import utils diff --git a/examples/CAgent/c_agent/driver/cdriver.py b/examples/CAgent/c_agent/driver/cdriver.py index 59e63b123f..9939fb645a 100644 --- a/examples/CAgent/c_agent/driver/cdriver.py +++ b/examples/CAgent/c_agent/driver/cdriver.py @@ -70,7 +70,7 @@ def so_lookup_function(shared_object, function_name): return function class CRegister(BaseRegister): - def __init__(self,read_only, pointName, units, description = ''): + def __init__(self, read_only, pointName, units, description=''): super(CRegister, self).__init__("byte", read_only, pointName, units, description = '') diff --git a/examples/CSVDriver/csvdriver.py b/examples/CSVDriver/csvdriver.py index f06ac60c78..20259803aa 100644 --- a/examples/CSVDriver/csvdriver.py +++ b/examples/CSVDriver/csvdriver.py @@ -36,10 +36,12 @@ # under Contract DE-AC05-76RL01830 # }}} -import os -from platform_driver.interfaces import BaseInterface, BaseRegister, BasicRevert from csv import DictReader, DictWriter import logging +import os + +from platform_driver.interfaces import BaseInterface, BaseRegister, BasicRevert + # Use the csv fields and default dictionary to create a CSV "device" for testing CSV_FIELDNAMES = ["Point Name", "Point Value"] diff --git a/examples/CSVHistorian/csv_historian/historian.py b/examples/CSVHistorian/csv_historian/historian.py index ed08a94410..d0bd4b21f0 100644 --- a/examples/CSVHistorian/csv_historian/historian.py +++ b/examples/CSVHistorian/csv_historian/historian.py @@ -36,15 +36,14 @@ # under Contract DE-AC05-76RL01830 # }}} +import csv +import logging import os import sys -import logging from volttron.platform.agent import utils from volttron.platform.agent.base_historian import BaseHistorian -import csv - utils.setup_logging() _log = logging.getLogger(__name__) diff --git a/examples/ConfigActuation/tests/test_config_actuation.py b/examples/ConfigActuation/tests/test_config_actuation.py index 9fd17c1f3f..c134d5e38a 100644 --- a/examples/ConfigActuation/tests/test_config_actuation.py +++ b/examples/ConfigActuation/tests/test_config_actuation.py @@ -87,7 +87,7 @@ def publish_agent(request, volttron_instance): assert process.returncode == 0 # Add platform driver configuration files to config store. - cmd = ['volttron-ctl', 'config', 'store',PLATFORM_DRIVER, + cmd = ['volttron-ctl', 'config', 'store', PLATFORM_DRIVER, 'fake.csv', 'fake_unit_testing.csv', '--csv'] process = Popen(cmd, env=volttron_instance.env, cwd='scripts/scalability-testing', @@ -157,7 +157,7 @@ def test_thing(publish_agent): assert value == 10.0 publish_agent.vip.rpc.call(CONFIGURATION_STORE, - "manage_store", + "set_config", "config_actuation", "fakedriver", jsonapi.dumps({"SampleWritableFloat1": 42.0}), diff --git a/examples/ConfigActuation/update_config.py b/examples/ConfigActuation/update_config.py index abf1ebc774..f8e7624c4c 100644 --- a/examples/ConfigActuation/update_config.py +++ b/examples/ConfigActuation/update_config.py @@ -97,21 +97,21 @@ def main(): agent = build_agent(**get_keys()) files = agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_list_configs', + 'list_configs', vip_id).get(timeout=10) if filename not in files: config = {key: value} else: config = agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_get', + 'get_config', vip_id, filename).get(timeout=10) config = jsonapi.loads(config) config[key] = value agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_store', + 'set_config', vip_id, filename, jsonapi.dumps(config), diff --git a/examples/DataPublisher/datapublisher/agent.py b/examples/DataPublisher/datapublisher/agent.py index b461c1911c..c825063447 100644 --- a/examples/DataPublisher/datapublisher/agent.py +++ b/examples/DataPublisher/datapublisher/agent.py @@ -41,14 +41,15 @@ import re import sys -from volttron.platform.vip.agent import * from volttron.platform.agent import utils from volttron.platform.messaging.utils import normtopic from volttron.platform.messaging import headers as headers_mod +from volttron.platform.vip.agent import Agent, RPC import gevent from collections import defaultdict + utils.setup_logging() _log = logging.getLogger(__name__) __version__ = '4.0.0' diff --git a/examples/DataPuller/puller/agent.py b/examples/DataPuller/puller/agent.py index 18e897ae85..4c1682953e 100644 --- a/examples/DataPuller/puller/agent.py +++ b/examples/DataPuller/puller/agent.py @@ -139,7 +139,7 @@ def on_message(self, peer, sender, bus, topic, headers, message): _log.debug("data in capture_data {}".format(data)) if isinstance(data, dict): data = data - elif isinstance(data, (int,float)): + elif isinstance(data, (int, float)): data = data # else: # data = data[0] diff --git a/examples/EnergyPlusAgent/energyplus/agent.py b/examples/EnergyPlusAgent/energyplus/agent.py index 3535fd6ac8..bfc2941ed1 100644 --- a/examples/EnergyPlusAgent/energyplus/agent.py +++ b/examples/EnergyPlusAgent/energyplus/agent.py @@ -371,20 +371,19 @@ def check_advance(self): self.tns_actuate, headers={}, message={}).get(timeout=10) - else: - if self.EnergyPlus_sim.hour > self.EnergyPlus_sim.currenthour or self.EnergyPlus_sim.passtime: - self.EnergyPlus_sim.passtime = True - self.cosim_sync_counter += timestep - if self.cosim_sync_counter < self.EnergyPlus_sim.co_sim_timestep: - self.advance_simulation(None, None, None, None, None, None) - else: - self.cosim_sync_counter = 0 - self.vip.pubsub.publish('pubsub', - self.tns_actuate, - headers={}, - message={}).get(timeout=10) - else: + elif self.EnergyPlus_sim.hour > self.EnergyPlus_sim.currenthour or self.EnergyPlus_sim.passtime: + self.EnergyPlus_sim.passtime = True + self.cosim_sync_counter += timestep + if self.cosim_sync_counter < self.EnergyPlus_sim.co_sim_timestep: self.advance_simulation(None, None, None, None, None, None) + else: + self.cosim_sync_counter = 0 + self.vip.pubsub.publish('pubsub', + self.tns_actuate, + headers={}, + message={}).get(timeout=10) + else: + self.advance_simulation(None, None, None, None, None, None) return diff --git a/examples/ExampleMatlabApplication/matlab/matlab_example.py b/examples/ExampleMatlabApplication/matlab/matlab_example.py index e0f5842469..6d7e73a2b0 100644 --- a/examples/ExampleMatlabApplication/matlab/matlab_example.py +++ b/examples/ExampleMatlabApplication/matlab/matlab_example.py @@ -21,11 +21,11 @@ print("Sending config_params") config_params = {"zone_temperature_list": ["ZoneTemperature1", "ZoneTemperature2"], "zone_setpoint_list": ["ZoneTemperatureSP1", "ZoneTemperatureSP2"]} - config_socket.send_json(config_params,zmq.NOBLOCK) + config_socket.send_json(config_params, zmq.NOBLOCK) print("Sending data") data = {"zone_temperature_list": ["72.3", "78.5"]} - data_socket.send_json(data,zmq.NOBLOCK) + data_socket.send_json(data, zmq.NOBLOCK) except ZMQError: print("No Matlab process running to send message") diff --git a/examples/ExampleSubscriber/subscriber/subscriber_agent.py b/examples/ExampleSubscriber/subscriber/subscriber_agent.py index 2ea9b899d0..694c295a25 100644 --- a/examples/ExampleSubscriber/subscriber/subscriber_agent.py +++ b/examples/ExampleSubscriber/subscriber/subscriber_agent.py @@ -164,7 +164,7 @@ def lookup_data(self): order = "FIRST_TO_LAST").get(timeout=10) print('Query Result', result) except Exception as e: - print ("Could not contact historian. Is it running?") + print("Could not contact historian. Is it running?") print(e) @Core.schedule(periodic(10)) @@ -176,9 +176,9 @@ def pub_fake_data(self): ''' #Make some random readings - oat_reading = random.uniform(30,100) - mixed_reading = oat_reading + random.uniform(-5,5) - damper_reading = random.uniform(0,100) + oat_reading = random.uniform(30, 100) + mixed_reading = oat_reading + random.uniform(-5, 5) + damper_reading = random.uniform(0, 100) # Create a message for all points. all_message = [{'OutsideAirTemperature': oat_reading, 'MixedAirTemperature': mixed_reading, diff --git a/examples/FNCS/fncs_example/agent.py b/examples/FNCS/fncs_example/agent.py index 011135a1a1..220a56197e 100644 --- a/examples/FNCS/fncs_example/agent.py +++ b/examples/FNCS/fncs_example/agent.py @@ -46,7 +46,7 @@ def fncs_example(config_path, **kwargs): stop_agent_when_sim_complete = config.get("stop_agent_when_sim_complete", False) subscription_topic = config.get("subscription_topic", None) return FncsExample(topic_mapping=topic_mapping, federate_name=federate, broker_location=broker_location, - time_delta=time_delta,subscription_topic=subscription_topic, sim_length=sim_length, + time_delta=time_delta, subscription_topic=subscription_topic, sim_length=sim_length, stop_agent_when_sim_complete=stop_agent_when_sim_complete, **kwargs) @@ -56,7 +56,7 @@ class FncsExample(Agent): """ def __init__(self, topic_mapping, federate_name=None, broker_location="tcp://localhost:5570", - time_delta="1s",subscription_topic=None, simulation_start_time=None, sim_length="10s", + time_delta="1s", subscription_topic=None, simulation_start_time=None, sim_length="10s", stop_agent_when_sim_complete=False, **kwargs): super(FncsExample, self).__init__(enable_fncs=True, enable_store=False, **kwargs) _log.debug("vip_identity: " + self.core.identity) diff --git a/examples/HELICS/helics_federate.py b/examples/HELICS/helics_federate.py index 5674aca904..2b8d42a94f 100644 --- a/examples/HELICS/helics_federate.py +++ b/examples/HELICS/helics_federate.py @@ -73,7 +73,7 @@ def federate_example(config_path): endid = {} subid = {} pubid = {} - for i in range(0,endpoint_count): + for i in range(0, endpoint_count): endid["m{}".format(i)] = h.helicsFederateGetEndpointByIndex(fed, i) end_name = h.helicsEndpointGetName(endid["m{}".format(i)]) logger.info( 'Registered Endpoint ---> {}'.format(end_name)) diff --git a/examples/MarketAgents/AHUAgent/ahu/agent.py b/examples/MarketAgents/AHUAgent/ahu/agent.py index 7b956fb156..e0f0402bd1 100644 --- a/examples/MarketAgents/AHUAgent/ahu/agent.py +++ b/examples/MarketAgents/AHUAgent/ahu/agent.py @@ -79,14 +79,16 @@ def ahu_agent(config_path, **kwargs): c3= config.get('c3') COP= config.get('COP') verbose_logging= config.get('verbose_logging', True) - return AHUAgent(air_market_name,electric_market_name,agent_name,subscribing_topic,c0,c1,c2,c3,COP,verbose_logging, **kwargs) + return AHUAgent(air_market_name, electric_market_name, agent_name, + subscribing_topic, c0, c1, c2, c3, COP, verbose_logging, **kwargs) class AHUAgent(MarketAgent, AhuChiller): """ The SampleElectricMeterAgent serves as a sample of an electric meter that sells electricity for a single building at a fixed price. """ - def __init__(self, air_market_name, electric_market_name, agent_name,subscribing_topic,c0,c1,c2,c3,COP,verbose_logging, **kwargs): + def __init__(self, air_market_name, electric_market_name, agent_name, + subscribing_topic, c0, c1, c2, c3, COP, verbose_logging, **kwargs): super(AHUAgent, self).__init__(verbose_logging, **kwargs) self.air_market_name = air_market_name @@ -155,10 +157,10 @@ def create_air_supply_curve(self, electric_price, electric_quantity): supply_curve = PolyLine() price = 65 quantity = 100000 - supply_curve.add(Point(price=price,quantity=quantity)) + supply_curve.add(Point(price=price, quantity=quantity)) price = 65 quantity = 0 # negative quantities are not real -1*10000 - supply_curve.add(Point(price=price,quantity=quantity)) + supply_curve.add(Point(price=price, quantity=quantity)) return supply_curve def create_electric_demand_curve(self, aggregate_air_demand): diff --git a/examples/NodeRed/node_red_publisher.py b/examples/NodeRed/node_red_publisher.py index d537b5c86f..b783a6492e 100644 --- a/examples/NodeRed/node_red_publisher.py +++ b/examples/NodeRed/node_red_publisher.py @@ -1,10 +1,9 @@ from datetime import datetime +import logging import os import sys - import gevent -import logging from gevent.core import callback from gevent import Timeout diff --git a/examples/NodeRed/node_red_subscriber.py b/examples/NodeRed/node_red_subscriber.py index f8206a7c5d..6658a62dc6 100644 --- a/examples/NodeRed/node_red_subscriber.py +++ b/examples/NodeRed/node_red_subscriber.py @@ -2,7 +2,6 @@ import os import sys - import gevent from volttron.platform.messaging import headers as headers_mod diff --git a/examples/SCPAgent/trigger_scp.py b/examples/SCPAgent/trigger_scp.py index 31a40adf8b..63c05239cd 100644 --- a/examples/SCPAgent/trigger_scp.py +++ b/examples/SCPAgent/trigger_scp.py @@ -35,12 +35,14 @@ # BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY # under Contract DE-AC05-76RL01830 # }}} -from pathlib import Path + +import argparse import os +from pathlib import Path import shutil -import argparse import gevent + from volttron.platform.vip.agent.utils import build_agent diff --git a/examples/SchedulerExample/schedule_example/agent.py b/examples/SchedulerExample/schedule_example/agent.py index 3371418261..9552d88f6e 100644 --- a/examples/SchedulerExample/schedule_example/agent.py +++ b/examples/SchedulerExample/schedule_example/agent.py @@ -95,7 +95,7 @@ def startup(self, sender, **kwargs): @PubSub.subscribe('pubsub', topics.ACTUATOR_SCHEDULE_ANNOUNCE(campus='campus', building='building',unit='unit')) def actuate(self, peer, sender, bus, topic, headers, message): - print ("response:",topic,headers,message) + print("response:", topic, headers, message) if headers[headers_mod.REQUESTER_ID] != agent_id: return '''Match the announce for our fake device with our ID diff --git a/examples/StandAloneFileWatcher/standalonefilewatchpublisher.py b/examples/StandAloneFileWatcher/standalonefilewatchpublisher.py index 5b1cf17f40..9ddb5a95a3 100644 --- a/examples/StandAloneFileWatcher/standalonefilewatchpublisher.py +++ b/examples/StandAloneFileWatcher/standalonefilewatchpublisher.py @@ -1,17 +1,15 @@ from datetime import datetime +import logging import os import sys import gevent -import logging - from volttron.platform.vip.agent import Agent, PubSub, RPC, Core from volttron.platform.agent import utils from volttron.platform.agent.utils import watch_file_with_fullpath from volttron.platform import jsonapi - # These are the options that can be set from the settings module. from settings import remote_url, config diff --git a/examples/StandAloneListener/standalonelistener.py b/examples/StandAloneListener/standalonelistener.py index 4acd2b61ce..f326cf2410 100644 --- a/examples/StandAloneListener/standalonelistener.py +++ b/examples/StandAloneListener/standalonelistener.py @@ -1,9 +1,9 @@ from datetime import datetime +import logging import os import sys import gevent -import logging from volttron.platform.messaging import headers as headers_mod from volttron.platform.vip.agent import Agent, PubSub, Core diff --git a/examples/StandAloneMatLab/standalone_matlab.py b/examples/StandAloneMatLab/standalone_matlab.py index 325e335fe9..2d07cd2038 100644 --- a/examples/StandAloneMatLab/standalone_matlab.py +++ b/examples/StandAloneMatLab/standalone_matlab.py @@ -1,10 +1,10 @@ from scriptwrapper import script_runner -import os -import sys -import json import gevent +import json import logging +import os +import sys from volttron.platform.vip.agent import Agent, PubSub, Core from volttron.platform.agent import utils diff --git a/examples/StandAloneWithAuth/standalonewithauth.py b/examples/StandAloneWithAuth/standalonewithauth.py index f0c4284c63..77598b0602 100644 --- a/examples/StandAloneWithAuth/standalonewithauth.py +++ b/examples/StandAloneWithAuth/standalonewithauth.py @@ -21,11 +21,11 @@ """ from datetime import datetime +import logging import os import sys import gevent -import logging from gevent.core import callback from volttron.platform.vip.agent import Agent, Core, RPC diff --git a/examples/configurations/drivers/dnp3.csv b/examples/configurations/drivers/dnp3.csv index 571d2e9a8e..e71b832b3d 100644 --- a/examples/configurations/drivers/dnp3.csv +++ b/examples/configurations/drivers/dnp3.csv @@ -1,13 +1,17 @@ -Volttron Point Name,Group,Index,Scaling,Units,Writable -DCHD.WTgt,41,65,1.0,NA,FALSE -DCHD.WTgt-In,30,90,1.0,NA,TRUE -DCHD.WinTms,41,66,1.0,NA,FALSE -DCHD.RmpTms,41,67,1.0,NA,FALSE -DCHD.RevtTms,41,68,1.0,NA,FALSE -DCHD.RmpUpRte,41,69,1.0,NA,FALSE -DCHD.RmpDnRte,41,70,1.0,NA,FALSE -DCHD.ChaRmpUpRte,41,71,1.0,NA,FALSE -DCHD.ChaRmpDnRte,41,72,1.0,NA,FALSE -DCHD.ModPrty,41,9,1.0,NA,FALSE -DCHD.VArAct,41,10,1.0,NA,FALSE -DCHD.ModEna,12,5,1.0,NA,FALSE +Point Name,Volttron Point Name,Group,Variation,Index,Scaling,Units,Writable,Notes +AnalogInput_index0,AnalogInput_index0,30,6,0,1,NA,FALSE,Double Analogue input without status +AnalogInput_index1,AnalogInput_index1,30,6,1,1,NA,FALSE,Double Analogue input without status +AnalogInput_index2,AnalogInput_index2,30,6,2,1,NA,FALSE,Double Analogue input without status +AnalogInput_index3,AnalogInput_index3,30,6,3,1,NA,FALSE,Double Analogue input without status +BinaryInput_index0,BinaryInput_index0,1,2,0,1,NA,FALSE,Single bit binary input with status +BinaryInput_index1,BinaryInput_index1,1,2,1,1,NA,FALSE,Single bit binary input with status +BinaryInput_index2,BinaryInput_index2,1,2,2,1,NA,FALSE,Single bit binary input with status +BinaryInput_index3,BinaryInput_index3,1,2,3,1,NA,FALSE,Single bit binary input with status +AnalogOutput_index0,AnalogOutput_index0,40,4,0,1,NA,TRUE,Double-precision floating point with flags +AnalogOutput_index1,AnalogOutput_index1,40,4,1,1,NA,TRUE,Double-precision floating point with flags +AnalogOutput_index2,AnalogOutput_index2,40,4,2,1,NA,TRUE,Double-precision floating point with flags +AnalogOutput_index3,AnalogOutput_index3,40,4,3,1,NA,TRUE,Double-precision floating point with flags +BinaryOutput_index0,BinaryOutput_index0,10,2,0,1,NA,TRUE,Binary Output with flags +BinaryOutput_index1,BinaryOutput_index1,10,2,1,1,NA,TRUE,Binary Output with flags +BinaryOutput_index2,BinaryOutput_index2,10,2,2,1,NA,TRUE,Binary Output with flags +BinaryOutput_index3,BinaryOutput_index3,10,2,3,1,NA,TRUE,Binary Output with flags diff --git a/examples/configurations/drivers/test_dnp3.config b/examples/configurations/drivers/test_dnp3.config index 7296eae4bc..3f66b07e9f 100644 --- a/examples/configurations/drivers/test_dnp3.config +++ b/examples/configurations/drivers/test_dnp3.config @@ -1,13 +1,14 @@ { - "driver_config": { - "dnp3_agent_id": "dnp3agent" - }, - "campus": "campus", - "building": "building", - "unit": "dnp3", - "driver_type": "dnp3", - "registry_config": "config://dnp3.csv", - "interval": 15, - "timezone": "US/Pacific", - "heart_beat_point": "Heartbeat" -} \ No newline at end of file + "driver_config": {"master_ip": "0.0.0.0", "outstation_ip": "127.0.0.1", + "master_id": 2, "outstation_id": 1, + "port": 20000}, + "registry_config":"config://udd-Dnp3.csv", + "driver_type": "udd_dnp3", + "interval": 5, + "timezone": "UTC", + "campus": "campus-vm", + "building": "building-vm", + "unit": "Dnp3", + "publish_depth_first_all": true, + "heart_beat_point": "random_bool" +} diff --git a/examples/configurations/rabbitmq/rabbitmq_config.yml b/examples/configurations/rabbitmq/rabbitmq_config.yml index e6bf802fdc..e34a3a97bc 100644 --- a/examples/configurations/rabbitmq/rabbitmq_config.yml +++ b/examples/configurations/rabbitmq/rabbitmq_config.yml @@ -48,8 +48,8 @@ ssl: true #use-existing-certs: True -# defaults to ~/rabbitmq_server/rabbbitmq_server-3.9.7 -rmq-home: ~/rabbitmq_server/rabbitmq_server-3.9.7 +# defaults to ~/rabbitmq_server/rabbbitmq_server-3.9.29 +rmq-home: ~/rabbitmq_server/rabbitmq_server-3.9.29 # RabbitMQ reconnect retry delay (in seconds) reconnect-delay: 30 diff --git a/integrations/energyplus_integration.py b/integrations/energyplus_integration.py index bb178d5e5e..55b8cfb71b 100644 --- a/integrations/energyplus_integration.py +++ b/integrations/energyplus_integration.py @@ -36,14 +36,16 @@ # under Contract DE-AC05-76RL01830 # }}} -import os import logging -from gevent import monkey, sleep -import weakref +from calendar import monthrange +from datetime import datetime +import os import socket import subprocess -from datetime import datetime -from calendar import monthrange +import weakref + +from gevent import monkey, sleep + from volttron.platform.agent.base_simulation_integration.base_sim_integration import BaseSimIntegration monkey.patch_socket() diff --git a/integrations/gridappsd_integration.py b/integrations/gridappsd_integration.py index 1ccb6ed5f7..dc9d763fef 100644 --- a/integrations/gridappsd_integration.py +++ b/integrations/gridappsd_integration.py @@ -46,11 +46,12 @@ HAS_GAPPSD = False RuntimeError('GridAPPSD must be installed before running this script ') -import os import logging -import gevent +import os import weakref +import gevent + from volttron.platform.agent.base_simulation_integration.base_sim_integration import BaseSimIntegration _log = logging.getLogger(__name__) diff --git a/integrations/helics_integration.py b/integrations/helics_integration.py index 2a660f4cac..f8b71707eb 100644 --- a/integrations/helics_integration.py +++ b/integrations/helics_integration.py @@ -43,13 +43,16 @@ HAS_HELICS = False RuntimeError('HELICS must be installed before running this script ') -import os +from copy import deepcopy import logging -import gevent +import os import weakref + +import gevent + from volttron.platform.agent.base_simulation_integration.base_sim_integration import BaseSimIntegration from volttron.platform import jsonapi -from copy import deepcopy + _log = logging.getLogger(__name__) __version__ = '1.0' @@ -351,5 +354,3 @@ def stop_simulation(self, *args, **kwargs): h.helicsCloseLibrary() except h._helics.HelicsException as e: _log.exception("Error stopping HELICS federate {}".format(e)) - - diff --git a/requirements.py b/requirements.py index bac0d08fcc..26f7e32a77 100644 --- a/requirements.py +++ b/requirements.py @@ -61,14 +61,15 @@ 'tzlocal==2.1', #'pyOpenSSL==19.0.0', 'cryptography==37.0.4', - 'watchdog-gevent==0.1.1'] + 'watchdog-gevent==0.1.1', + 'deprecated==1.2.14'] extras_require = {'crate': ['crate==0.27.1'], 'databases': ['mysql-connector-python==8.0.30', - 'pymongo==4.2.0', + 'pymongo==4.5.0', 'crate==0.27.1', 'influxdb==5.3.1', - 'psycopg2-binary==2.8.6'], + 'psycopg2-binary==2.9.7'], 'documentation': ['mock==4.0.3', 'docutils<0.18', 'sphinx-rtd-theme==1.0.0', @@ -80,10 +81,10 @@ 'pyserial==3.5'], 'influxdb': ['influxdb==5.3.1'], 'market': ['numpy==1.23.1', 'transitions==0.8.11'], - 'mongo': ['pymongo==4.2.0'], + 'mongo': ['pymongo==4.5.0'], 'mysql': ['mysql-connector-python==8.0.30'], 'pandas': ['numpy==1.23.1', 'pandas==1.4.3'], - 'postgres': ['psycopg2-binary==2.8.6'], + 'postgres': ['psycopg2-binary==2.9.7'], # This is installed in bootstrap.py itself so we don't # include here, though we include the version number here # @@ -105,4 +106,6 @@ 'passlib==1.7.4', 'argon2-cffi==21.3.0', 'Werkzeug==2.2.1', - 'treelib==1.6.1']} + 'treelib==1.6.1'], + 'dnp3': ['dnp3-python==0.2.3b3'], + 'openadr': ['openleadr==0.5.30']} diff --git a/scripts/dnp3/get_point_demo.py b/scripts/dnp3/get_point_demo.py new file mode 100644 index 0000000000..bb1e090383 --- /dev/null +++ b/scripts/dnp3/get_point_demo.py @@ -0,0 +1,41 @@ +""" +A demo to test dnp3-driver get_point method using rpc call. + +Pre-requisite: +- install platform-driver +- configure dnp3-driver +- a dnp3 outstation/server is up and running +- platform-driver is up and running +""" + +import random +from volttron.platform.vip.agent.utils import build_agent +from time import sleep +import datetime + + +def main(): + a = build_agent() + while True: + sleep(5) + print("============") + try: + rpc_method = "get_point" + device_name = "campus-vm/building-vm/Dnp3" + + reg_pt_name = "AnalogInput_index0" + rs = a.vip.rpc.call("platform.driver", rpc_method, + device_name, + reg_pt_name).get(timeout=10) + print(datetime.datetime.now(), "point_name: ", reg_pt_name, "value: ", rs) + reg_pt_name = "AnalogInput_index1" + rs = a.vip.rpc.call("platform.driver", rpc_method, + device_name, + reg_pt_name).get(timeout=10) + print(datetime.datetime.now(), "point_name: ", reg_pt_name, "value: ", rs) + except Exception as e: + print(e) + + +if __name__ == "__main__": + main() diff --git a/scripts/dnp3/set_point_demo.py b/scripts/dnp3/set_point_demo.py new file mode 100644 index 0000000000..4f8de9115f --- /dev/null +++ b/scripts/dnp3/set_point_demo.py @@ -0,0 +1,52 @@ +""" +A demo to test dnp3-driver set_point method using rpc call. + +Pre-requisite: +- install platform-driver +- configure dnp3-driver +- a dnp3 outstation/server is up and running +- platform-driver is up and running +""" + +import random +from volttron.platform.vip.agent.utils import build_agent +from time import sleep +import datetime + + +def main(): + a = build_agent() + while True: + sleep(5) + print("============") + try: + rpc_method = "set_point" + device_name = "campus-vm/building-vm/Dnp3" + + for i in range(3): + reg_pt_name = "AnalogOutput_index" + str(i) + val_to_set = random.random() + rs = a.vip.rpc.call("platform.driver", rpc_method, + device_name, + reg_pt_name, + val_to_set).get(timeout=10) + print(datetime.datetime.now(), "point_name: ", reg_pt_name, "response: ", rs) + + # verify + sleep(1) + + for i in range(3): + rpc_method = "get_point" + reg_pt_name = "AnalogOutput_index" + str(i) + # val_to_set = random.random() + rs = a.vip.rpc.call("platform.driver", rpc_method, + device_name, + reg_pt_name + ).get(timeout=10) + print(datetime.datetime.now(), "point_name: ", reg_pt_name, "response: ", rs) + except Exception as e: + print(e) + + +if __name__ == "__main__": + main() diff --git a/scripts/extract_config_store.py b/scripts/extract_config_store.py index 5e56afa817..583feebb2b 100644 --- a/scripts/extract_config_store.py +++ b/scripts/extract_config_store.py @@ -74,7 +74,7 @@ def get_configs(config_id, output_directory): event.wait() config_list = agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_list_configs', + 'list_configs', config_id).get(timeout=10) if not config_list: @@ -88,7 +88,7 @@ def get_configs(config_id, output_directory): for config in config_list: print("Retrieving configuration", config) raw_config = agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_get', + 'get_config', config_id, config, raw=True).get(timeout=10) diff --git a/scripts/historian-scripts/install-dbs-ubuntu-2004.sh b/scripts/historian-scripts/install-dbs-ubuntu-2004.sh new file mode 100755 index 0000000000..91cbe97564 --- /dev/null +++ b/scripts/historian-scripts/install-dbs-ubuntu-2004.sh @@ -0,0 +1,110 @@ +#! /bin/bash + + # Utility script for installing mysql, mongodb, and postgresql on Ubuntu 22.04 and creates test data base and test + # db user for testing volttron agents that use these databases. Installs databases on a /databases + # folder and assumes the unix user running VOLTTRON is "volttron" + # You can use this as a reference, update database versions and user name as needed to install database environment for + # testing volttron agents + # To run provide execute permissions and pass one or more database names as input + # For example + # ./install-dbs-ubuntu-2204.sh mongodb mysql postgresql + # ./install-dbs-ubuntu-2204.sh mongodb + # ./install-dbs-ubuntu-2204.sh mysql + +function install_mongodb(){ + mkdir -p /databases/mongodb + cd /databases/mongodb + sudo apt-get install -y libcurl4 openssl liblzma5 + wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2004-7.0.1.tgz + tar -xvzf mongodb-linux-x86_64-ubuntu2004-7.0.1.tgz + ln -s mongodb-linux-x86_64-ubuntu2004-7.0.1 mongodb + mkdir data + mkdir log + chown -R volttron /databases/mongodb + export PATH=$PATH:/databases/mongodb/mongodb/bin + echo 'export PATH=$PATH:/databases/mongodb/mongodb/bin' >> /home/volttron/.bashrc + echo "alias start_mongo='mongod --dbpath /databases/mongodb/data --logpath /databases/mongodb/log/mongod.log --fork'" >> /home/volttron/.bash_aliases + echo "alias stop_mongo='mongod --dbpath /databases/mongodb/data --logpath /databases/mongodb/log/mongod.log --shutdown'" >> /home/volttron/.bash_aliases + su volttron -c "/databases/mongodb/mongodb/bin/mongod --dbpath /databases/mongodb/data --logpath /databases/mongodb/log/mongod.log --fork" + wget https://downloads.mongodb.com/compass/mongosh-1.10.6-linux-x64-openssl11.tgz + tar -xvzf mongosh-1.10.6-linux-x64-openssl11.tgz + mv mongosh-1.10.6-linux-x64-openssl11/bin/* /databases/mongodb/mongodb/bin + chmod a+x /databases/mongodb/mongodb/bin/mongosh + mongosh admin --eval 'db.createUser( {user: "admin", pwd: "volttron", roles: [ { role: "userAdminAnyDatabase", db: "admin" }]});' + mongosh test_historian -u admin -p volttron --authenticationDatabase admin --eval 'db.createUser( {user: "historian", pwd: "historian", roles: [ { role: "readWrite", db: "test_historian" }]});' + su volttron -c "/databases/mongodb/mongodb/bin/mongod --dbpath /databases/mongodb/data --logpath /databases/mongodb/log/mongod.log --shutdown" +} + + +function install_mysql(){ + apt-get install -y libaio1 libncurses5 libnuma1 + mkdir -p /databases/mysql + cd /databases/mysql + wget https://downloads.mysql.com/archives/get/p/23/file/mysql-8.0.25-linux-glibc2.12-x86_64.tar.xz + tar -xvf mysql-8.0.25-linux-glibc2.12-x86_64.tar.xz + ln -s mysql-8.0.25-linux-glibc2.12-x86_64 mysql + groupadd mysql + useradd -r -g mysql -s /bin/false mysql + mkdir mysql-files data etc log + chmod 750 mysql-files data etc log + echo "[mysqld]" > etc/my.cnf + echo "basedir=/databases/mysql/mysql" >> etc/my.cnf + echo "datadir=/databases/mysql/data" >> etc/my.cnf + echo "log-error=/databases/mysql/log/mysql.err" >> etc/my.cnf + cd /databases/mysql + cp mysql/support-files/mysql.server mysql/bin + chown -R mysql:mysql /databases/mysql + export PATH=/databases/mysql/mysql/bin:$PATH + echo 'export PATH=/databases/mysql/mysql/bin:$PATH' >> /root/.bashrc + echo 'export PATH=/databases/mysql/mysql/bin:$PATH' >> /home/volttron/.bashrc + echo "alias start_mysql='sudo /databases/mysql/mysql/bin/mysql.server start'" >> /home/volttron/.bash_aliases + echo "alias stop_mysql='sudo /databases/mysql/mysql/bin/mysql.server stop'" >> /home/volttron/.bash_aliases + mysqld --defaults-file=/databases/mysql/etc/my.cnf --initialize-insecure --user=mysql + sed -i 's/^basedir=/basedir=\/databases\/mysql\/mysql/' /databases/mysql/mysql/bin/mysql.server + sed -i 's/^datadir=/datadir=\/databases\/mysql\/data/' /databases/mysql/mysql/bin/mysql.server + mysql.server start + mysql -u root -e "CREATE DATABASE test_historian;" + mysql -u root -e "CREATE USER 'historian'@'localhost' IDENTIFIED BY 'historian';" + mysql -u root -e "GRANT SELECT, INSERT, DELETE, CREATE, INDEX, UPDATE, DROP ON test_historian.* TO 'historian'@'localhost';" + mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'volttron';" + mysql.server stop +} + + +function install_postgresql(){ + apt-get install -y libreadline8 libreadline-dev zlib1g-dev + mkdir -p /databases/postgresql + cd /databases/postgresql + wget https://ftp.postgresql.org/pub/source/v10.16/postgresql-10.16.tar.gz + tar -xvzf postgresql-10.16.tar.gz + ln -s postgresql-10.16 postgresql_source + cd postgresql_source + ./configure --prefix=/databases/postgresql/pgsql + make + make install + echo 'export LD_LIBRARY_PATH=/databases/postgresql/pgsql/lib' >> /home/volttron/.bashrc + export LD_LIBRARY_PATH=/databases/postgresql/pgsql/lib + echo 'export PATH=/databases/postgresql/pgsql/bin:$PATH' >> /home/volttron/.bashrc + export PATH=/databases/postgresql/pgsql/bin:$PATH + export LD_LIBRARY_PATH=/databases/postgresql/pgsql/lib + ln -s /databases/postgresql/pgsql/lib/libpq.so.5 /usr/lib/libpq.so.5 + adduser --disabled-password --gecos "" postgres + mkdir /databases/postgresql/pgsql/data + chown -R postgres /databases/postgresql + su postgres -c "/databases/postgresql/pgsql/bin/initdb -D /databases/postgresql/pgsql/data" + echo "alias start_postgres='sudo su postgres -c \"/databases/postgresql/pgsql/bin/pg_ctl -D /databases/postgresql/pgsql/data -l /databases/postgresql/logfile start\"'" >> /home/volttron/.bash_aliases + echo "alias stop_postgres='sudo su postgres -c \"/databases/postgresql/pgsql/bin/pg_ctl -D /databases/postgresql/pgsql/data -l /databases/postgresql/logfile stop\"'" >> /home/volttron/.bash_aliases + sudo su postgres -c "/databases/postgresql/pgsql/bin/pg_ctl -D /databases/postgresql/pgsql/data -l /databases/postgresql/logfile start" + psql -U postgres -c 'CREATE DATABASE test_historian;' + psql -U postgres -c "CREATE USER historian with encrypted password 'historian';" + psql -U postgres -c "GRANT ALL PRIVILEGES on database test_historian to historian;" + sudo su postgres -c "/databases/postgresql/pgsql/bin/pg_ctl -D /databases/postgresql/pgsql/data -l /databases/postgresql/logfile stop" +} + +echo "Configured to install dbs: $@" +v=( "$@" ) +for i in ${v[@]} +do + echo "Calling install_$i" + install_$i +done diff --git a/scripts/historian-scripts/install-dbs-ubuntu-2204.sh b/scripts/historian-scripts/install-dbs-ubuntu-2204.sh new file mode 100755 index 0000000000..694d5b7b6f --- /dev/null +++ b/scripts/historian-scripts/install-dbs-ubuntu-2204.sh @@ -0,0 +1,110 @@ +#! /bin/bash + + # Utility script for installing mysql, mongodb, and postgresql on Ubuntu 22.04 and creates test data base and test + # db user for testing volttron agents that use these databases. Installs databases on a /databases + # folder and assumes the unix user running VOLTTRON is "volttron" + # You can use this as a reference, update database versions and user name as needed to install database environment for + # testing volttron agents + # To run provide execute permissions and pass one or more database names as input + # For example + # ./install-dbs-ubuntu-2204.sh mongodb mysql postgresql + # ./install-dbs-ubuntu-2204.sh mongodb + # ./install-dbs-ubuntu-2204.sh mysql + +function install_mongodb(){ + mkdir -p /databases/mongodb + cd /databases/mongodb + sudo apt-get install -y libcurl4 libgssapi-krb5-2 libldap-2.5-0 libwrap0 libsasl2-2 libsasl2-modules libsasl2-modules-gssapi-mit openssl liblzma5 + wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-7.0.1.tgz + tar -xvzf mongodb-linux-x86_64-ubuntu2204-7.0.1.tgz + ln -s mongodb-linux-x86_64-ubuntu2204-7.0.1 mongodb + mkdir data + mkdir log + chown -R volttron /databases/mongodb + export PATH=$PATH:/databases/mongodb/mongodb/bin + echo 'export PATH=$PATH:/databases/mongodb/mongodb/bin' >> /home/volttron/.bashrc + echo "alias start_mongo='mongod --dbpath /databases/mongodb/data --logpath /databases/mongodb/log/mongod.log --fork'" >> /home/volttron/.bash_aliases + echo "alias stop_mongo='mongod --dbpath /databases/mongodb/data --logpath /databases/mongodb/log/mongod.log --shutdown'" >> /home/volttron/.bash_aliases + su volttron -c "/databases/mongodb/mongodb/bin/mongod --dbpath /databases/mongodb/data --logpath /databases/mongodb/log/mongod.log --fork" + wget https://downloads.mongodb.com/compass/mongosh-1.10.6-linux-x64-openssl3.tgz + tar -xvzf mongosh-1.10.6-linux-x64-openssl3.tgz + mv mongosh-1.10.6-linux-x64-openssl3/bin/* /databases/mongodb/mongodb/bin + chmod a+x /databases/mongodb/mongodb/bin/mongosh + mongosh admin --eval 'db.createUser( {user: "admin", pwd: "volttron", roles: [ { role: "userAdminAnyDatabase", db: "admin" }]});' + mongosh test_historian -u admin -p volttron --authenticationDatabase admin --eval 'db.createUser( {user: "historian", pwd: "historian", roles: [ { role: "readWrite", db: "test_historian" }]});' + su volttron -c "/databases/mongodb/mongodb/bin/mongod --dbpath /databases/mongodb/data --logpath /databases/mongodb/log/mongod.log --shutdown" +} + + +function install_mysql(){ + apt-get install -y libaio1 libncurses5 libnuma1 + mkdir -p /databases/mysql + cd /databases/mysql + wget https://downloads.mysql.com/archives/get/p/23/file/mysql-8.0.25-linux-glibc2.12-x86_64.tar.xz + tar -xvf mysql-8.0.25-linux-glibc2.12-x86_64.tar.xz + ln -s mysql-8.0.25-linux-glibc2.12-x86_64 mysql + groupadd mysql + useradd -r -g mysql -s /bin/false mysql + mkdir mysql-files data etc log + chmod 750 mysql-files data etc log + echo "[mysqld]" > etc/my.cnf + echo "basedir=/databases/mysql/mysql" >> etc/my.cnf + echo "datadir=/databases/mysql/data" >> etc/my.cnf + echo "log-error=/databases/mysql/log/mysql.err" >> etc/my.cnf + cd /databases/mysql + cp mysql/support-files/mysql.server mysql/bin + chown -R mysql:mysql /databases/mysql + export PATH=/databases/mysql/mysql/bin:$PATH + echo 'export PATH=/databases/mysql/mysql/bin:$PATH' >> /root/.bashrc + echo 'export PATH=/databases/mysql/mysql/bin:$PATH' >> /home/volttron/.bashrc + echo "alias start_mysql='sudo /databases/mysql/mysql/bin/mysql.server start'" >> /home/volttron/.bash_aliases + echo "alias stop_mysql='sudo /databases/mysql/mysql/bin/mysql.server stop'" >> /home/volttron/.bash_aliases + mysqld --defaults-file=/databases/mysql/etc/my.cnf --initialize-insecure --user=mysql + sed -i 's/^basedir=/basedir=\/databases\/mysql\/mysql/' /databases/mysql/mysql/bin/mysql.server + sed -i 's/^datadir=/datadir=\/databases\/mysql\/data/' /databases/mysql/mysql/bin/mysql.server + mysql.server start + mysql -u root -e "CREATE DATABASE test_historian;" + mysql -u root -e "CREATE USER 'historian'@'localhost' IDENTIFIED BY 'historian';" + mysql -u root -e "GRANT SELECT, INSERT, DELETE, CREATE, INDEX, UPDATE, DROP ON test_historian.* TO 'historian'@'localhost';" + mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'volttron';" + mysql.server stop +} + + +function install_postgresql(){ + apt-get install -y libreadline8 libreadline-dev zlib1g-dev + mkdir -p /databases/postgresql + cd /databases/postgresql + wget https://ftp.postgresql.org/pub/source/v10.16/postgresql-10.16.tar.gz + tar -xvzf postgresql-10.16.tar.gz + ln -s postgresql-10.16 postgresql_source + cd postgresql_source + ./configure --prefix=/databases/postgresql/pgsql + make + make install + echo 'export LD_LIBRARY_PATH=/databases/postgresql/pgsql/lib' >> /home/volttron/.bashrc + export LD_LIBRARY_PATH=/databases/postgresql/pgsql/lib + echo 'export PATH=/databases/postgresql/pgsql/bin:$PATH' >> /home/volttron/.bashrc + export PATH=/databases/postgresql/pgsql/bin:$PATH + export LD_LIBRARY_PATH=/databases/postgresql/pgsql/lib + ln -s /databases/postgresql/pgsql/lib/libpq.so.5 /usr/lib/libpq.so.5 + adduser --disabled-password --gecos "" postgres + mkdir /databases/postgresql/pgsql/data + chown -R postgres /databases/postgresql + su postgres -c "/databases/postgresql/pgsql/bin/initdb -D /databases/postgresql/pgsql/data" + echo "alias start_postgres='sudo su postgres -c \"/databases/postgresql/pgsql/bin/pg_ctl -D /databases/postgresql/pgsql/data -l /databases/postgresql/logfile start\"'" >> /home/volttron/.bash_aliases + echo "alias stop_postgres='sudo su postgres -c \"/databases/postgresql/pgsql/bin/pg_ctl -D /databases/postgresql/pgsql/data -l /databases/postgresql/logfile stop\"'" >> /home/volttron/.bash_aliases + sudo su postgres -c "/databases/postgresql/pgsql/bin/pg_ctl -D /databases/postgresql/pgsql/data -l /databases/postgresql/logfile start" + psql -U postgres -c 'CREATE DATABASE test_historian;' + psql -U postgres -c "CREATE USER historian with encrypted password 'historian';" + psql -U postgres -c "GRANT ALL PRIVILEGES on database test_historian to historian;" + sudo su postgres -c "/databases/postgresql/pgsql/bin/pg_ctl -D /databases/postgresql/pgsql/data -l /databases/postgresql/logfile stop" +} + +echo "Configured to install dbs: $@" +v=( "$@" ) +for i in ${v[@]} +do + echo "Calling install_$i" + install_$i +done diff --git a/scripts/install_platform_driver_configs.py b/scripts/install_platform_driver_configs.py index 9bdf02ed86..43816cca4c 100644 --- a/scripts/install_platform_driver_configs.py +++ b/scripts/install_platform_driver_configs.py @@ -89,13 +89,13 @@ def install_configs(input_directory, keep=False): if not keep: print("Deleting old Platform Driver store") agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_delete_store', + 'delete_store', PLATFORM_DRIVER).get(timeout=10) with open("config") as f: print("Storing main configuration") agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'config', f.read(), @@ -105,7 +105,7 @@ def install_configs(input_directory, keep=False): with open(name) as f: print("Storing configuration:", name) agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_store', + 'set_config', PLATFORM_DRIVER, name, f.read(), @@ -117,7 +117,7 @@ def install_configs(input_directory, keep=False): with open(name) as f: print("Storing configuration:", name) agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_store', + 'set_config', PLATFORM_DRIVER, name, f.read(), diff --git a/scripts/rabbit_dependencies.sh b/scripts/rabbit_dependencies.sh deleted file mode 100755 index c14508639c..0000000000 --- a/scripts/rabbit_dependencies.sh +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env bash -set -e -ubuntu_list=(bionic focal) -list=(buster ) -list=("${ubuntu_list[@]}" "${debian_list[@]}") -declare -A ubuntu_versions -ubuntu_versions=( ["ubuntu-18.04"]="bionic" ["ubuntu-20.04"]="focal") - -function exit_on_error { - rc=$? - if [[ $rc != 0 ]] - then - printf "\n## Script could not complete successfully because of above error## \n" - exit $rc - fi - -} - -function print_usage { - echo " -Command Usage: -/rabbit_dependencies.sh or centos version> -Valid Debian distributions: ${list[@]} ${!ubuntu_versions[@]} -Valid centos versions: 8 -" - exit 0 - -} - - -function install_on_centos { - - if [[ "$DIST" != "8" ]]; then - printf "Invalid centos version. Centos 8 is the only compatible versions\n" - print_usage - fi - - repo="## In /etc/yum.repos.d/erlang.repo -[erlang-solutions] -name=CentOS $releasever - $basearch - Erlang Solutions -baseurl=https://packages.erlang-solutions.com/rpm/centos/\$releasever/\$basearch -gpgcheck=1 -gpgkey=https://packages.erlang-solutions.com/rpm/erlang_solutions.asc -enabled=1 -" - if [[ -f "/etc/yum.repos.d/erlang.repo" ]]; then - echo "\n/etc/yum.repos.d/erlang.repo exists. renaming current file to rlang.repo.old\n" - mv /etc/yum.repos.d/erlang.repo /etc/yum.repos.d/erlang.repo.old - exit_on_error - fi - echo "$repo" | ${prefix} tee -a /etc/yum.repos.d/erlang.repo - rpm --import https://packages.erlang-solutions.com/rpm/erlang_solutions.asc - ${prefix} yum install -y erlang-$erlang_package_version - exit_on_error -} - -function install_on_debian { - FOUND=0 - OS="" - for item in "${ubuntu_list[@]}"; do - if [[ "$DIST" == "$item" ]]; then - FOUND=1 - OS="ubuntu" - break - fi - done - - if [[ "$FOUND" != "1" ]]; then - for item in "${debian_list[@]}"; do - if [[ "$DIST" == "$item" ]]; then - FOUND=1 - OS="debian" - break - fi - done - fi - - if [[ "$FOUND" != "1" ]]; then - # check if ubuntu-version was passed if so map it to name - for ubuntu_version in "${!ubuntu_versions[@]}"; do - if [[ "$DIST" == "$ubuntu_version" ]]; then - FOUND=1 - DIST="${ubuntu_versions[$ubuntu_version]}" - OS="ubuntu" - break - fi - done - fi - - if [[ "$FOUND" != "1" ]]; then - echo "Invalid distribution found" - print_usage - fi - - echo "Installing ERLANG" - ${prefix} apt-get update - ${prefix} apt-get install -y gnupg apt-transport-https -y - ${prefix} apt-get purge -yf erlang-base - # Adds erlang repository entry - wget https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb - sudo dpkg -i erlang-solutions_2.0_all.deb - rm erlang-solutions_2.0_all.deb - if [[ -f "/etc/apt/sources.list.d/erlang.list" ]]; then - echo "\n/etc/apt/sources.list.d/erlang.list exists. renaming current file to erlang.list.old\n" - ${prefix} mv /etc/apt/sources.list.d/erlang.list /etc/apt/sources.list.d/erlang.list.old - exit_on_error - fi - version=${erlang_package_version} - to_install="\ - erlang-base=$version\ - erlang-asn1=$version \ - erlang-crypto=$version \ - erlang-eldap=$version \ - erlang-ftp=$version \ - erlang-inets=$version \ - erlang-mnesia=$version \ - erlang-os-mon=$version \ - erlang-parsetools=$version \ - erlang-public-key=$version \ - erlang-runtime-tools=$version \ - erlang-snmp=$version \ - erlang-ssl=$version \ - erlang-syntax-tools=$version \ - erlang-tools=$version \ - erlang-xmerl=$version \ - erlang-tftp=$version \ - " - - ${prefix} apt-get update - ${prefix} apt-get install -y --allow-downgrades ${to_install} -} - -os_name="$1" -DIST="$2" -user=`whoami` -if [[ ${user} == 'root' ]]; then - prefix="" -else - prefix="sudo" -fi -is_arm="FALSE" - -${prefix} pwd > /dev/null - -if [[ "$os_name" == "debian" ]]; then - erlang_package_version="1:24.1.7-1" - is_arm="FALSE" - install_on_debian -elif [[ "$os_name" == "centos" ]]; then - erlang_package_version="24.2-1.el8" - install_on_centos -else - printf "For operating system/distributions not supported by this script, please refer to https://www.rabbitmq.com/which-erlang.html#erlang-repositories\n" - print_usage -fi - -echo "Finished installing dependencies for rabbitmq" diff --git a/scripts/scalability-testing/agents/NullHistorian/null_historian/agent.py b/scripts/scalability-testing/agents/NullHistorian/null_historian/agent.py index 9ce7908b23..7f0103da8b 100644 --- a/scripts/scalability-testing/agents/NullHistorian/null_historian/agent.py +++ b/scripts/scalability-testing/agents/NullHistorian/null_historian/agent.py @@ -39,10 +39,10 @@ import logging import sys -from volttron.platform.vip.agent import * from volttron.platform.agent.base_historian import BaseHistorian, add_timing_data_to_header from volttron.platform.agent import utils from volttron.platform.agent import math_utils +from volttron.platform.vip.agent.core import Core utils.setup_logging() _log = logging.getLogger(__name__) diff --git a/scripts/secure_user_permissions.sh b/scripts/secure_user_permissions.sh index faa7816e0d..ac614c57cd 100755 --- a/scripts/secure_user_permissions.sh +++ b/scripts/secure_user_permissions.sh @@ -265,6 +265,19 @@ while true; do fi done +# Get full path to python executable +while true; do + echo -n "Enter full path to python used for volttron:" + read python_path + valid=0 + version=`$python_path -V` + if [ $? -eq 0 ]; then + break + else + echo "Invalid python_path" + fi +done + echo "$volttron_user ALL= NOPASSWD: /usr/sbin/groupadd volttron_$name" | sudo EDITOR='tee -a' visudo -f /etc/sudoers.d/volttron_$name echo "$volttron_user ALL= NOPASSWD: /usr/sbin/usermod -a -G volttron_$name $USER" | sudo EDITOR='tee -a' visudo -f /etc/sudoers.d/volttron_$name echo "$volttron_user ALL= NOPASSWD: /usr/sbin/useradd volttron_[1-9]* -r -G volttron_$name" | sudo EDITOR='tee -a' visudo -f /etc/sudoers.d/volttron_$name @@ -273,6 +286,6 @@ echo "$volttron_user ALL= NOPASSWD: $source_dir/scripts/stop_agent_running_in_is # TODO want delete only users with pattern of particular group echo "$volttron_user ALL= NOPASSWD: /usr/sbin/userdel volttron_[1-9]*" | sudo EDITOR='tee -a' visudo -f /etc/sudoers.d/volttron_$name # allow user to run all non-sudo commands for all volttron agent users -echo "$volttron_user ALL=(%volttron_$name) NOPASSWD: ALL" | sudo EDITOR='tee -a' visudo -f /etc/sudoers.d/volttron_$name +echo "$volttron_user ALL=(%volttron_$name) NOPASSWD:SETENV: $python_path" | sudo EDITOR='tee -a' visudo -f /etc/sudoers.d/volttron_$name echo "Permissions set for $volttron_user" echo "Volttron agent isolation mode setup is complete" diff --git a/scripts/tagging_scripts/insert_id_and_ref_tags.py b/scripts/tagging_scripts/insert_id_and_ref_tags.py index 23c5738822..3c8a6ebb57 100644 --- a/scripts/tagging_scripts/insert_id_and_ref_tags.py +++ b/scripts/tagging_scripts/insert_id_and_ref_tags.py @@ -164,7 +164,7 @@ def mongo_insert(tags, execute_now=False): try: result = mongo_bulk.execute() if result['nInserted'] != mongo_batch_size: - print ("bulk execute result {}".format(result)) + print("bulk execute result {}".format(result)) errors = True except BulkWriteError as ex: print(str(ex.details)) diff --git a/services/contrib/InfluxdbHistorian/tests/test_influxdb_historian.py b/services/contrib/InfluxdbHistorian/tests/test_influxdb_historian.py index 0544ea39ed..b6218d58a5 100644 --- a/services/contrib/InfluxdbHistorian/tests/test_influxdb_historian.py +++ b/services/contrib/InfluxdbHistorian/tests/test_influxdb_historian.py @@ -42,12 +42,11 @@ import gevent import pytz import os -import json from pytest import approx from datetime import datetime, timedelta from dateutil import parser -from volttron.platform import get_services_core, jsonapi +from volttron.platform import jsonapi from volttron.platform.agent.utils import format_timestamp, parse_timestamp_string, get_aware_utc_now from volttron.platform.messaging import headers as headers_mod @@ -1333,7 +1332,7 @@ def test_update_config_store(volttron_instance, influxdb_client): publish_some_fake_data(publisher, 5) # Update config store - publisher.vip.rpc.call('config.store', 'manage_store', 'influxdb.historian', 'config', + publisher.vip.rpc.call('config.store', 'set_config', 'influxdb.historian', 'config', jsonapi.dumps(updated_influxdb_config), config_type="json").get(timeout=10) publish_some_fake_data(publisher, 5) diff --git a/services/contrib/KafkaAgent/Test/kafka_producer.py b/services/contrib/KafkaAgent/Test/kafka_producer.py index 00d9b4600f..eea5a02ade 100644 --- a/services/contrib/KafkaAgent/Test/kafka_producer.py +++ b/services/contrib/KafkaAgent/Test/kafka_producer.py @@ -2,7 +2,6 @@ from kafka import KafkaProducer -from kafka.errors import KafkaError from volttron.platform import jsonapi diff --git a/services/contrib/MarketServiceAgent/market_service/market.py b/services/contrib/MarketServiceAgent/market_service/market.py index 12843d0b69..7fbaddb5f4 100644 --- a/services/contrib/MarketServiceAgent/market_service/market.py +++ b/services/contrib/MarketServiceAgent/market_service/market.py @@ -147,7 +147,7 @@ def make_offer(self, participant, curve): _log.debug("Make offer Market: {} {} entered in state {}".format(self.market_name, participant.buyer_seller, self.state)) - if (participant.buyer_seller == SELLER): + if participant.buyer_seller == SELLER: self.receive_sell_offer() else: self.receive_buy_offer() @@ -163,7 +163,7 @@ def make_offer(self, participant, curve): participant.buyer_seller, offer_count, curve.tuppleize())) self.offers.make_offer(participant.buyer_seller, curve) if self.all_satisfied(participant.buyer_seller): - if (participant.buyer_seller == SELLER): + if participant.buyer_seller == SELLER: self.last_sell_offer() else: self.last_buy_offer() @@ -199,25 +199,24 @@ def clear_market(self): error_code = None error_message = None aux = {} - if (self.state in [ACCEPT_ALL_OFFERS, ACCEPT_BUY_OFFERS, ACCEPT_SELL_OFFERS]): + if self.state in [ACCEPT_ALL_OFFERS, ACCEPT_BUY_OFFERS, ACCEPT_SELL_OFFERS]: error_code = SHORT_OFFERS error_message = 'The market {} failed to receive all the expected offers. ' \ 'The state is {}.'.format(self.market_name, self.state) - elif (self.state != MARKET_DONE): + elif self.state != MARKET_DONE: error_code = BAD_STATE error_message = 'Programming error in Market class. State of {} and clear market signal arrived. ' \ 'This represents a logic error.'.format(self.state) + elif not self.has_market_formed(): + error_code = NOT_FORMED + error_message = 'The market {} has not received a buy and a sell reservation.'.format(self.market_name) else: - if not self.has_market_formed(): - error_code = NOT_FORMED - error_message = 'The market {} has not received a buy and a sell reservation.'.format(self.market_name) - else: - quantity, price, aux = self.offers.settle() - _log.info("Clearing mixmarket: {} Price: {} Qty: {}".format(self.market_name, price, quantity)) - aux = {} - if price is None or quantity is None: - error_code = NO_INTERSECT - error_message = "Error: The supply and demand curves do not intersect. The market {} failed to clear.".format(self.market_name) + quantity, price, aux = self.offers.settle() + _log.info("Clearing mixmarket: {} Price: {} Qty: {}".format(self.market_name, price, quantity)) + aux = {} + if price is None or quantity is None: + error_code = NO_INTERSECT + error_message = "Error: The supply and demand curves do not intersect. The market {} failed to clear.".format(self.market_name) _log.info("Clearing price for Market: {} Price: {} Qty: {}".format(self.market_name, price, quantity)) timestamp = self._get_time() timestamp_string = utils.format_timestamp(timestamp) @@ -241,9 +240,9 @@ def log_market_failure(self, message): def all_satisfied(self, buyer_seller): are_satisfied = False - if (buyer_seller == BUYER): + if buyer_seller == BUYER: are_satisfied = self.reservations.buyer_count() == self.offers.buyer_count() - if (buyer_seller == SELLER): + if buyer_seller == SELLER: are_satisfied = self.reservations.seller_count() == self.offers.seller_count() return are_satisfied diff --git a/services/contrib/MarketServiceAgent/market_service/reservation_manager.py b/services/contrib/MarketServiceAgent/market_service/reservation_manager.py index 23f18e282c..50a4726ad7 100644 --- a/services/contrib/MarketServiceAgent/market_service/reservation_manager.py +++ b/services/contrib/MarketServiceAgent/market_service/reservation_manager.py @@ -49,7 +49,7 @@ def __init__(self): self._sell_reservations = {} def make_reservation(self, participant): - if (participant.is_buyer()): + if participant.is_buyer(): self._make_buy_reservation(participant.identity) else: self._make_sell_reservation(participant.identity) @@ -68,7 +68,7 @@ def _make_sell_reservation(self, owner): self._add_reservation(self._sell_reservations, owner, 'sell') def take_reservation(self, participant): - if (participant.is_buyer()): + if participant.is_buyer(): self._take_buy_reservation(participant.identity) else: self._take_sell_reservation(participant.identity) diff --git a/services/contrib/MessageDebuggerAgent/messagedebugger/agent.py b/services/contrib/MessageDebuggerAgent/messagedebugger/agent.py index eb90ddbbe8..b40e8c8281 100644 --- a/services/contrib/MessageDebuggerAgent/messagedebugger/agent.py +++ b/services/contrib/MessageDebuggerAgent/messagedebugger/agent.py @@ -53,7 +53,7 @@ from volttron.platform.agent import utils from volttron.platform import jsonapi -from volttron.platform.control import ControlConnection, KnownHostsStore, KeyStore +from volttron.platform.control import KnownHostsStore, KeyStore from volttron.platform.vip.agent import Agent, RPC, Core from volttron.platform.vip.router import ERROR, UNROUTABLE, INCOMING, OUTGOING diff --git a/services/core/ActuatorAgent/tests/test_actuator_rpc.py b/services/core/ActuatorAgent/tests/test_actuator_rpc.py index 17d45ac99f..47b0a348a5 100644 --- a/services/core/ActuatorAgent/tests/test_actuator_rpc.py +++ b/services/core/ActuatorAgent/tests/test_actuator_rpc.py @@ -215,7 +215,7 @@ def test_request_new_schedule(publish_agent, cancel_schedules, taskid, expected_ :param cancel_schedules: fixture used to cancel the schedule at the end of test so that other tests can use the same device and time slot """ - print ("\n**** test_schedule_success ****") + print("\n**** test_schedule_success ****") # used by cancel_schedules agentid = TEST_AGENT cancel_schedules.append({'agentid': agentid, 'taskid': taskid}) @@ -460,7 +460,7 @@ def test_request_new_schedule_should_suceed_on_preempt_active_task(publish_agent :param cancel_schedules: fixture used to cancel the schedule at the end of test so that other tests can use the same device and time slot """ - print ("\n**** test_schedule_preempt_active_task ****") + print("\n**** test_schedule_preempt_active_task ****") # used by cancel_schedules agentid = 'new_agent' taskid = 'task_high_priority2' @@ -656,7 +656,7 @@ def test_request_new_schedule_should_return_failure_on_preempt_active_task(publi :param cancel_schedules: fixture used to cancel the schedule at the end of test so that other tests can use the same device and time slot """ - print ("\n**** test_schedule_preempt_error_active_task ****") + print("\n**** test_schedule_preempt_error_active_task ****") # used by cancel_schedules agentid = TEST_AGENT taskid = 'task_low_priority3' @@ -713,7 +713,7 @@ def test_request_new_schedule_should_succeed_on_preempt_future_task(publish_agen :param cancel_schedules: fixture used to cancel the schedule at the end of test so that other tests can use the same device and time slot """ - print ("\n**** test_schedule_preempt_future_task ****") + print("\n**** test_schedule_preempt_future_task ****") # used by cancel_schedules agentid = 'new_agent' taskid = 'task_high_priority4' @@ -789,7 +789,7 @@ def test_request_new_schedule_should_return_failure_on_conflicting_time_slots(pu :param publish_agent: fixture invoked to setup all agents necessary and returns an instance of Agent object used for publishing """ - print ("\n**** test_schedule_conflict_self ****") + print("\n**** test_schedule_conflict_self ****") # used by cancel_schedules taskid = 'task_self_conflict' start = str(datetime.now()) @@ -868,7 +868,7 @@ def test_request_new_schedule_should_succeed_on_overlap_time_slots(publish_agent :param cancel_schedules: fixture used to cancel the schedule at the end of test so that other tests can use the same device and time slot """ - print ("\n**** test_schedule_overlap_success ****") + print("\n**** test_schedule_overlap_success ****") # set agentid and task id for cancel_schedules fixture agentid = TEST_AGENT taskid = 'task_overlap' @@ -902,7 +902,7 @@ def test_request_cancel_schedule_should_succeed(publish_agent): :param publish_agent: fixture invoked to setup all agents necessary and returns an instance of Agent object used for publishing """ - print ("\n**** test_cancel_success ****") + print("\n**** test_cancel_success ****") start = str(datetime.now()) end = str(datetime.now() + timedelta(seconds=2)) @@ -940,7 +940,7 @@ def test_request_cancel_schedule_should_return_failure_on_invalid_taskid(publish returns an instance of Agent object used for publishing """ - print ("\n**** test_cancel_error_invalid_taskid ****") + print("\n**** test_cancel_error_invalid_taskid ****") result = publish_agent.vip.rpc.call( PLATFORM_ACTUATOR, REQUEST_CANCEL_SCHEDULE, @@ -963,7 +963,7 @@ def test_get_point_should_succeed(publish_agent): :param publish_agent: fixture invoked to setup all agents necessary and returns an instance of Agent object used for publishing """ - print ("\n**** test_get_default ****") + print("\n**** test_get_default ****") result = publish_agent.vip.rpc.call( PLATFORM_ACTUATOR, # Target agent @@ -1072,7 +1072,7 @@ def test_revert_point_should_succeed(publish_agent, cancel_schedules): :param cancel_schedules: fixture used to cancel the schedule at the end of test so that other tests can use the same device and time slot """ - print ("\n**** test_set_float_value ****") + print("\n**** test_set_float_value ****") taskid = 'test_revert_point' agentid = TEST_AGENT cancel_schedules.append({'agentid': agentid, 'taskid': taskid}) @@ -1136,7 +1136,7 @@ def test_revert_point_with_point_should_succeed(publish_agent, cancel_schedules) :param cancel_schedules: fixture used to cancel the schedule at the end of test so that other tests can use the same device and time slot """ - print ("\n**** test_set_float_value ****") + print("\n**** test_set_float_value ****") taskid = 'test_revert_point' agentid = TEST_AGENT cancel_schedules.append({'agentid': agentid, 'taskid': taskid}) @@ -1380,7 +1380,7 @@ def test_set_point_raises_value_error(publish_agent, cancel_schedules): :param cancel_schedules: fixture used to cancel the schedule at the end of test so that other tests can use the same device and time slot """ - print ("\n**** test_set_value_error ****") + print("\n**** test_set_value_error ****") agentid = TEST_AGENT taskid = 'task_set_value_error' cancel_schedules.append({'agentid': agentid, 'taskid': taskid}) @@ -1562,7 +1562,7 @@ def test_set_point_raises_remote_error_on_lock_failure(publish_agent, cancel_sch :param cancel_schedules: fixture used to cancel the schedule at the end of test so that other tests can use the same device and time slot """ - print ("\n**** test_set_float_value ****") + print("\n**** test_set_float_value ****") agentid = TEST_AGENT with pytest.raises(RemoteError): diff --git a/services/core/DNP3Agent/README.md b/services/core/DNP3Agent/README.md deleted file mode 100644 index 3cf6162e19..0000000000 --- a/services/core/DNP3Agent/README.md +++ /dev/null @@ -1,86 +0,0 @@ -DNP3Agent and MesaAgent, either of which can be built from this directory, -are VOLTTRON agents that handle DNP3 communications. -They implement a DNP3 outstation, communicating with a DNP3 master. - -For further information about these agents and DNP3 communications, please see the VOLTTRON -DNP3 and MESA specifications, located in VOLTTRON readthedocs -under http://volttron.readthedocs.io/en/develop/specifications/dnp3_agent.html -and http://volttron.readthedocs.io/en/develop/specifications/mesa_agent.html. - -These agents depend on the pydnp3 library, which must be installed in the VOLTTRON virtual environment: - - (volttron) $ pip install pydnp3 - -Installing MesaAgent --------------------- - -MesaAgent implements MESA-ESS, an enhanced version of the DNP3 protocol. - -MesaAgent can be installed by running the **install_mesa_agent.sh** -command-line script as follows: - - (volttron) $ export VOLTTRON_ROOT= - (volttron) $ source $VOLTTRON_ROOT/services/core/DNP3Agent/install_mesa_agent.sh - -The **install_mesa_agent.sh** script installs the agent: - - (volttron) $ export DNP3_ROOT=$VOLTTRON_ROOT/services/core/DNP3Agent - (volttron) $ export AGENT_MODULE=dnp3.mesa.agent - (volttron) $ cd $VOLTTRON_ROOT - (volttron) $ python scripts/install-agent.py -s $DNP3_ROOT -i mesaagent -c $DNP3_ROOT/mesaagent.config -t mesaagent -f - -(Note that $AGENT_MODULE directs the installer to use agent -source code residing in the "dnp3/mesa" subdirectory.) - -Then the script stores DNP3 point and MESA function definitions in the agent's config store: - - (volttron) $ cd $DNP3_ROOT - (volttron) $ python dnp3/mesa/conversion.py < dnp3/mesa/mesa_functions.yaml > dnp3/mesa/mesa_functions.config - (volttron) $ cd $VOLTTRON_ROOT - (volttron) $ vctl config store mesaagent mesa_points.config $DNP3_ROOT/dnp3/mesa_points.config - (volttron) $ vctl config store mesaagent mesa_functions.config $DNP3_ROOT/dnp3/mesa/mesa_functions.config - -Regression tests can be run from a command-line shell as follows: - - (volttron) $ pytest services/core/DNP3Agent/tests/test_mesa_agent.py - -Installing DNP3Agent --------------------- - -DNP3Agent implements the basic DNP3 protocol. - -DNP3Agent can be installed by running the **install_dnp3_agent.sh** -command-line script as follows: - - (volttron) $ export VOLTTRON_ROOT= - (volttron) $ source $VOLTTRON_ROOT/services/core/DNP3Agent/install_dnp3_agent.sh - -The **install_dnp3_agent.sh** script installs the agent: - - (volttron) $ export DNP3_ROOT=$VOLTTRON_ROOT/services/core/DNP3Agent - (volttron) $ export AGENT_MODULE=dnp3.agent - (volttron) $ cd $VOLTTRON_ROOT - (volttron) $ python scripts/install-agent.py -s $DNP3_ROOT -i dnp3agent -c $DNP3_ROOT/config -t dnp3agent -f - -(Note that $AGENT_MODULE directs the installer to use agent -source code residing in the "dnp3" directory.) - -Then the script stores DNP3 point (but not MESA function) definitions in the agent's config store: - - (volttron) $ vctl config store dnp3agent mesa_points.config $DNP3_ROOT/dnp3/mesa_points.config - -Regression tests can be run from a command-line shell as follows: - - (volttron) $ cd $VOLTTRON_ROOT - (volttron) $ pytest services/core/DNP3Agent/tests/test_dnp3_agent.py - -Maintaining mesa_points.config and mesa_functions.yaml ------------------------------------------------------- - -mesa_points.config is installed in $DNP3_ROOT/dnp3/mesa_points.config - -mesa_functions.yaml is installed in $DNP3_ROOT/dnp3/mesa/mesa_functions.yaml - -To update Mesa points and functions config files, please follow instructions for -[mesa_points.config](https://docs.google.com/document/d/1WgiGkNCtILLvNKSm0ZsNo0HrqY0akIQIiGQNZP1PBP0/edit#heading=h.5224t5rtcb0g) -and [mesa_functions.yaml](https://docs.google.com/document/d/1WgiGkNCtILLvNKSm0ZsNo0HrqY0akIQIiGQNZP1PBP0/edit#heading=h.qhuvbxq207n2) diff --git a/services/core/DNP3Agent/config b/services/core/DNP3Agent/config deleted file mode 100644 index fe60378e83..0000000000 --- a/services/core/DNP3Agent/config +++ /dev/null @@ -1,11 +0,0 @@ -{ - "points": "config://mesa_points.config", - "point_topic": "dnp3/point", - "outstation_status_topic": "dnp3/outstation_status", - "outstation_config": { - "database_sizes": 10000, - "log_levels": ["NORMAL"] - }, - "local_ip": "0.0.0.0", - "port": 20000 -} \ No newline at end of file diff --git a/services/core/DNP3Agent/conftest.py b/services/core/DNP3Agent/conftest.py deleted file mode 100644 index 74134a5b76..0000000000 --- a/services/core/DNP3Agent/conftest.py +++ /dev/null @@ -1,19 +0,0 @@ -import sys - -from volttrontesting.fixtures.volttron_platform_fixtures import * - -collect_ignore = ["function_test.py", "tests/mesa_platform_test.py"] - -try: - import pydnp3 -except ImportError: - # pydnp3 library has not been installed -- all pytest modules would fail - collect_ignore.extend(["tests/test_dnp3_agent.py", - "tests/test_mesa_agent.py", - "tests/test_mesa_data.py"]) - -# Add system path of the agent's directory -sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) - -# Add system path of the agent's dnp3 subdirectory -sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/dnp3')) diff --git a/services/core/DNP3Agent/dnp3/__init__.py b/services/core/DNP3Agent/dnp3/__init__.py deleted file mode 100644 index d0525d5da5..0000000000 --- a/services/core/DNP3Agent/dnp3/__init__.py +++ /dev/null @@ -1,74 +0,0 @@ -from pydnp3 import opendnp3 - -DEFAULT_POINT_TOPIC = 'dnp3/point' -DEFAULT_OUTSTATION_STATUS_TOPIC = 'mesa/outstation_status' -DEFAULT_LOCAL_IP = "0.0.0.0" -DEFAULT_PORT = 20000 - -# StepDefinition.fcode values: -DIRECT_OPERATE = 'direct_operate' # This is actually DIRECT OPERATE / RESPONSE -SELECT = 'select' # This is actually SELECT / RESPONSE -OPERATE = 'operate' # This is actually OPERATE / RESPONSE -READ = 'read' -RESPONSE = 'response' - -# PointDefinition.action values: -PUBLISH = 'publish' -PUBLISH_AND_RESPOND = 'publish_and_respond' - -# Some PointDefinition.type values -POINT_TYPE_ARRAY = 'array' -POINT_TYPE_SELECTOR_BLOCK = 'selector_block' -POINT_TYPE_ENUMERATED = 'enumerated' -POINT_TYPES = [POINT_TYPE_ARRAY, POINT_TYPE_SELECTOR_BLOCK, POINT_TYPE_ENUMERATED] - -# Some PointDefinition.point_type values: -DATA_TYPE_ANALOG_INPUT = 'AI' -DATA_TYPE_ANALOG_OUTPUT = 'AO' -DATA_TYPE_BINARY_INPUT = 'BI' -DATA_TYPE_BINARY_OUTPUT = 'BO' - -# PointDefinition.group -DEFAULT_GROUP_BY_DATA_TYPE = { - DATA_TYPE_BINARY_INPUT: 1, - DATA_TYPE_BINARY_OUTPUT: 10, - DATA_TYPE_ANALOG_INPUT: 30, - DATA_TYPE_ANALOG_OUTPUT: 40 -} - -# variation = 1: 32 bit, variation = 2: 16 bit -DEFAULT_VARIATION = { - DATA_TYPE_BINARY_INPUT: {'evariation': opendnp3.EventBinaryVariation.Group2Var1, - 'svariation': opendnp3.StaticBinaryVariation.Group1Var2}, - DATA_TYPE_BINARY_OUTPUT: {'evariation': opendnp3.EventBinaryOutputStatusVariation.Group11Var1, - 'svariation': opendnp3.StaticBinaryOutputStatusVariation.Group10Var2}, - DATA_TYPE_ANALOG_INPUT: {'evariation': opendnp3.EventAnalogVariation.Group32Var1, - 'svariation': opendnp3.StaticAnalogVariation.Group30Var1}, - DATA_TYPE_ANALOG_OUTPUT: {'evariation': opendnp3.EventAnalogOutputStatusVariation.Group42Var1, - 'svariation': opendnp3.StaticAnalogOutputStatusVariation.Group40Var1} -} - -# PointDefinition.event_class -DEFAULT_EVENT_CLASS = 2 - -EVENT_CLASSES = { - 0: opendnp3.PointClass.Class0, - 1: opendnp3.PointClass.Class1, - 2: opendnp3.PointClass.Class2, - 3: opendnp3.PointClass.Class3 -} - -DATA_TYPES_BY_GROUP = { - # Single-Bit Binary: See DNP3 spec, Section A.2-A.5 and Table 11-17 - 1: DATA_TYPE_BINARY_INPUT, # Binary Input (static): Reporting the present value of a single-bit binary object - 2: DATA_TYPE_BINARY_INPUT, # Binary Input Event: Reporting single-bit binary input events and flag bit changes - # Binary Output: See DNP3 spec, Section A.6-A.9 and Table 11-12 - 10: DATA_TYPE_BINARY_OUTPUT, # Binary Output (static): Reporting the present output status - 11: DATA_TYPE_BINARY_OUTPUT, # Binary Output Event: Reporting changes to the output status or flag bits - # Analog Input: See DNP3 spec, Section A.14-A.18 and Table 11-9 - 30: DATA_TYPE_ANALOG_INPUT, # Analog Input (static): Reporting the present value - 32: DATA_TYPE_ANALOG_INPUT, # Analog Input Event: Reporting analog input events or changes to the flag bits - # Analog Output: See DNP3 spec, Section A.19-A.22 and Table 11-10 - 40: DATA_TYPE_ANALOG_OUTPUT, # Analog Output Status (static): Reporting present value of analog outputs - 42: DATA_TYPE_ANALOG_OUTPUT # Analog Output Event: Reporting changes to the analog output or flag bits -} diff --git a/services/core/DNP3Agent/dnp3/agent.py b/services/core/DNP3Agent/dnp3/agent.py deleted file mode 100644 index 8ecbc7cd24..0000000000 --- a/services/core/DNP3Agent/dnp3/agent.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, SLAC / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# This material was prepared as an account of work sponsored by an agency of -# the United States Government. Neither the United States Government nor the -# United States Department of Energy, nor SLAC, nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by the United States Government or any agency thereof, or -# SLAC, or Kisensum. The views and opinions of authors expressed -# herein do not necessarily state or reflect those of the -# United States Government or any agency thereof. -# }}} - -import logging -import sys - -from volttron.platform.agent import utils -from dnp3.base_dnp3_agent import BaseDNP3Agent - -utils.setup_logging() -_log = logging.getLogger(__name__) - -__version__ = '1.1' - - -class DNP3Agent(BaseDNP3Agent): - """ - DNP3Agent is a VOLTTRON agent that handles DNP3 outstation communications. - - DNP3Agent models a DNP3 outstation, communicating with a DNP3 master. - - For further information about this agent and DNP3 communications, please see the VOLTTRON - DNP3 specification, located in VOLTTRON readthedocs - under http://volttron.readthedocs.io/en/develop/specifications/dnp3_agent.html. - - This agent can be installed from a command-line shell as follows: - $ export VOLTTRON_ROOT= - $ cd $VOLTTRON_ROOT - $ source services/core/DNP3Agent/install_dnp3_agent.sh - """ - - def _process_point_value(self, point_value): - """DNP3Agent publishes each point value to the message bus as the value is received from the master.""" - point_val = super(DNP3Agent, self)._process_point_value(point_value) - if point_val: - self.publish_point_value(point_value) - - -def dnp3_agent(config_path, **kwargs): - """ - Parse the DNP3 Agent configuration. Return an agent instance created from that config. - - :param config_path: (str) Path to a configuration file. - :returns: (DNP3Agent) The DNP3 agent - """ - try: - config = utils.load_config(config_path) - except Exception: - config = {} - return DNP3Agent(points=config.get('points', None), - point_topic=config.get('point_topic', 'dnp3/point'), - local_ip=config.get('local_ip', '0.0.0.0'), - port=config.get('port', 20000), - outstation_config=config.get('outstation_config', {}), - **kwargs) - - -def main(): - """Main method called to start the agent.""" - utils.vip_main(dnp3_agent, identity='dnp3agent', version=__version__) - - -if __name__ == '__main__': - # Entry point for script - try: - sys.exit(main()) - except KeyboardInterrupt: - pass diff --git a/services/core/DNP3Agent/dnp3/base_dnp3_agent.py b/services/core/DNP3Agent/dnp3/base_dnp3_agent.py deleted file mode 100644 index 46eb97a4f8..0000000000 --- a/services/core/DNP3Agent/dnp3/base_dnp3_agent.py +++ /dev/null @@ -1,517 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, SLAC / 8minutenergy / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# This material was prepared in part as an account of work sponsored by an agency of -# the United States Government. Neither the United States Government nor the -# United States Department of Energy, nor SLAC, nor 8minutenergy, nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by the United States Government or any agency thereof, or -# SLAC, 8minutenergy, or Kisensum. The views and opinions of authors expressed -# herein do not necessarily state or reflect those of the -# United States Government or any agency thereof. -# }}} - -import logging -import numbers -import os - -from pydnp3 import opendnp3 - -from volttron.platform.vip.agent import RPC -from volttron.platform.agent import utils -from volttron.platform.messaging import headers -from volttron.platform.vip.agent import Agent - -from dnp3.outstation import DNP3Outstation -from dnp3 import DEFAULT_POINT_TOPIC, DEFAULT_OUTSTATION_STATUS_TOPIC -from dnp3 import DEFAULT_LOCAL_IP, DEFAULT_PORT -from dnp3 import DATA_TYPE_ANALOG_INPUT, DATA_TYPE_BINARY_INPUT -from dnp3 import PUBLISH_AND_RESPOND -from dnp3.points import PointDefinitions, PointDefinition, PointArray -from dnp3.points import DNP3Exception - -utils.setup_logging() -_log = logging.getLogger(__name__) - - -class BaseDNP3Agent(Agent): - """ - DNP3Agent is a VOLTTRON agent that handles DNP3 outstation communications. - - DNP3Agent models a DNP3 outstation, communicating with a DNP3 master. - - For further information about this agent and DNP3 communications, please see the VOLTTRON - DNP3 specification, located in VOLTTRON readthedocs - under http://volttron.readthedocs.io/en/develop/specifications/dnp3_agent.html. - - This agent can be installed from a command-line shell as follows: - export VOLTTRON_ROOT= - export DNP3_ROOT=$VOLTTRON_ROOT/services/core/DNP3Agent - cd $VOLTTRON_ROOT - python scripts/install-agent.py -s $DNP3_ROOT -i dnp3agent -c $DNP3_ROOT/config -t dnp3agent -f - """ - - def __init__(self, points=None, point_topic='', local_ip=None, port=None, - outstation_config=None, local_point_definitions_path=None, **kwargs): - """Initialize the DNP3 agent.""" - super(BaseDNP3Agent, self).__init__(**kwargs) - self.points = points - self.point_topic = point_topic - self.local_ip = local_ip - self.port = port - self.outstation_config = outstation_config - self.default_config = { - 'points': points, - 'point_topic': point_topic, - 'local_ip': local_ip, - 'port': port, - 'outstation_config': outstation_config, - } - self.application = None - self.volttron_points = None - - self.point_definitions = None - self._current_point_values = {} - self._current_array = None - self._local_point_definitions_path = local_point_definitions_path - - self.vip.config.set_default('config', self.default_config) - self.vip.config.subscribe(self._configure, actions=['NEW', 'UPDATE'], pattern='config') - - def _configure(self, config_name, action, contents): - """Initialize/Update the agent configuration.""" - self._configure_parameters(contents) - - def load_point_definitions(self): - """ - Load and cache a dictionary of PointDefinitions from a json list. - - Index the dictionary by point_type and point index. - """ - _log.debug('Loading DNP3 point definitions.') - try: - self.point_definitions = PointDefinitions() - self.point_definitions.load_points(self.points) - except (AttributeError, TypeError) as err: - if self._local_point_definitions_path: - _log.warning("Attempting to load point definitions from local path.") - self.point_definitions = PointDefinitions(point_definitions_path=self._local_point_definitions_path) - else: - raise DNP3Exception("Failed to load point definitions from config store: {}".format(err)) - - def start_outstation(self): - """Start the DNP3Outstation instance, kicking off communication with the DNP3 Master.""" - _log.info('Starting DNP3Outstation') - self.publish_outstation_status('starting') - self.application = DNP3Outstation(self.local_ip, self.port, self.outstation_config) - self.application.start() - self.publish_outstation_status('running') - - def stop_outstation(self): - """Shutdown the DNP3Outstation application.""" - _log.info('Stopping DNP3Outstation') - self.publish_outstation_status('stopping') - self.application.shutdown() - self.publish_outstation_status('stopped') - self.application = None - - def _configure_parameters(self, contents): - """ - Initialize/Update the DNP3 agent configuration. - - DNP3Agent configuration parameters (the MesaAgent subclass has some more): - - points: (string) A JSON structure of point definitions to be loaded. - point_topic: (string) Message bus topic to use when publishing DNP3 point values. - Default: mesa/point. - outstation_status_topic: (string) Message bus topic to use when publishing outstation status. - Default: mesa/outstation_status. - local_ip: (string) Outstation's host address (DNS resolved). - Default: 0.0.0.0. - port: (integer) Outstation's port number - the port that the remote endpoint (Master) is listening on. - Default: 20000. - outstation_config: (dictionary) Outstation configuration parameters. All are optional. - Parameters include: - database_sizes: (integer) Size of each DNP3 database buffer. - Default: 10000. - event_buffers: (integer) Size of the database event buffers. - Default: 10. - allow_unsolicited: (boolean) Whether to allow unsolicited requests. - Default: True. - link_local_addr: (integer) Link layer local address. - Default: 10. - link_remote_addr: (integer) Link layer remote address. - Default: 1. - log_levels: List of bit field names (OR'd together) that filter what gets logged by DNP3. - Default: [NORMAL]. - Possible values: ALL, ALL_APP_COMMS, ALL_COMMS, NORMAL, NOTHING. - threads_to_allocate: (integer) Threads to allocate in the manager's thread pool. - Default: 1. - """ - config = self.default_config.copy() - config.update(contents) - self.points = config.get('points', []) - self.point_topic = config.get('point_topic', DEFAULT_POINT_TOPIC) - self.outstation_status_topic = config.get('outstation_status_topic', DEFAULT_OUTSTATION_STATUS_TOPIC) - self.local_ip = config.get('local_ip', DEFAULT_LOCAL_IP) - self.port = int(config.get('port', DEFAULT_PORT)) - self.outstation_config = config.get('outstation_config', {}) - _log.debug('DNP3Agent configuration parameters:') - _log.debug('\tpoints type={}'.format(type(self.points))) - _log.debug('\tpoint_topic={}'.format(self.point_topic)) - _log.debug('\toutstation_status_topic={}'.format(self.outstation_status_topic)) - _log.debug('\tlocal_ip={}'.format(self.local_ip)) - _log.debug('\tport={}'.format(self.port)) - _log.debug('\toutstation_config={}'.format(self.outstation_config)) - self.load_point_definitions() - DNP3Outstation.set_agent(self) - - # Stop outstation if DNP3 config has been changed - if self.application and ( - self.application.local_ip, self.application.port, self.application.outstation_config) != ( - self.local_ip, self.port, self.outstation_config): - self.stop_outstation() - - # Start outstation if the DNP3 application has not started - if not self.application: - self.start_outstation() - - return config - - @RPC.export - def reset(self): - """Reset the agent's internal state, emptying point value caches. Used during iterative testing.""" - _log.info('Resetting agent state.') - self._current_point_values = {} - self._current_array = {} - - def get_current_point_value(self, data_type, index): - """Return the most-recently-received PointValue for a given PointDefinition.""" - if data_type not in self._current_point_values or index not in self._current_point_values[data_type]: - return None - else: - return self._current_point_values[data_type][index] - - def _set_point(self, point_name, value): - """ - (Internal) Set the value of a given input point (no debug trace). - - @param point_name: The VOLTTRON point name of a DNP3 PointDefinition. - @param value: The value to set. The value's data type must match the one in the DNP3 PointDefinition. - """ - point_properties = self.volttron_points.get(point_name, {}) - data_type = point_properties.get('data_type', None) - index = point_properties.get('index', None) - try: - if data_type == DATA_TYPE_ANALOG_INPUT: - wrapped_value = opendnp3.Analog(value) - elif data_type == DATA_TYPE_BINARY_INPUT: - wrapped_value = opendnp3.Binary(value) - else: - raise Exception('Unexpected data type for DNP3 point named {0}'.format(point_name)) - DNP3Outstation.apply_update(wrapped_value, index) - except Exception as e: - raise DNP3Exception(e) - - def process_point_value(self, command_type, command, index, op_type): - """ - A point value was received from the Master. Process its payload. - - @param command_type: Either 'Select' or 'Operate'. - @param command: A ControlRelayOutputBlock or else a wrapped data value (AnalogOutputInt16, etc.). - @param index: DNP3 index of the payload's data definition. - @param op_type: An OperateType, or None if command_type == 'Select'. - @return: A CommandStatus value. - """ - try: - point_value = self.point_definitions.point_value_for_command(command_type, command, index, op_type) - if point_value is None: - return opendnp3.CommandStatus.DOWNSTREAM_FAIL - except Exception as ex: - _log.error('No DNP3 PointDefinition for command with index {}'.format(index)) - return opendnp3.CommandStatus.DOWNSTREAM_FAIL - - try: - self._process_point_value(point_value) - except Exception as ex: - _log.error('Error processing DNP3 command: {}'.format(ex)) - # Delete a cached point value (typically occurs only if an error is being handled). - try: - self._current_point_values.get(point_value.point_def.data_type, {}).pop(int(point_value.index), None) - except Exception as err: - _log.error('Error discarding cached value {}'.format(point_value)) - return opendnp3.CommandStatus.DOWNSTREAM_FAIL - - return opendnp3.CommandStatus.SUCCESS - - def _process_point_value(self, point_value): - _log.info('Received DNP3 {}'.format(point_value)) - if point_value.command_type == 'Select': - # Perform any needed validation now, then wait for the subsequent Operate command. - return None - else: - # Update a dictionary that holds the most-recently-received value of each point. - self._current_point_values.setdefault(point_value.point_def.data_type, {})[ - int(point_value.index)] = point_value - return point_value - - def get_point_named(self, point_name): - return self.point_definitions.get_point_named(point_name) - - def update_array_for_point(self, point_value): - """A received point belongs to a PointArray. Update it.""" - if point_value.point_def.is_array_head_point: - self._current_array = PointArray(point_value.point_def) - elif self._current_array is None: - raise DNP3Exception('Array point received, but there is no current Array.') - elif not self._current_array.contains_index(point_value.index): - raise DNP3Exception('Received Array point outside of current Array.') - self._current_array.add_point_value(point_value) - - def update_input_point(self, point_def, value): - """ - Update an input point. This may send its PointValue to the Master. - - :param point_def: A PointDefinition. - :param value: A value to send (unwrapped simple data type, or else a list/array). - """ - if type(value) == list: - # It's an array. Break it down into its constituent points, and apply each one separately. - col_count = len(point_def.array_points) - cols_by_name = {pt['name']: col for col, pt in enumerate(point_def.array_points)} - for row_number, point_dict in enumerate(value): - for pt_name, pt_val in point_dict.items(): - pt_index = point_def.index + col_count * row_number + cols_by_name[pt_name] - array_point_def = self.point_definitions.get_point_named(point_def.name, index=pt_index) - self._apply_point_update(array_point_def, pt_index, pt_val) - else: - self._apply_point_update(point_def, point_def.index, value) - - @staticmethod - def _apply_point_update(point_def, point_index, value): - """ - Set an input point in the outstation database. This may send its PointValue to the Master. - - :param point_def: A PointDefinition. - :param point_index: A numeric index for the point. - :param value: A value to send (unwrapped, simple data type). - """ - data_type = point_def.data_type - if data_type == DATA_TYPE_ANALOG_INPUT: - wrapped_val = opendnp3.Analog(float(value)) - if isinstance(value, bool) or not isinstance(value, numbers.Number): - # Invalid data type - raise DNP3Exception('Received {} value for {}.'.format(type(value), point_def)) - elif data_type == DATA_TYPE_BINARY_INPUT: - wrapped_val = opendnp3.Binary(value) - if not isinstance(value, bool): - # Invalid data type - raise DNP3Exception('Received {} value for {}.'.format(type(value), point_def)) - else: - # The agent supports only DNP3's Analog and Binary point types at this time. - raise DNP3Exception('Unsupported point type {}'.format(data_type)) - if wrapped_val is not None: - DNP3Outstation.apply_update(wrapped_val, point_index) - _log.debug('Sent DNP3 point {}, value={}'.format(point_def, wrapped_val.value)) - - def publish_point_value(self, point_value): - """Publish a PointValue as it is received from the DNP3 Master.""" - _log.info('Publishing DNP3 {}'.format(point_value)) - msg = { - point_value.name: (point_value.unwrapped_value() if point_value else None) - } - - if point_value.point_def.action == PUBLISH_AND_RESPOND: - msg.update({ - 'response': point_value.point_def.response - }) - - self.publish_data(self.point_topic, msg) - - def publish_outstation_status(self, outstation_status): - """Publish outstation status.""" - _log.info('Publishing outstation status: {}'.format(outstation_status)) - self.publish_data(self.outstation_status_topic, outstation_status) - - def publish_data(self, topic, msg): - """Publish a payload to the message bus.""" - try: - self.vip.pubsub.publish(peer='pubsub', - topic=topic, - headers={headers.TIMESTAMP: utils.format_timestamp(utils.get_aware_utc_now())}, - message=msg) - except Exception as err: - if os.environ.get('UNITTEST', False): - _log.debug('Disregarding publish_data exception during unit test') - else: - raise DNP3Exception('Error publishing topic {}, message {}: {}'.format(topic, msg, err)) - - def dnp3_point_name(self, point_name): - """ - Return a point's DNP3 point name, mapped from its VOLTTRON point name if necessary. - - If VOLTTRON point names were configured (by the DNP device driver), map them to DNP3 point names. - """ - dnp3_point_name = self.volttron_points.get(point_name, '') if self.volttron_points else point_name - if not dnp3_point_name: - raise DNP3Exception('No configured point for {}'.format(point_name)) - return dnp3_point_name - - @RPC.export - def get_point(self, point_name): - """ - Look up the most-recently-received value for a given output point. - - @param point_name: The point name of a DNP3 PointDefinition. - @return: The (unwrapped) value of a received point. - """ - _log.info('Getting point value for {}'.format(point_name)) - try: - point_name = self.dnp3_point_name(point_name) - point_def = self.point_definitions.get_point_named(point_name) - point_value = self.get_current_point_value(point_def.data_type, point_def.index) - return point_value.unwrapped_value() if point_value else None - except Exception as e: - raise DNP3Exception(e) - - @RPC.export - def get_point_by_index(self, data_type, index): - """ - Look up the most-recently-received value for a given point. - - @param data_type: The data_type of a DNP3 point. - @param index: The index of a DNP3 point. - @return: The (unwrapped) value of a received point. - """ - _log.info('Getting point value for data_type {} and index {}'.format(data_type, index)) - try: - point_value = self.get_current_point_value(data_type, index) - return point_value.unwrapped_value() if point_value else None - except Exception as e: - raise DNP3Exception(e) - - @RPC.export - def get_points(self, point_list): - """ - Look up the most-recently-received value of each configured output point. - - @param point_list: A list of point names. - @return: A dictionary of point values, indexed by their point names. - """ - _log.info('Getting values for the following points: {}'.format(point_list)) - try: - return {name: self.get_point(name) for name in point_list} - except Exception as e: - raise DNP3Exception(e) - - @RPC.export - def get_configured_points(self): - """ - Look up the most-recently-received value of each configured point. - - @return: A dictionary of point values, indexed by their point names. - """ - if self.volttron_points is None: - raise DNP3Exception('DNP3 points have not been configured') - - _log.info('Getting all DNP3 configured point values') - try: - return {name: self.get_point(name) for name in self.volttron_points} - except Exception as e: - raise DNP3Exception(e) - - @RPC.export - def set_point(self, point_name, value): - """ - Set the value of a given input point. - - @param point_name: The point name of a DNP3 PointDefinition. - @param value: The value to set. The value's data type must match the one in the DNP3 PointDefinition. - """ - _log.info('Setting DNP3 {} point value = {}'.format(point_name, value)) - try: - self.update_input_point(self.get_point_named(self.dnp3_point_name(point_name)), value) - - except Exception as e: - raise DNP3Exception(e) - - @RPC.export - def set_points(self, point_dict): - """ - Set point values for a dictionary of points. - - @param point_dict: A dictionary of {point_name: value} for a list of DNP3 points to set. - """ - _log.info('Setting DNP3 point values: {}'.format(point_dict)) - try: - for point_name, value in point_dict.items(): - self.update_input_point(self.get_point_named(self.dnp3_point_name(point_name)), value) - except Exception as e: - raise DNP3Exception(e) - - @RPC.export - def config_points(self, point_map): - """ - For each of the agent's points, map its VOLTTRON point name to its DNP3 group and index. - - @param point_map: A dictionary that maps a point's VOLTTRON point name to its DNP3 group and index. - """ - _log.info('Configuring DNP3 points: {}'.format(point_map)) - self.volttron_points = point_map - - @RPC.export - def get_point_definitions(self, point_name_list): - """ - For each DNP3 point name in point_name_list, return a dictionary with each of the point definitions. - - The returned dictionary looks like this: - - { - "point_name1": { - "property1": "property1_value", - "property2": "property2_value", - ... - }, - "point_name2": { - "property1": "property1_value", - "property2": "property2_value", - ... - } - } - - If a definition cannot be found for a point name, it is omitted from the returned dictionary. - - :param point_name_list: A list of point names. - :return: A dictionary of point definitions. - """ - _log.info('Fetching a list of DNP3 point definitions for {}'.format(point_name_list)) - try: - response = {} - for name in point_name_list: - point_def = self.point_definitions.get_point_named(self.dnp3_point_name(name)) - if point_def is not None: - response[name] = point_def.as_json() - return response - except Exception as e: - raise DNP3Exception(e) diff --git a/services/core/DNP3Agent/dnp3/mesa/agent.py b/services/core/DNP3Agent/dnp3/mesa/agent.py deleted file mode 100644 index 4a0200a982..0000000000 --- a/services/core/DNP3Agent/dnp3/mesa/agent.py +++ /dev/null @@ -1,353 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, 8minutenergy / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Neither 8minutenergy nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by 8minutenergy or Kisensum. -# }}} -import logging -import sys - -from volttron.platform.agent import utils -from volttron.platform.vip.agent import RPC - -from dnp3.base_dnp3_agent import BaseDNP3Agent - -from dnp3.points import DNP3Exception -from dnp3 import DEFAULT_LOCAL_IP, DEFAULT_PORT -from dnp3 import DEFAULT_POINT_TOPIC, DEFAULT_OUTSTATION_STATUS_TOPIC -from dnp3 import PUBLISH, PUBLISH_AND_RESPOND - -from dnp3.mesa.functions import DEFAULT_FUNCTION_TOPIC, ACTION_PUBLISH_AND_RESPOND -from dnp3.mesa.functions import FunctionDefinitions, Function, FunctionException - -__version__ = '1.1' - -utils.setup_logging() -_log = logging.getLogger(__name__) - - -class MesaAgent(BaseDNP3Agent): - """ - MesaAgent is a VOLTTRON agent that handles MESA-ESS DNP3 outstation communications. - - MesaAgent models a DNP3 outstation, communicating with a DNP3 master. - - For further information about this agent, MESA-ESS, and DNP3 communications, please - see the VOLTTRON MESA-ESS agent specification, which can be found in VOLTTRON readthedocs - at http://volttron.readthedocs.io/en/develop/specifications/mesa_agent.html. - - This agent can be installed from a command-line shell as follows: - $ export VOLTTRON_ROOT= - $ cd $VOLTTRON_ROOT - $ source services/core/DNP3Agent/install_mesa_agent.sh - That file specifies a default agent configuration, which can be overridden as needed. - """ - - def __init__(self, functions=None, function_topic='', outstation_status_topic='', - all_functions_supported_by_default=False, - local_function_definitions_path=None, function_validation=False, **kwargs): - """Initialize the MESA agent.""" - super(MesaAgent, self).__init__(**kwargs) - self.functions = functions - self.function_topic = function_topic - self.outstation_status_topic = outstation_status_topic - self.all_functions_supported_by_default = all_functions_supported_by_default - self.function_validation = function_validation - - # Update default config - self.default_config.update({ - 'functions': functions, - 'function_topic': function_topic, - 'outstation_status_topic': outstation_status_topic, - 'all_functions_supported_by_default': all_functions_supported_by_default, - 'function_validation': function_validation - }) - - # Update default config in config store. - self.vip.config.set_default('config', self.default_config) - - self.function_definitions = None - self._local_function_definitions_path = local_function_definitions_path - - self._current_functions = dict() # {function_id: Function} - self._current_block = dict() # {name: name, index: index} - self._selector_block = dict() # {selector_block_point_name: {selector_index: [Step]}} - self._edit_selectors = list() # [{name: name, index: index}] - - def _configure_parameters(self, contents): - """ - Initialize/Update the MesaAgent configuration. - - See also the superclass version of this method, which does most of the initialization. - MesaAgent configuration parameters: - - functions: (string) A JSON structure of function definitions to be loaded. - function_topic: (string) Message bus topic to use when publishing MESA-ESS functions. - Default: mesa/function. - all_functions_supported_by_default: (boolean) When deciding whether to reject points for unsupported - functions, ignore the values of their 'supported' points: simply treat all functions as - supported. - Default: False. - """ - config = super(MesaAgent, self)._configure_parameters(contents) - self.functions = config.get('functions', {}) - self.function_topic = config.get('function_topic', DEFAULT_FUNCTION_TOPIC) - self.all_functions_supported_by_default = config.get('all_functions_supported_by_default', False) - self.function_validation = config.get('function_validation', False) - _log.debug('MesaAgent configuration parameters:') - _log.debug('\tfunctions type={}'.format(type(self.functions))) - _log.debug('\tfunction_topic={}'.format(self.function_topic)) - _log.debug('\tall_functions_supported_by_default={}'.format(bool(self.all_functions_supported_by_default))) - _log.debug('\tfuntion_validation={}'.format(bool(self.function_validation))) - self.load_function_definitions() - self.supported_functions = [] - # Un-comment the next line to do more detailed validation and print definition statistics. - # validate_definitions(self.point_definitions, self.function_definitions) - - def load_function_definitions(self): - """Populate the FunctionDefinitions repository from JSON in the config store.""" - _log.debug('Loading MESA function definitions') - try: - self.function_definitions = FunctionDefinitions(self.point_definitions) - self.function_definitions.load_functions(self.functions['functions']) - except (AttributeError, TypeError) as err: - if self._local_function_definitions_path: - _log.warning("Attempting to load Function Definitions from local path.") - self.function_definitions = FunctionDefinitions( - self.point_definitions, - function_definitions_path=self._local_function_definitions_path) - else: - raise DNP3Exception("Failed to load Function Definitions from config store: {}".format(err)) - - @RPC.export - def reset(self): - """Reset the agent's internal state, emptying point value caches. Used during iterative testing.""" - super(MesaAgent, self).reset() - self._current_functions = dict() - self._current_block = dict() - self._selector_block = dict() - self._edit_selectors = list() - - @RPC.export - def get_selector_block(self, block_name, index): - try: - return {step.definition.name: step.as_json() for step in self._selector_block[block_name][index]} - except KeyError: - _log.debug('Have not received data for Selector Block {} at Edit Selector {}'.format(block_name, index)) - return None - - def _process_point_value(self, point_value): - """ - A PointValue was received from the Master. Process its payload. - - :param point_value: A PointValue. - """ - try: - point_val = super(MesaAgent, self)._process_point_value(point_value) - - if point_val: - if point_val.point_def.is_selector_block: - self._current_block = { - 'name': point_val.point_def.name, - 'index': float(point_val.value) - } - _log.debug('Starting to receive Selector Block {name} at Edit Selector {index}'.format( - **self._current_block - )) - - # Publish mesa/point if the point action is PUBLISH or PUBLISH_AND_RESPOND - if point_val.point_def.action in (PUBLISH, PUBLISH_AND_RESPOND): - self.publish_point_value(point_value) - - self.update_function_for_point_value(point_val) - - if self._current_functions: - for current_func_id, current_func in self._current_functions.items(): - # if step action is ACTION_ECHO or ACTION_ECHO_AND_PUBLISH - if current_func.has_input_point(): - self.update_input_point( - self.get_point_named(current_func.input_point_name()), - point_val.unwrapped_value() - ) - - # if step is the last curve or schedule step - if self._current_block and point_val.point_def == current_func.definition.last_step.point_def: - current_block_name = self._current_block['name'] - self._selector_block.setdefault(current_block_name, dict()) - self._selector_block[current_block_name][self._current_block['index']] = current_func.steps - - _log.debug('Saved Selector Block {} at Edit Selector {}: {}'.format( - self._current_block['name'], - self._current_block['index'], - self.get_selector_block(self._current_block['name'], self._current_block['index']) - )) - - self._current_block = dict() - - # if step reference to a curve or schedule function - func_ref = current_func.last_step.definition.func_ref - if func_ref: - block_name = self.function_definitions[func_ref].first_step.name - block_index = float(point_val.value) - if not self._selector_block.get(block_name, dict()).get(block_index, None): - error_msg = 'Have not received data for Selector Block {} at Edit Selector {}' - raise DNP3Exception(error_msg.format(block_name, block_index)) - current_edit_selector = { - 'name': block_name, - 'index': block_index - } - if current_edit_selector not in self._edit_selectors: - self._edit_selectors.append(current_edit_selector) - - # if step action is ACTION_PUBLISH, ACTION_ECHO_AND_PUBLISH, or ACTION_PUBLISH_AND_RESPOND - if current_func.publish_now(): - self.publish_function_step(current_func.last_step) - - # if current function is completed - if current_func.complete: - self._current_functions.pop(current_func_id) - self._edit_selectors = list() - - except (DNP3Exception, FunctionException) as err: - self._current_functions = dict() - self._edit_selectors = list() - if type(err) == DNP3Exception: - raise DNP3Exception('Error processing point value: {}'.format(err)) - - def update_function_for_point_value(self, point_value): - """Add point_value to the current Function if appropriate.""" - error_msg = None - current_functions = self.current_function_for(point_value.point_def) - if not current_functions: - return None - for function_id, current_function in current_functions.items(): - try: - if point_value.point_def.is_array_point: - self.update_array_for_point(point_value) - current_function.add_point_value(point_value, - current_array=self._current_array, - function_validation=self.function_validation) - except (DNP3Exception, FunctionException) as err: - current_functions.pop(function_id) - if type(err) == DNP3Exception: - error_msg = err - if error_msg and not current_functions: - raise DNP3Exception('Error updating function: {}'.format(error_msg)) - - def current_function_for(self, new_point_def): - """A point was received. Return the current Function, updating it if necessary.""" - new_point_function_def = self.function_definitions.get_fdef_for_pdef(new_point_def) - if new_point_function_def is None: - return None - if self._current_functions: - current_funcs = dict() - for func_def in new_point_function_def: - val = self._current_functions.pop(func_def.function_id, None) - if val: - current_funcs.update({func_def.function_id: val}) - self._current_functions = current_funcs - else: - for func_def in new_point_function_def: - if not self.all_functions_supported_by_default and not func_def.supported: - raise DNP3Exception('Received a point for unsupported {}'.format(func_def)) - self._current_functions[func_def.function_id] = Function(func_def) - return self._current_functions - - def update_input_point(self, point_def, value): - """ - Update an input point. This may send its PointValue to the Master. - - :param point_def: A PointDefinition. - :param value: A value to send (unwrapped simple data type, or else a list/array). - """ - super(MesaAgent, self).update_input_point(point_def, value) - if type(value) != list: - # Side-effect: If it's a Support point for a Function, update the Function's "supported" property. - func = self.function_definitions.support_point_names().get(point_def.name, None) - if func is not None and func.supported != value: - _log.debug('Updating supported property to {} in {}'.format(value, func)) - func.supported = value - - def publish_function_step(self, step_to_send): - """A Function Step was received from the DNP3 Master. Publish the Function.""" - function_to_send = step_to_send.function - - points = {step.definition.name: step.as_json() for step in function_to_send.steps} - for edit_selector in self._edit_selectors: - block_name = edit_selector['name'] - index = edit_selector['index'] - try: - points[block_name][index] = self.get_selector_block(block_name, index) - except (KeyError, TypeError): - points[block_name] = { - index: self.get_selector_block(block_name, index) - } - - msg = { - "function_id": function_to_send.definition.function_id, - "function_name": function_to_send.definition.name, - "points": points - } - if step_to_send.definition.action == ACTION_PUBLISH_AND_RESPOND: - msg["expected_response"] = step_to_send.definition.response - _log.info('Publishing MESA {} message {}'.format(function_to_send, msg)) - self.publish_data(self.function_topic, msg) - - -def mesa_agent(config_path, **kwargs): - """ - Parse the MesaAgent configuration. Return an agent instance created from that config. - - :param config_path: (str) Path to a configuration file. - :returns: (MesaAgent) The MESA agent - """ - try: - config = utils.load_config(config_path) - except Exception: - config = {} - return MesaAgent(points=config.get('points', []), - functions=config.get('functions', []), - point_topic=config.get('point_topic', DEFAULT_POINT_TOPIC), - function_topic=config.get('function_topic', DEFAULT_FUNCTION_TOPIC), - outstation_status_topic=config.get('outstation_status_topic', DEFAULT_OUTSTATION_STATUS_TOPIC), - local_ip=config.get('local_ip', DEFAULT_LOCAL_IP), - port=config.get('port', DEFAULT_PORT), - outstation_config=config.get('outstation_config', {}), - all_functions_supported_by_default=config.get('all_functions_supported_by_default', False), - function_validation=config.get('function_validation', False), - **kwargs) - - -def main(): - """Main method called to start the agent.""" - utils.vip_main(mesa_agent, identity='mesaagent', version=__version__) - - -if __name__ == '__main__': - # Entry point for script - try: - sys.exit(main()) - except KeyboardInterrupt: - pass diff --git a/services/core/DNP3Agent/dnp3/mesa/conversion.py b/services/core/DNP3Agent/dnp3/mesa/conversion.py deleted file mode 100644 index f78824791a..0000000000 --- a/services/core/DNP3Agent/dnp3/mesa/conversion.py +++ /dev/null @@ -1,4 +0,0 @@ -import sys, yaml, json - -y=yaml.load(sys.stdin.read()) -print(json.dumps(y)) \ No newline at end of file diff --git a/services/core/DNP3Agent/dnp3/mesa/functions.py b/services/core/DNP3Agent/dnp3/mesa/functions.py deleted file mode 100644 index f6e0cc4c27..0000000000 --- a/services/core/DNP3Agent/dnp3/mesa/functions.py +++ /dev/null @@ -1,570 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, 8minutenergy / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Neither 8minutenergy nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by 8minutenergy or Kisensum. -# }}} -import argparse -import logging -import os -import collections.abc -import yaml - -from dnp3.points import PointDefinitions, PointDefinition, DNP3Exception - -DEFAULT_FUNCTION_TOPIC = 'mesa/function' - -# Values of StepDefinition.optional -OPTIONAL = 'O' -MANDATORY = 'M' -INITIALIZE = 'I' -ALL_OPTIONALITY = [OPTIONAL, MANDATORY, INITIALIZE] - -# Values of the elements of StepDefinition.fcodes: -DIRECT_OPERATE = 'direct_operate' # This is actually DIRECT OPERATE / RESPONSE -SELECT = 'select' # This is actually SELECT / RESPONSE -OPERATE = 'operate' # This is actually OPERATE / RESPONSE -READ = 'read' -RESPONSE = 'response' - -# Values of StepDefinition.action: -ACTION_ECHO = 'echo' -ACTION_PUBLISH = 'publish' -ACTION_ECHO_AND_PUBLISH = 'echo_and_publish' -ACTION_PUBLISH_AND_RESPOND = 'publish_and_respond' -ACTION_NONE = 'none' - -_log = logging.getLogger(__name__) - - -class FunctionDefinitions(collections.abc.Mapping): - """In-memory repository of FunctionDefinitions.""" - - def __init__(self, point_definitions, function_definitions_path=None): - """Data holder for all MESA-ESS functions.""" - self._point_definitions = point_definitions - self._functions = dict() # {function_id: FunctionDefinition} - self._pdef_function_map = dict() # {PointDefinition: [FunctionDefinition]} - if function_definitions_path: - file_path = os.path.expandvars(os.path.expanduser(function_definitions_path)) - self.load_functions_from_yaml_file(file_path) - - def __getitem__(self, function_id): - """Return the function associated with this function_id. Must be unique.""" - return self._functions[function_id] - - def __iter__(self): - return iter(self._functions) - - def __len__(self): - """Return the total number of functions from FunctionDefinitions.""" - return len(self._functions) - - @property - def all_function_ids(self): - """Return all function_id from FunctionDefinitions.""" - return self._functions.keys() - - @property - def function_def_lst(self): - """Return a list of all FunctionDefinition in the FunctionDefinitions.""" - return self._functions.values() - - def support_point_names(self): - """Return a dictionary of FunctionDefinitions keyed by their (non-null) support_point_names.""" - return {f.support_point_name: f - for f_id, f in self._functions.items() - if f.support_point_name is not None} - - def function_for_id(self, function_id): - """Return a specific function definition from (cached) dictionary of FunctionDefinitions.""" - return self._functions.get(function_id, None) - - def load_functions_from_yaml_file(self, function_definitions_path): - """Load and cache a YAML file of FunctionDefinitions. Index them by function name.""" - _log.debug('Loading MESA-ESS FunctionDefinitions from {}.'.format(function_definitions_path)) - if function_definitions_path: - fdef_path = os.path.expandvars(os.path.expanduser(function_definitions_path)) - self._functions = {} - try: - with open(fdef_path, 'r') as f: - self.load_functions(yaml.load(f)['functions']) - except Exception as err: - raise ValueError('Problem parsing {}. Error={}'.format(fdef_path, err)) - _log.debug('Loaded {} FunctionDefinitions'.format(len(self._functions.keys()))) - - def get_fdef_for_pdef(self, pdef): - """ - Return a list of FunctionDefinition that contains the PointDefinition or None otherwise. - - :param pdef: PointDefinition - """ - return self._pdef_function_map.get(pdef, None) - - def load_functions(self, function_definitions_json): - """ - Load and cache a JSON dictionary of FunctionDefinitions. Index them by function ID. - Check if function_id is unique and func_ref in steps are valid. - """ - self._functions = {} - try: - for function_def in function_definitions_json: - new_function = FunctionDefinition(self._point_definitions, function_def) - function_id = new_function.function_id - if self._functions.get(function_id, None): - raise ValueError('There are multiple functions for function id {}'.format(function_id)) - self._functions[function_id] = new_function - for pdef in new_function.all_point_defs(): - try: - self._pdef_function_map[pdef].append(new_function) - except KeyError: - self._pdef_function_map[pdef] = [new_function] - except Exception as err: - raise ValueError('Problem parsing FunctionDefinitions. Error={}'.format(err)) - - for fdef in self.function_def_lst: - for step in fdef.steps: - func_ref = step.func_ref - if func_ref and func_ref not in self.all_function_ids: - raise ValueError('Invalid Function Reference {} for Step {} in Function {}'. format( - func_ref, - step.step_number, - fdef.function_id - )) - - _log.debug('Loaded {} FunctionDefinitions'.format(len(self))) - - -class FunctionDefinition: - """A MESA-ESS FunctionDefinition (aka mode, command).""" - - def __init__(self, point_definitions, function_def_dict): - """ - Data holder for the definition of a MESA-ESS function. Including parsing data validation. - self._point_steps_map: dictionary mapping PointDefinition including all Array points to StepDefinition - self.steps: a list of all StepDefinition (not including array points) in the function - """ - self.function_id = function_def_dict.get('id', None) # Must be unique - self.name = function_def_dict.get('name', None) - self.mode_types = function_def_dict.get('mode_types', {}) - self.ref = function_def_dict.get('ref', None) - self.support_point_name = function_def_dict.get('support_point', None) - self._point_steps_map = {} - - # function_id and steps validation - if not self.function_id: - raise ValueError('Missing function ID') - json_steps = function_def_dict.get('steps', None) - if not json_steps: - raise ValueError('Missing steps for function {}'.format(self.function_id)) - - step_numbers = list() - try: - self.steps = [StepDefinition(point_definitions, self.function_id, step_def) for step_def in json_steps] - is_selector_block = self.is_selector_block - for step in self.steps: - step_number = step.step_number - - # Check if there are duplicated step number - if step_number in step_numbers: - raise ValueError('Duplicated step number {} for function {}'.format(step_number, self.function_id)) - step_numbers.append(step_number) - - # If function is selector block (curve or schedule), all steps must be mandatory or initialize - if is_selector_block and step.optional not in [INITIALIZE, MANDATORY]: - raise ValueError( - 'Function {} - Step {}: optionality must be either INITIALIZE or MANDATORY'.format( - self.function_id, step_number)) - - # Update self._point_steps_map - for pd in step.all_point_defs(): - self._point_steps_map[pd] = step - except AttributeError as err: - raise AttributeError('Error creating FunctionDefinition {}, err={}'.format(self.name, err)) - - # Check is there is missing steps - if set([i for i in range(1, len(self.steps) + 1)]) != set(step_numbers): - raise ValueError('There are missing steps for function {}'.format(self.function_id)) - - def __str__(self): - return 'Function {}'.format(self.name) - - def __contains__(self, point_def): - return point_def in self.all_point_defs() - - def __getitem__(self, point_def): - return self._point_steps_map[point_def] - - @property - def supported(self): - """ - Set supported to False if the Function has a defined support_point_name -- the Control Agent must set it. - To override this (support all functions), set config all_functions_supported_by_default = "True". - """ - return not self.support_point_name - - @property - def first_step(self): - """First step of the function. Mainly used for Selector Block.""" - for step in self.steps: - if step.step_number == 1: - return step - return None - - @property - def last_step(self): - """Last step of the function. Mainly used for Selector Block.""" - for step in self.steps: - if step.step_number == len(self.steps): - return step - return None - - @property - def is_selector_block(self): - return self.first_step.point_def and self.first_step.point_def.is_selector_block - - def instance(self): - """Return an instance of this FunctionDefinition.""" - return Function(self) - - def describe_function(self): - """Return a string describing a function: its name and all of its StepDefinitions.""" - return 'Function {}: {}'.format(self.name, [s.__str__() for s in self.steps]) - - def all_point_defs(self): - """Return all point definition including array points.""" - return self._point_steps_map.keys() - - def all_points(self): - """Return all point definition not including array points and None points.""" - return [step_def.point_def for step_def in self.steps if step_def] - - def is_mode(self): - """Return True if there is mode enable point in the function, False otherwise.""" - for point in self.all_points(): - if point and point.category == 'mode_enable': - return True - return False - - def get_mode_enable(self): - """Return a list of all mode enable points in the function.""" - return [point for point in self.all_points() if point and point.category == 'mode_enable'] - - -class StepDefinition: - """Step definition in a MESA-ESS FunctionDefinition.""" - - def __init__(self, point_definitions, function_id, step_def=None): - """ - Data holder for the definition of a step in a MESA-ESS FunctionDefinition. - - :param function_def: The FunctionDefinition to which the StepDefinition belongs. - :param step_def: A dictionary of data from which to create the StepDefinition. - """ - self.function_id = function_id - self.name = step_def.get('point_name', None) - self.point_def = point_definitions[self.name] - self.step_number = step_def.get('step_number', None) - self.optional = step_def.get('optional', OPTIONAL) - self.fcodes = step_def.get('fcodes', []) - self.action = step_def.get('action', None) - self.func_ref = step_def.get('func_ref', None) - self.description = step_def.get('description', None) - self.validate() - - try: - self.response = point_definitions[step_def.get('response', None)] - except Exception as err: - raise AttributeError('Response point in function {} step {} does not match point definition. Error={}'.format( - self.function_id, - self.step_number, - err - )) - - def __str__(self): - return '{} Step {}: {}'.format(self.function_id, self.step_number, self.name) - - def all_point_defs(self): - """Return a list of all PointDefinition including all Array points""" - all_defs = [self.point_def] - if self.point_def and self.point_def.is_array_head_point: - all_defs.extend(self.point_def.array_point_definitions) - return all_defs - - def validate(self): - if self.step_number is None: - raise AttributeError('Missing step number in function {}'.format(self.function_id)) - if not self.name: - raise AttributeError('Missing name in function {} step {}'.format(self.function_id, self.step_number)) - if self.optional not in ALL_OPTIONALITY: - raise AttributeError('Invalid optional value in function {} step {}: {}'.format(self.function_id, - self.step_number, - self.optional)) - if type(self.fcodes) != list: - raise AttributeError('Invalid fcodes in function {} step {}, type={}'.format(self.function_id, - self.step_number, - type(self.fcodes))) - for fc in self.fcodes: - if fc not in [DIRECT_OPERATE, SELECT, OPERATE, READ, RESPONSE]: - raise AttributeError('Invalid fcode in function {} step {}, fcode={}'.format(self.function_id, - self.step_number, - fc)) - if fc == READ and self.optional != OPTIONAL: - raise AttributeError('Invalid optionality in function {} step {}: must be OPTIONAL'.format( - self.function_id, - self.step_number - )) - - -class Step: - """A MESA-ESS Step that has been received by an outstation.""" - - def __init__(self, definition, func, value): - """ - Data holder for a received Step. - - :param definition: A StepDefinition. - :param value: A PointValue. - """ - self.definition = definition - self.function = func - self.value = value - - def __str__(self): - return '{}: {}'.format(self.definition, self.value) - - def as_json(self): - return self.value.as_json() if self.definition.point_def.is_array_head_point else self.value.unwrapped_value() - - def echoes_input(self): - return self.definition.action in [ACTION_ECHO, ACTION_ECHO_AND_PUBLISH] - - def publish(self): - return self.definition.action in [ACTION_PUBLISH, - ACTION_ECHO_AND_PUBLISH, - ACTION_PUBLISH_AND_RESPOND] - - -class FunctionException(Exception): - """ - Raise exceptions that are used for _process_point_value in Mesa agent. - Set the current function to None if the exception is raised. - """ - pass - - -class Function: - """A MESA-ESS Function that has been received by an outstation.""" - - def __init__(self, definition): - """ - Data holder for a Function received by an outstation. - - :param definition: A FunctionDefinition. - """ - self.definition = definition - self.steps = [] - - def __str__(self): - return 'Function {}'.format(self.definition.name) - - def __contains__(self, point_def): - if not isinstance(point_def, PointDefinition): - raise ValueError("Membership test only works for PointDefinition instance, not {}".format(point_def)) - return point_def in self.definition - - @property - def last_step(self): - """ - Return last received step of the function. - """ - return self.steps[-1] if self.steps else None - - @property - def complete(self): - """ - Return True if function is completed, False otherwise. - """ - if self.next_remaining_mandatory_step_number: - return False - return True - - @property - def next_remaining_mandatory_step_number(self): - """ - Return next remaining mandatory step number of the function if there is one existed, None otherwise. - """ - last_received_step_number = 0 if not self.last_step else self.last_step.definition.step_number - for step_def in self.definition.steps: - step_number = step_def.step_number - if step_number > last_received_step_number and step_def.optional in [MANDATORY, INITIALIZE]: - return step_number - return None - - def add_step(self, step_def, value, function_validation=False): - """ - Add a step to function if no mandatory step missing and return the step, raise exception otherwise. - - :param step_def: step definition to add to function - :param value: value of the point in step_def - :param function_validation: defaults to False. - When there is mandatory step missing, raise DNP3Exception if function_validation is True, - raise FunctionException otherwise. - FunctionException is used for _process_point_value in Mesa agent, if the FunctionException is raised, - reset current function to None and process the next point as the first step of a new function. - """ - # Check for missing mandatory steps up to the current step - if self.next_remaining_mandatory_step_number \ - and step_def.step_number > self.next_remaining_mandatory_step_number: - exception_message = '{} is missing Mandatory step number {}'.format( - self, - self.next_remaining_mandatory_step_number - ) - if function_validation: - raise DNP3Exception(exception_message) - raise FunctionException(exception_message) - # add current step to self.steps - step_value = Step(step_def, self, value) - self.steps.append(step_value) - return step_value - - def add_point_value(self, point_value, current_array=None, function_validation=False): - """ - Add a received PointValue as a Step in the current Function. Return the Step. - - :param point_value: point value - :param current_array: current array - :param function_validation: defaults to False. If function_validation is True, - raise DNP3Exception when getting an error while adding a new step to the current function. - If function_validation is False, reset current function to None if missing mandatory step, - set the adding step as the first step of the current function if step is not in order, - or replace the last step by the adding step if step is duplicated. - """ - step_def = self.definition[point_value.point_def] - step_number = step_def.step_number - if not self.last_step: - self.add_step(step_def, point_value, function_validation) - else: - last_received_step_number = self.last_step.definition.step_number - if step_number != last_received_step_number: - if step_number < last_received_step_number: - if self.next_remaining_mandatory_step_number: - if function_validation: - raise DNP3Exception('Step {} received after {}'.format(step_number, - last_received_step_number)) - # Since the old function was complete, treat this as the first step of a new function. - self.steps = [] - self.add_step(step_def, point_value, function_validation) - else: - if not point_value.point_def.is_array_point: - if function_validation: - raise DNP3Exception('Duplicate step number {} received'.format(step_number)) - self.steps.pop() - self.add_step(step_def, point_value, function_validation) - else: - # An array point was received for an existing step. Update the step's value. - self.last_step.value = current_array - - return self.last_step - - def has_input_point(self): - """Function has an input pont to be echoed following last step.""" - return self.last_step.echoes_input() if self.last_step else False - - def input_point_name(self): - """The name of the input point - - @todo This really should be a point_def - """ - return self.last_step.definition.response if self.last_step else '' - - def publish_now(self): - """The function has points to published following last step.""" - return self.last_step.publish() if self.last_step else False - - -def load_and_validate_definitions(): - """ - Standalone method, intended to be invoked from the command line. - - Load PointDefinition and FunctionDefinition files as specified in command line args, - and validate their contents. - """ - # Grab JSON and YAML definition file paths from the command line. - parser = argparse.ArgumentParser() - parser.add_argument('point_defs', help='path of the point definitions file (json)') - parser.add_argument('function_defs', help='path of the function definitions file (yaml)') - args = parser.parse_args() - - point_definitions = PointDefinitions(point_definitions_path=args.point_defs) - function_definitions = FunctionDefinitions(point_definitions, function_definitions_path=args.function_defs) - validate_definitions(point_definitions, function_definitions) - - -def validate_definitions(point_definitions, function_definitions): - """Validate PointDefinitions, Arrays, SelectorBlocks and FunctionDefinitions.""" - - print('\nValidating Point definitions...') - all_points = point_definitions.all_points() - print('\t{} point definitions'.format(len(all_points))) - - print('\nValidating Array definitions...') - array_head_points = [pt for pt in all_points if pt.is_array_head_point] - array_bounds = {pt: [pt.index, pt.array_last_index] for pt in array_head_points} - for pt in array_head_points: - # Print each array's definition. Also, check for overlapping array bounds. - print('\t{} ({}): indexes=({},{}), elements={}'.format(pt.name, - pt.data_type, - pt.index, - pt.array_last_index, - len(pt.array_points))) - for other_pt, other_bounds in array_bounds.iteritems(): - if pt.name != other_pt.name: - if other_bounds[0] <= pt.index <= other_bounds[1]: - print('\tERROR: Overlapping array definition in {} and {}'.format(pt, other_pt)) - if other_bounds[0] <= pt.array_last_index <= other_bounds[1]: - print('\tERROR: Overlapping array definition in {} and {}'.format(pt, other_pt)) - print('\t{} array definitions'.format(len(array_head_points))) - - print('\nValidating Selector Block definitions...') - selector_block_points = [pt for pt in all_points if pt.is_selector_block] - selector_block_bounds = {pt: [pt.selector_block_start, pt.selector_block_end] for pt in selector_block_points} - for pt in selector_block_points: - # Print each selector block's definition. Also, check for overlapping selector block bounds. - print('\t{} ({}): indexes=({},{})'.format(pt.name, - pt.data_type, - pt.selector_block_start, - pt.selector_block_end)) - for other_pt, other_bounds in selector_block_bounds.iteritems(): - if pt.name != other_pt.name: - if other_bounds[0] <= pt.selector_block_start <= other_bounds[1]: - print('\tERROR: Overlapping selector blocks in {} and {}'.format(pt, other_pt)) - if other_bounds[0] <= pt.selector_block_end <= other_bounds[1]: - print('\tERROR: Overlapping selector blocks in {} and {}'.format(pt, other_pt)) - # Check that each save_on_write point references a selector_block_point - print('\t{} selector block definitions'.format(len(selector_block_points))) - print('\nValidating Function definitions...') - functions = function_definitions.all_function_ids - print('\t{} function definitions'.format(len(functions))) diff --git a/services/core/DNP3Agent/dnp3/mesa/mesa_functions.yaml b/services/core/DNP3Agent/dnp3/mesa/mesa_functions.yaml deleted file mode 100644 index 500f52d3eb..0000000000 --- a/services/core/DNP3Agent/dnp3/mesa/mesa_functions.yaml +++ /dev/null @@ -1,2595 +0,0 @@ -functions: -- id: connect_and_disconnect - name: Connect and Disconnect - ref: AN2018 Spec section 2.4.4 Table 29 - steps: - - description: Set time window - fcodes: - - direct_operate - optional: I - point_name: DCTE.WinTms.AO16 - response: DCTE.WinTms.AI60 - step_number: 1 - - description: Set reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DCTE.RvrtTms.AO17 - response: DCTE.RvrtTms.AI61 - step_number: 2 - - description: Retrieve status of switch - fcodes: - - read - - response - optional: O - point_name: n/a - response: DSTO.DEROpSt.off.BI23 - step_number: 3 - - action: publish - description: Issue switch control command and receive response - fcodes: - - select - - operate - optional: M - point_name: CSWI.Pos.BO5 - response: DSTO.DEROpSt.off.BI23 - step_number: 4 - - description: Detect if switch is moving - fcodes: - - read - - response - optional: O - point_name: n/a - response: CSWI.Pos.BI24 - step_number: 5 -- id: cease_to_energize_and_return_to_service - name: Cease to Energize and Return to Service - ref: AN2018 Spec section 2.4.5 Table 30 - steps: - - description: Set Cease to Energize Time Window - fcodes: - - direct_operate - optional: I - point_name: DCTE.WinTms.AO13 - response: DCTE.WinTms.AI57 - step_number: 1 - - description: Set Cease to Energize Ramp DownTime - fcodes: - - direct_operate - optional: I - point_name: DCTE.RmpTms.AO14 - response: DCTE.RmpTms.AI58 - step_number: 2 - - description: Set Cease to Energize Timeout Period - fcodes: - - direct_operate - optional: I - point_name: DCTE.RvrtTms.AO15 - response: DCTE.RvrtTms.AI59 - step_number: 3 - - description: Cause DER to Cease to Energize - fcodes: - - select - - operate - optional: M - point_name: DCTE.CeaEngzReq.BO2 - response: DSTO.DEROpSt.connectedandidle.BI14 - step_number: 4 - - description: Give DER Permission to Stop - fcodes: - - select - - operate - optional: M - point_name: DSTO.PrmDscon.BO4 - response: DSTO.PrmDscon.BI17 - step_number: 5 - - description: Confirm DER is Stopping - fcodes: - - read - - response - optional: O - point_name: n/a - response: DSTO.DEROpSt.stopping.BI13 - step_number: 6 - - description: Confirm DER has Ceased to Energize - fcodes: - - read - - response - optional: O - point_name: n/a - response: DSTO.DEROpSt.ceasedtoenergize.BI15 - step_number: 7 - - description: Set High Voltage Limit - fcodes: - - direct_operate - optional: I - point_name: DCTE.VHiLim.AO6 - response: DCTE.VHiLim.AI50 - step_number: 8 - - description: Set Low Voltage Limit - fcodes: - - direct_operate - optional: I - point_name: DCTE.VLoLim.AO7 - response: DCTE.VLoLim.AI51 - step_number: 9 - - description: Set High Frequency Limit - fcodes: - - direct_operate - optional: I - point_name: DCTE.HzHiLim.AO8 - response: DCTE.HzHiLim.AI52 - step_number: 10 - - description: Set Low Frequency Limit - fcodes: - - direct_operate - optional: I - point_name: DCTE.HzLoLim.AO9 - response: DCTE.HzLoLim.AI53 - step_number: 11 - - description: Set Delay Time - fcodes: - - direct_operate - optional: I - point_name: DCTE.RtnDlyTmms.AO10 - response: DCTE.RtnDlTmms.AI54 - step_number: 12 - - description: Set Return to Service Time Window - fcodes: - - direct_operate - optional: I - point_name: DCTE.WinTms.AO11 - response: DCTE.WinTms.AI55 - step_number: 13 - - description: Set Return to Service Ramp Up Time - fcodes: - - direct_operate - optional: I - point_name: DCTE.RtnRmpTmms.AO12 - response: DCTE.RtnRmpTmms.AI56 - step_number: 14 - - description: Cause DER to Return to Service - fcodes: - - select - - operate - optional: M - point_name: DCTE.RtnSrvReq.BO1 - response: DSTO.DEROpSt.startingandsynchronizing.BI12 - step_number: 15 - - action: publish - description: Give DER Permission to Start - fcodes: - - select - - operate - optional: M - point_name: DSTO.PrmConn.BO3 - response: DSTO.PrmConn.BI16 - step_number: 16 - - description: Confirm DER is Started - fcodes: - - read - - response - optional: O - point_name: n/a - response: DSTO.DEROpSt.connectedandidle.BI14 - step_number: 17 -- id: enable_low_high_voltage_ride-through_mode - mode_types: - curve: - - 9 - - 10 - - 11 - - 12 - schedule: - - 1 - - 2 - - 3 - - 4 - name: Enable Low/High Voltage Ride-Through Mode - ref: AN2018 Spec section 2.5.1 Table 33 - steps: - - description: Set the Reference Voltage if it is not already set - fcodes: - - direct_operate - optional: I - point_name: DECP.VRef.AO0 - response: DECP.VRef.AI29 - step_number: 1 - - description: Set the Reference Voltage Offset if it is not already set - fcodes: - - direct_operate - optional: I - point_name: DECP.VRefOfs.AO1 - response: DECP.VRefOfs.AI30 - step_number: 2 - - description: Identify the meter used to measure the voltage. By default this - is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DHVT.EcpRef.AO22 - response: DHVT.EcpRef.AI71 - step_number: 3 - - description: 'DGSMn.ModTyp.AO245 = <9> HVRT Must Trip: If the curve is a must - trip curve, identify the index of the curve which specifies trip points when - the voltage is high' - fcodes: - - direct_operate - func_ref: curve - optional: O - point_name: PTOV.BlkRef.AO23 - response: PTOV.BlkRef.AI73 - step_number: 4 - - description: 'DGSMn.ModTyp.AO245 = <11> LVRT Must Trip: If the curve is a must - trip curve, identify the index of the curve which specifies trip points when - the voltage is low' - fcodes: - - direct_operate - func_ref: curve - optional: O - point_name: PTUV.BlkRef.AO24 - response: PTUV.BlkRef.AI74 - step_number: 5 - - description: 'DGSMn.ModTyp.AO245 = <10> HVRT Momentary Cessation: If the curve - is a must trip curve, identify the index of the curve which specifies where - generation/discharging must stop when the voltage is high' - fcodes: - - direct_operate - func_ref: curve - optional: O - point_name: PTOV.BlkRef.AO25 - response: PTOV.BlkRef.AI75 - step_number: 6 - - description: 'DGSMn.ModTyp.AO245 = <12> LVRT Momentary Cessation: If the curve - is a must trip curve, identify the index of the curve which specifies where - generation/discharging must stop when the voltage is low' - fcodes: - - direct_operate - func_ref: curve - optional: O - point_name: PTUV.BlkRef.AO26 - response: PTUV.BlkRef.AI76 - step_number: 7 - - action: publish - description: Enable the Low/High Voltage Ride-Through Mode - fcodes: - - select - - operate - optional: M - point_name: DHVT.ModEna.BO12 - response: DHVT.ModEna.BI64 - step_number: 8 -- id: enable_low_high_frequency_ride-through_mode - mode_types: - curve: - - 13 - - 14 - - 15 - - 16 - schedule: - - 5 - - 6 - - 7 - - 8 - name: Enable Low/High Frequency Ride-Through Mode - ref: AN2018 Spec section 2.5.2 Table 35 - steps: - - description: Set the Nominal Grid Frequency if it is not already set - fcodes: - - direct_operate - optional: I - point_name: DECP.EcpNomHz.AO2 - response: DECP.EcpNomHz.AI31 - step_number: 1 - - description: Identify the meter used to measure the frequency. By default this - is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DHFT.EcpRef.AO27 - response: DHFT.EcpRef.AI77 - step_number: 2 - - description: 'DGSMn.ModTyp.AO245 = <13> HFRT Must Trip: Identify the index of - the frequency ride through curve which specifies trip ponts when the frequency - is high' - fcodes: - - direct_operate - func_ref: curve - optional: O - point_name: PTOF.BlkRef.AO28 - response: PTOF.BlkRef.AI79 - step_number: 3 - - description: 'DGSMn.ModTyp.AO245 = <15> LFRT Must Trip: Identify the index of - the frequency ride through curve which specifies trip ponts when the voltage - is low' - fcodes: - - direct_operate - func_ref: curve - optional: O - point_name: PTUF.BlkRef.AO29 - response: PTUF.BlkRef.AI80 - step_number: 4 - - description: 'DGSMn.ModTyp.AO245 = <14> HFRT Mandatory Operation: Identify the - index of the frequency ride through curve which specifies where generation/discharging - must stop when the frequency is high' - fcodes: - - direct_operate - func_ref: curve - optional: O - point_name: PTOF.BlkRef.AO30 - response: PTOF.BlkRef.AI81 - step_number: 5 - - description: 'DGSMn.ModTyp.AO245 = <16> LFRT Mandatory Operation: Identify the - index of the frequency ride through curve which specifies where generation/discharging - must stop when the frequency is high' - fcodes: - - direct_operate - func_ref: curve - optional: O - point_name: PTUF.BlkRef.AO31 - response: PTUF.BlkRef.AI82 - step_number: 6 - - action: publish - description: Enable the Low/High Frequency Ride-Through Mode - fcodes: - - select - - operate - optional: M - point_name: DHFT.ModEna.BO13 - response: DHFT.ModEna.BI65 - step_number: 7 -- id: enable_frequency-watt_mode - name: Enable Frequency-Watt Mode - ref: AN2018 Spec section 2.5.3 Table 36 - mode_types: - schedule: - - 11 - steps: - - description: If not already established, set the Nominal Grid Frequency - fcodes: - - direct_operate - optional: I - point_name: DECP.EcpNomHz.AO2 - response: DECP.EcpNomHz.AI31 - step_number: 1 - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DHFW2.ModPrio.AO57 - response: DHFW2.ModPrio.AI115 - step_number: 2 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DHFW.WinTms.AO58 - response: DHFW.WinTms.AI116 - step_number: 3 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DHFW.RmpTms.AO59 - response: DHFW.RmpTms.AI117 - step_number: 4 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DHFW.RvrtTms.AO60 - response: DHFW.RvrtTms.AI118 - step_number: 5 - - description: Identify the meter used to measure the frequency. By default this - is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DHFW.EcpRef.AO61 - response: DHFW2.EcpRef.AI119 - step_number: 6 - - description: Set the High Starting Frequency - fcodes: - - direct_operate - optional: I - point_name: DHFW.HzStr.AO62 - response: DHFW2.HzStr.AI121 - step_number: 7 - - description: Set the High Stopping Frequency - fcodes: - - direct_operate - optional: I - point_name: DHFW.HzStop.AO63 - response: DHFW2.HzStop.AI122 - step_number: 8 - - description: Set the High Discharging Gradient - fcodes: - - direct_operate - optional: I - point_name: DHFW.WGra.AO64 - response: DHFW.WGra.AI123 - step_number: 9 - - description: Set the High Charging Gradient - fcodes: - - direct_operate - optional: I - point_name: DHFW.WChaGra.AO65 - response: DHFW.WChaGra.AI124 - step_number: 10 - - description: Set the Low Starting Frequency - fcodes: - - direct_operate - optional: I - point_name: DLFW.HzStr.AO66 - response: DLFW2.HzStr.AI125 - step_number: 11 - - description: Set the Low Stopping Frequency - fcodes: - - direct_operate - optional: I - point_name: DLFW.HzStop.AO67 - response: DLFW2.HzStop.AI126 - step_number: 12 - - description: Set the Low Discharging Gradient - fcodes: - - direct_operate - optional: I - point_name: DLFW.WGra.AO68 - response: DLFW.WGra.AI127 - step_number: 13 - - description: Set the Low Charging Gradient - fcodes: - - direct_operate - optional: I - point_name: DLFW.WChaGra.AO69 - response: DLFW.WChaGra.AI128 - step_number: 14 - - description: Set the Start Delay - fcodes: - - direct_operate - optional: I - point_name: DHFW2.ActStrDlTmms.AO70 - response: DHFW2.ActStrDlTmms.AI129 - step_number: 15 - - description: Set the Stop Delay - fcodes: - - direct_operate - optional: I - point_name: DHFW2.ActStopDlTmms.AO71 - response: DHFW2.ActStopDlTmms.AI130 - step_number: 16 - - description: Set the Ramp Up Time Constant - fcodes: - - direct_operate - optional: I - point_name: DHFW.OpnLoop.AO72 - response: DLFW.OpnLoopMax.AI131 - step_number: 17 - - description: Set the Ramp Down Time Constant - fcodes: - - direct_operate - optional: I - point_name: DHFW.OpnLoop.AO73 - response: DHFW.OpnLoopMax.AI132 - step_number: 18 - - description: Set the Discharging Up Ramp Rate - fcodes: - - direct_operate - optional: I - point_name: DHFW.DschRpuRte.AO74 - response: DHFW.RpuRte.AI133 - step_number: 19 - - description: Set the Discharging Down Ramp Rate - fcodes: - - direct_operate - optional: I - point_name: DHFW.DschRpdRte.AO75 - response: DHFW.RpdRteMax.AI134 - step_number: 20 - - description: Set the Charging Up Ramp Rate - fcodes: - - direct_operate - optional: I - point_name: DHFW.ChaRpuRte.AO76 - response: DHFW.RpuChaRte.AI135 - step_number: 21 - - description: Set the Charging Down Ramp Rate - fcodes: - - direct_operate - optional: I - point_name: DHFW.ChaRpdRte.AO77 - response: DHFW.RpdChaRteMax.AI136 - step_number: 22 - - description: Set the Hight Return Gradient - fcodes: - - direct_operate - optional: I - point_name: DHFW.RtnRmpRte.AO78 - response: DHFW2.RtnRmpRte.AI137 - step_number: 23 - - description: Set the Low Return Gradient - fcodes: - - direct_operate - optional: I - point_name: DLFW.RtnRmpRte.AO79 - response: DLFW2.RtnRmpRte.AI138 - step_number: 24 - - description: Set the minium State of Charge to be used by this mode - fcodes: - - direct_operate - optional: I - point_name: DHFW.SocUseMin.AO80 - response: DAGC.SocUseMinPct.AI140 - step_number: 25 - - description: Set the maximum State of Charge to be used by this mode - fcodes: - - direct_operate - optional: I - point_name: DHFW.SocUseMax.AO81 - response: DAGC.SocUseMaxPct.AI141 - step_number: 26 - - description: Enable or Disable Hysteresis - fcodes: - - direct_operate - optional: I - point_name: DHFW.HysEna.BO34 - response: DHFW.HysEna.BI86 - step_number: 27 - - description: Enable or Disable Snapshot of Power - fcodes: - - direct_operate - optional: I - point_name: DHFW.SnptEna.BO35 - response: DHFW.SnptEna.BI87 - step_number: 28 - - action: publish - description: Enable Frequency-Watt Mode - fcodes: - - select - - operate - optional: M - point_name: DHFW.ModEna.BO16 - response: DHFW.ModEna.BI68 - step_number: 29 -- id: enable_dynamic_reactive_current_support_mode - name: Enable Dynamic Reactive Current Support Mode - ref: AN2018 Spec section 2.5.4 Table 37 - mode_types: - schedule: - - 9 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DRGS.ModPrio.AO32 - response: DRGS.ModPrio.AI83 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DRGS.WinTms.AO33 - response: DRGS.WinTms.AI84 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DRGS.RmpTms.AO34 - response: DRGS.RmpTms.AI85 - step_number: 3 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DRGS.RvrtTms.AO35 - response: DRGS.RvrtTms.AI86 - step_number: 4 - - description: Identify the meter used to measure the voltage. By default this - is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DRGS.EcpRef.AO36 - response: DRGS.EcpRef.AI87 - step_number: 5 - - description: Set the Gradient Mode to select the curve shape - fcodes: - - direct_operate - optional: M - point_name: DRGS.ArGraMod.AO37 - response: DRGS.ArGraMod.AI91 - step_number: 6 - - description: Set the Deadband Minimum Voltage - fcodes: - - direct_operate - optional: M - point_name: DRGS.DbVMin.AO38 - response: DRGS.DbVMin.AI92 - step_number: 7 - - description: Set the Deadband Maximum Voltage - fcodes: - - direct_operate - optional: M - point_name: DRGS.DbVMax.AO39 - response: DRGS.DbVMax.AI93 - step_number: 8 - - description: Set the Reactive Current Support Gradient for Sags - fcodes: - - direct_operate - optional: M - point_name: DRGS.ArGraSag.AO40 - response: DRGS.ArGraSag.AI94 - step_number: 9 - - description: Set the Reactive Current Support Gradient for Swells - fcodes: - - direct_operate - optional: M - point_name: DRGS.ArGraSwl.AO41 - response: DRGS.ArGraSwl.AI95 - step_number: 10 - - description: Set the Filter Time for the Moving Average Voltage in seconds - fcodes: - - direct_operate - optional: M - point_name: DRGS.FilTms.AO42 - response: DRGS.FilTms.AI96 - step_number: 11 - - description: Enable Event-Based Reactive Current Support if required. It shall - default to Disabled. - fcodes: - - direct_operate - optional: I - point_name: DRGS.ArGraMod.BO33 - response: DRGS.ModEna.BI85 - step_number: 12 - - description: Set the Hold Time in milliseconds if Event-Based Reactive Current - Support is required. - fcodes: - - direct_operate - optional: I - point_name: DRGS.HoldTmms.AO46 - response: DRGS.HoldTmms.AI100 - step_number: 13 - - description: Set the Block Zone Voltage if required. Otherwise it shall default - to zero. - fcodes: - - direct_operate - optional: I - point_name: DRGS.BlkZnV.AO43 - response: DRGS.BlkZnV.AI97 - step_number: 14 - - description: Set the Hysteresis Block Zone Voltage if required. Otherwise it - shall default to zero. - fcodes: - - direct_operate - optional: I - point_name: DRGS.HysBlkZnV.AO44 - response: DRGS.HysBlkZnV.AI98 - step_number: 15 - - description: Set the Block Zone Time in milliseconds if required. Otherwise it - shall default to zero. - fcodes: - - direct_operate - optional: I - point_name: DRGS.BlkZnTmms.AO45 - response: DRGS.BlkZnTmms.AI99 - step_number: 16 - - action: publish - description: Enable Dynamic Reactive Current Mode - fcodes: - - select - - operate - optional: M - point_name: DRGS.ModEna.BO14 - response: DRGS.ModEna.BI66 - step_number: 17 -- id: enable_volt-watt_mode - name: Enable Volt-Watt Mode - ref: AN2018 Spec section 2.5.5 Table 38 - steps: - - description: If not already established, set the Reference Voltage - fcodes: - - direct_operate - optional: I - point_name: DECP.VRef.AO0 - response: DECP.VRef.AI29 - step_number: 1 - - description: If not already established, set the Reference Voltage Offset - fcodes: - - direct_operate - optional: I - point_name: DECP.VRefOfs.AO1 - response: DECP.VRefOfs.AI30 - step_number: 2 - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DVWD.ModPrio.AO48 - response: DVWD.ModPrio.AI102 - step_number: 3 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DVWD.WinTms.AO49 - response: DVWD.WinTms.AI103 - step_number: 4 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DVWD.RmpTms.AO50 - response: DVWD.RmpTms.AI104 - step_number: 5 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DVWD.RvrtTms.AO51 - response: DVWD.RvrtTms.AI105 - step_number: 6 - - description: Identify the meter used to measure the voltage. By default this - is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DVWD2.EcpRef.AO52 - response: DVWD2.EcpRef.AI106 - step_number: 7 - - description: Set the Dynamic Volt-Watt Gradient - fcodes: - - direct_operate - optional: I - point_name: DVWD.DynVWGra.AO53 - response: DVWD.DynVWGra.AI110 - step_number: 8 - - description: Set the Dynamic Volt-Watt Filter Time - fcodes: - - direct_operate - optional: I - point_name: DVWD.VWFilTms.AO54 - response: DVWD.VWFilTms.AI111 - step_number: 9 - - description: Set the Dynamic Volt-Watt Lower Deadband - fcodes: - - direct_operate - optional: I - point_name: DVWD.DbVWLo.AO55 - response: DVWD.DbVWLo.AI112 - step_number: 10 - - description: Set the Dynamic Volt-Watt Upper Deadband - fcodes: - - direct_operate - optional: I - point_name: DVWD.DbVWHi.AO56 - response: DVWD.DbVWHi.AI113 - step_number: 11 - - action: publish - description: Enable Dynamic Volt-Watt mode - fcodes: - - select - - operate - optional: M - point_name: DVWD.ModEna.BO15 - response: DVWD.ModEna.BI67 - step_number: 12 -- id: enable_active_power_limit_mode - name: Enable Active Power Limit Mode - ref: AN2018 Spec section 2.6.1 Table 39 - mode_types: - schedule: - - 12 - - 13 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DWMX.ModPrio.AO82 - response: DWMX.ModPrio.AI142 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DWMX.WinTms.AO83 - response: DWMX.WinTms.AI143 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DWMX.RmpTms.AO84 - response: DWMX.RmpTms.AI144 - step_number: 3 - - description: Set reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DWMX.RvrtTms.AO85 - response: DWMX.RvrtTms.AI145 - step_number: 4 - - description: Identify the meter used to measure the active power. By default - this is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DWMX.EcpRef.AO86 - response: DWMX.EcpRef.AI146 - step_number: 5 - - description: Retrieve Maximum Active Generation Power Capability - fcodes: - - read - - response - optional: O - point_name: n/a - response: DGEN.WMax.AI32 - step_number: 6 - - description: Retrieve Maximum Active Charging Power Capability - fcodes: - - read - - response - optional: O - point_name: n/a - response: DSTO.ChaWMax.AI33 - step_number: 7 - - description: Set maximum output in percent of nominal Watts (Charging) - fcodes: - - direct_operate - optional: M - point_name: DWMX.WLimPct.AO87 - response: DWMX.WLimPct.AI148 - step_number: 8 - - description: Set maximum output in percent of nominal Watts (Generating) - fcodes: - - direct_operate - optional: M - point_name: DWMN.WLimPct.AO88 - response: DWMN.WLimPct.AI149 - step_number: 9 - - action: publish - description: Enable Active Power Limit mode and receive response - fcodes: - - select - - operate - optional: M - point_name: DWLM.ModEna.BO17 - response: DWLM.ModEna.BI69 - step_number: 10 -- id: enable_charge_discharge_storage_mode - name: Enable Charge/Discharge Storage Mode - ref: AN2018 Spec section 2.6.2 Table 41 - mode_types: - schedule: - - 14 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DWGC.ModPrio.AO89 - response: DWGC.ModPrio.AI150 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DWGC.WinTms.AO90 - response: DWGC.WinTms.AI151 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DWGC.RmpTms.AO91 - response: DWGC.RmpTms.AI152 - step_number: 3 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DWGC.RvrtTms.AO92 - response: DWGC.RvrtTms.AI153 - step_number: 4 - - description: Select whether to use Ramp Rates or Time Constants - fcodes: - - direct_operate - optional: I - point_name: DWGC.UseRmpRte.BO38 - response: DWGC.UseRmpRte.BI90 - step_number: 5 - - description: 'If DWGC.UseRmpRte = 0: Set Charge/Discharge Time Constant Ramp Up - Time' - fcodes: - - direct_operate - optional: O - point_name: DWGC.OpnLoop.AO94 - response: DWGC.OpnLoopMax.AI155 - step_number: 6 - - description: 'If DWGC.UseRmpRte = 0: Set Charge/Discharge Time Constant Ramp Down - Time' - fcodes: - - direct_operate - optional: O - point_name: DWGC.OpnLoop.AO95 - response: DWGC.OpnLoopMax.AI156 - step_number: 7 - - description: 'If DWGC.UseRmpRte = 1: Set Discharge Ramp Up Rate' - fcodes: - - direct_operate - optional: O - point_name: DWGC.DschRpuRte.AO96 - response: DWGC.RpuRte.AI157 - step_number: 8 - - description: 'If DWGC.UseRmpRte = 1: Set Discharge Ramp Down Rate' - fcodes: - - direct_operate - optional: O - point_name: DWGC.DschRpdRte.AO97 - response: DWGC.RpdRteMax.AI158 - step_number: 9 - - description: 'If DWGC.UseRmpRte = 1: Set Charge Ramp Up Rate' - fcodes: - - direct_operate - optional: O - point_name: DWGC.ChaRpuRte.AO98 - response: DWGC.RpuChaRte.AI159 - step_number: 10 - - description: 'If DWGC.UseRmpRte = 1: Set Charge Ramp Down Rate' - fcodes: - - direct_operate - optional: O - point_name: DWGC.ChaRpdRte.AO99 - response: DWGC.RpdChaRteMax.AI160 - step_number: 11 - - description: Set Minimum Reserve for Storage (percent of Battery Capacity Rating) - fcodes: - - direct_operate - optional: I - point_name: DWGC.SocUseMinPct.AO100 - response: DWGC.SocUseMinPct.AI161 - step_number: 12 - - description: Set Maximum Reserve for Storage (percent of Battery Capacity Rating) - fcodes: - - direct_operate - optional: I - point_name: DWGC.SocUseMaxPct.AO101 - response: DWGC.SocUseMaxPct.AI162 - step_number: 13 - - description: Set discharge/charge Active Power Target. Positive is discharging, - negative is charging. - fcodes: - - direct_operate - optional: M - point_name: DWGC.GnWPctSpt.AO93 - response: DWGC.GnWPctSpt.AI154 - step_number: 14 - - action: publish - description: Enable charge/discharge mode and receive response - fcodes: - - select - - operate - optional: M - point_name: DWGC.ModEna.BO18 - response: DWGC.ModEna.BI70 - step_number: 15 -- id: enable_coordinated_charge_discharge_management_mode - name: Enable Coordinated Charge/Discharge Management Mode - ref: AN2018 Spec section 2.6.3 Table 42 - mode_types: - schedule: - - 15 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DTCD.ModPrio.AO102 - response: DTCD.ModPrio.AI163 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DTCD.WinTms.AO103 - response: DTCD.WinTms.AI164 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DTCD.RmpTms.AO104 - response: DTCD.RmpTms.AI165 - step_number: 3 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DTCD.RvrtTms.AO105 - response: DTCD.RvrtTms.AI166 - step_number: 4 - - description: Set the Target State of Charge, as a percentage of Usable Capacity - fcodes: - - direct_operate - optional: M - point_name: DTCD.SocUseTgtPct.AO106 - response: DTCD.SocUseTgtPct.AI167 - step_number: 5 - - description: Set the Target Date Charge Needed - fcodes: - - direct_operate - optional: M - point_name: DTCD.DateTgt.AO107 - response: DTCD.DateTgt.AI168 - step_number: 6 - - description: Set the Target Time Charge Needed (milliseconds since midnight) - fcodes: - - direct_operate - optional: M - point_name: DTCD.DateTgtTms.AO108 - response: DTCD.DateTgtTms.AI169 - step_number: 7 - - action: publish - description: Enable coordinated charge/discharge mode and receive response - fcodes: - - select - - operate - optional: M - point_name: DWGC.ModEna.BO18 - response: DTCD.ModEna.BI71 - step_number: 8 -- id: enable_active_power_response_mode_1 - name: Enable Active Power Response Mode 1 - ref: AN2018 Spec section 2.6.4 Table 43 - mode_types: - schedule: - - 16 - steps: - - description: 'Set the priority of mode #1' - fcodes: - - direct_operate - optional: I - point_name: DPKP.ModPrio.AO115 - response: DPKP.ModPrio.AI176 - step_number: 1 - - description: 'Set enabling time window of mode #1' - fcodes: - - direct_operate - optional: I - point_name: DPKP.WinTms.AO116 - response: DPKP.WinTms.AI177 - step_number: 2 - - description: 'Set enabling ramp time of mode #1' - fcodes: - - direct_operate - optional: I - point_name: DPKP.RmpTms.AO117 - response: DPKP.RmpTms.AI178 - step_number: 3 - - description: 'Set reversion timeout period of mode #1' - fcodes: - - direct_operate - optional: I - point_name: DPKP.RvrtTms.AO118 - response: DPKP.RvrtTms.AI179 - step_number: 4 - - description: 'Identify the meter used to measure the Reference Power Input of - mode #1. By default this is the System Meter (ID = 0)' - fcodes: - - direct_operate - optional: I - point_name: DPKP.EcpRef.AO119 - response: DPKP.EcpRef.AI180 - step_number: 5 - - description: 'Set the power threshold for activating mode #1' - fcodes: - - direct_operate - optional: M - point_name: DPKP.PkPwrWLim.AO120 - response: DPKP.PkPwrWLim.AI182 - step_number: 6 - - description: 'Set the ratio used to calculate the output power from the measured - power of mode #1' - fcodes: - - direct_operate - optional: M - point_name: DPKP.PkPwrFolPct.AO121 - response: DPKP.PkPwrFolPct.AI183 - step_number: 7 - - description: 'Set the maximum ramp up rate of mode #1' - fcodes: - - direct_operate - optional: I - point_name: DPKP.RpuRte.AO122 - response: DPKP.RpuRte.AI184 - step_number: 8 - - description: 'Set the maximum ramp down rate of mode #1' - fcodes: - - direct_operate - optional: I - point_name: DPKP.RpdRte.AO123 - response: DPKP.RpdRte.AI185 - step_number: 9 - - action: publish - description: 'Enable the active response mode #1' - fcodes: - - select - - operate - optional: M - point_name: DPKP.ModEna.BO20 - response: DPKP.ModEna.BI72 - step_number: 10 -- id: enable_active_power_response_mode_2 - name: Enable Active Power Response Mode 2 - ref: AN2018 Spec section 2.6.4 Table 43 - mode_types: - schedule: - - 17 - steps: - - description: 'Set the priority of mode #2' - fcodes: - - direct_operate - optional: I - point_name: DGFL.ModPrio.AO124 - response: DGFL.ModPrio.AI187 - step_number: 1 - - description: 'Set enabling time window of mode #2' - fcodes: - - direct_operate - optional: I - point_name: DGFL.WinTms.AO125 - response: DGFL.WinTms.AI188 - step_number: 2 - - description: 'Set enabling ramp time of mode #2' - fcodes: - - direct_operate - optional: I - point_name: DGFL.RmpTms.AO126 - response: DGFL.RmpTms.AI189 - step_number: 3 - - description: 'Set reversion timeout period of mode #2' - fcodes: - - direct_operate - optional: I - point_name: DGFL.RvrtTms.AO127 - response: DGFL.RvrtTms.AI190 - step_number: 4 - - description: 'Identify the meter used to measure the Reference Power Input of - mode #2. By default this is the System Meter (ID = 0)' - fcodes: - - direct_operate - optional: I - point_name: DGFL.EcpRef.AO128 - response: DGFL.EcpRef.AI191 - step_number: 5 - - description: 'Set the power threshold for activating mode #2' - fcodes: - - direct_operate - optional: M - point_name: DGFL.PkPwrWLim.AO129 - response: DGFL.PkPwrWLim.AI193 - step_number: 6 - - description: 'Set the ratio used to calculate the output power from the measured - power of mode #2' - fcodes: - - direct_operate - optional: M - point_name: DGFL.PkPwrFolPct.AO130 - response: DGFL.PkPwrFolPct.AI194 - step_number: 7 - - description: 'Set the maximum ramp up rate of mode #2' - fcodes: - - direct_operate - optional: I - point_name: DGFL.RpuRte.AO131 - response: DGFL.RpuRte.AI195 - step_number: 8 - - description: 'Set the maximum ramp down rate of mode #2' - fcodes: - - direct_operate - optional: I - point_name: DGFL.RpdRte.AO132 - response: DGFL.RpdRte.AI196 - step_number: 9 - - action: publish - description: 'Enable the active response mode #2' - fcodes: - - select - - operate - optional: M - point_name: DGFL.ModEna.BO21 - response: DGFL.ModEna.BI73 - step_number: 10 -- id: enable_active_power_response_mode_3 - name: Enable Active Power Response Mode 3 - ref: AN2018 Spec section 2.6.4 Table 43 - mode_types: - schedule: - - 18 - steps: - - description: 'Set the priority of mode #3' - fcodes: - - direct_operate - optional: I - point_name: DLFL.ModPrio.AO133 - response: DLFL.ModPrio.AI198 - step_number: 1 - - description: 'Set enabling time window of mode #3' - fcodes: - - direct_operate - optional: I - point_name: DLFL.WinTms.AO134 - response: DLFL.WinTms.AI199 - step_number: 2 - - description: 'Set enabling ramp time of mode #3' - fcodes: - - direct_operate - optional: I - point_name: DLFL.RmpTms.AO135 - response: DLFL.RmpTms.AI200 - step_number: 3 - - description: 'Set reversion timeout period of mode #3' - fcodes: - - direct_operate - optional: I - point_name: DLFL.RvrtTms.AO136 - response: DLFL.RvrtTms.AI201 - step_number: 4 - - description: 'Identify the meter used to measure the Reference Power Input of - mode #3. By default this is the System Meter (ID = 0)' - fcodes: - - direct_operate - optional: I - point_name: DLFL.EcpRef.AO137 - response: DLFL.EcpRef.AI202 - step_number: 5 - - description: 'Set the power threshold for activating mode #3' - fcodes: - - direct_operate - optional: M - point_name: DLFL.PkPwrWLim.AO138 - response: DLFL.PkPwrWLim.AI204 - step_number: 6 - - description: 'Set the ratio used to calculate the output power from the measured - power of mode #3' - fcodes: - - direct_operate - optional: M - point_name: DLFL.PkPwrFolPct.AO139 - response: DLFL.PkPwrFolPct.AI205 - step_number: 7 - - description: 'Set the maximum ramp up rate of mode #3' - fcodes: - - direct_operate - optional: I - point_name: DLFL.RpuRte.AO140 - response: DLFL.RpuRte.AI206 - step_number: 8 - - description: 'Set the maximum ramp down rate of mode #3' - fcodes: - - direct_operate - optional: I - point_name: DLFL.RpdRte.AO141 - response: DLFL.RpdRte.AI207 - step_number: 9 - - action: publish - description: 'Enable the active response mode #3' - fcodes: - - select - - operate - optional: M - point_name: DLFL.ModEna.BO22 - response: DLFL.ModEna.BI74 - step_number: 10 -- id: perform_automatic_generation_control_mode - name: Perform Automatic Generation Control Mode - ref: AN2018 Spec section 2.6.5 Table 45 - mode_types: - schedule: - - 19 - steps: - - description: Set priority of the mode - fcodes: - - direct_operate - optional: I - point_name: DAGC.ModPrio.AO142 - response: DAGC.ModPrio.AI209 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DAGC.WinTms.AO143 - response: DAGC.WinTms.AI210 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DAGC.RmpTms.AO144 - response: DAGC.RmpTms.AI211 - step_number: 3 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DAGC.RvrtTms.AO145 - response: DAGC.RvrtTms.AI212 - step_number: 4 - - description: Select whether to use Ramp Rates or Time Constants - fcodes: - - direct_operate - optional: I - point_name: DAGC.UseRmpRte.BO39 - response: DAGC.UseRmpRte.BI91 - step_number: 5 - - description: 'If DAGC.UseRmpRte = 0: Set AGC Ramp Time Constant Up Time' - fcodes: - - direct_operate - optional: O - point_name: DAGC.OpnLoop.AO147 - response: DAGC.RmpUpTms.AI214 - step_number: 6 - - description: 'If DAGC.UseRmpRte = 0: Set AGC Ramp Time Constant Down Time' - fcodes: - - direct_operate - optional: O - point_name: DAGC.OpnLoop.AO148 - response: DAGC.RmpDnTms.AI215 - step_number: 7 - - description: 'If DAGC.UseRmpRte = 1: Set AGC Discharge Ramp Up Rate' - fcodes: - - direct_operate - optional: O - point_name: DAGC.DschRpuRte.AO149 - response: DAGC.RpuRte.AI216 - step_number: 8 - - description: 'If DAGC.UseRmpRte = 1: Set AGC Discharge Ramp Down Rate' - fcodes: - - direct_operate - optional: O - point_name: DAGC.DschRpdRte.AO150 - response: DAGC.RpdRte.AI217 - step_number: 9 - - description: 'If DAGC.UseRmpRte = 1: Set AGC Charge Ramp Up Rate' - fcodes: - - direct_operate - optional: O - point_name: DAGC.ChaRpuRte.AO151 - response: DAGC.RpuChaRte.AI218 - step_number: 10 - - description: 'If DAGC.UseRmpRte = 1: Set AGC Charge Ramp Down Rate' - fcodes: - - direct_operate - optional: O - point_name: DAGC.ChaRpdRte.AO152 - response: DAGC.RpdChaRte.AI219 - step_number: 11 - - description: Set Minimum Usable State of Charge (percent of Usable Capacity Rating) - fcodes: - - direct_operate - optional: I - point_name: DAGC.SocUseMinPct.AO153 - response: DAGC.SocUseMinPct.AI220 - step_number: 12 - - description: Set Maximum Usable State of Charge (percent of Usable Capacity Rating) - fcodes: - - direct_operate - optional: I - point_name: DAGC.SocUseMaxPct.AO154 - response: DAGC.SocUseMaxPct.AI221 - step_number: 13 - - description: Set the Active Power Target (in Watts) Positive is discharging, negative - is charging. - fcodes: - - direct_operate - optional: M - point_name: DAGC.GnWSpt.AO146 - response: DAGC.GnWSpt.AI213 - step_number: 14 - - description: Enable AGC mode and receive response. - fcodes: - - select - - operate - optional: M - point_name: DAGC.ModEna.BO23 - response: DAGC.ModEna.BI75 - step_number: 15 - - action: publish - description: Once the mode is enabled, periodically update the Active Power Target. - fcodes: - - direct_operate - optional: M - point_name: DAGC.GnWSpt.AO146 - response: DAGC.GnWSpt.AI213 - step_number: 16 - - description: Read the predicted State of Charge - fcodes: - - read - optional: O - point_name: n/a - response: DAGC.SocExpc.AI224 - step_number: 17 -- id: enable_active_power_smoothing - name: Enable Active Power Smoothing - ref: AN2018 Spec section 2.6.6 Table 46 - mode_types: - schedule: - - 20 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DWSM.ModPrio.AO155 - response: DWSM.ModPrio.AI227 - step_number: 1 - - description: Set time window - fcodes: - - direct_operate - optional: I - point_name: DWSM.WinTms.AO156 - response: DWSM.WinTms.AI228 - step_number: 2 - - description: Set ramp time - fcodes: - - direct_operate - optional: I - point_name: DWSM.RmpTms.AO157 - response: DWSM.RmpTms.AI229 - step_number: 3 - - description: Set reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DWSM.RvrtTms.AO158 - response: DWSM.RvrtTms.AI230 - step_number: 4 - - description: Identify the meter used to measure the Reference Power Input. By - default this is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DWSM.EcpRef.AO159 - response: DWSM.EcpRef.AI231 - step_number: 5 - - description: Set the Active Power Smoothing Gradient - fcodes: - - direct_operate - optional: I - point_name: DWSM.WSmthGra.AO160 - response: DWSM.WSmthGra.AI233 - step_number: 6 - - description: Set the Active Power Smoothing Lower Limit - fcodes: - - direct_operate - optional: I - point_name: DWSM.WSmthLoLim.AO161 - response: DWSM.WSmthLoLim.AI234 - step_number: 7 - - description: Set the Active Power Smoothing Upper Limit - fcodes: - - direct_operate - optional: I - point_name: DWSM.WSmthHiLim.AO162 - response: DWSM.WSmthHiLim.AI235 - step_number: 8 - - description: Set the Active Power Smoothing Filter Time - fcodes: - - direct_operate - optional: I - point_name: DWSM.FilTms.AO163 - response: DWSM.FilTms.AI236 - step_number: 9 - - description: Set the Discharge Ramp Up Rate - fcodes: - - direct_operate - optional: I - point_name: DWSM.DschRpuRte.AO164 - response: DWSM.RpuRte.AI237 - step_number: 10 - - description: Set the Discharge Ramp Down Rate - fcodes: - - direct_operate - optional: I - point_name: DWSM.DschRpdRte.AO165 - response: DWSM.RpdRte.AI238 - step_number: 11 - - description: Set the Charge Ramp Up Rate - fcodes: - - direct_operate - optional: I - point_name: DWSM.ChaRpuRte.AO166 - response: DWSM.RpuChaRte.AI239 - step_number: 12 - - description: Set the Charge Ramp Down Rate - fcodes: - - direct_operate - optional: I - point_name: DWSM.ChaRpdRte.AO167 - response: DWSM.RpdChaRte.AI240 - step_number: 13 - - action: publish - description: Enable Active Power Smoothing Mode - fcodes: - - select - - operate - optional: M - point_name: DWSM.ModEna.BO24 - response: DWSM.ModEna.BI76 - step_number: 14 -- id: enable_volt-watt_curve - mode_types: - curve: - - 5 - schedule: - - 21 - name: Enable Volt-Watt Curve - ref: AN2018 Spec section 2.6.7 Table 47 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DVWC.ModPrio.AO168 - response: DVWC.ModPrio.AI242 - step_number: 1 - - description: Set time window - fcodes: - - direct_operate - optional: I - point_name: DVWC.WinTms.AO169 - response: DVWC.WinTms.AI243 - step_number: 2 - - description: Set ramp time - fcodes: - - direct_operate - optional: I - point_name: DVWC.RmpTms.AO170 - response: DVWC.RmpTms.AI244 - step_number: 3 - - description: Set reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DVWC.RvrtTms.AO171 - response: DVWC.RvrtTms.AI245 - step_number: 4 - - description: Identify the meter used to measure the Voltage. By default this - is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DVWC.EcpRef.AO172 - response: DVWC.EcpRef.AI246 - step_number: 5 - - description: Set the reference voltage if it has not already been set - fcodes: - - direct_operate - optional: I - point_name: DECP.VRef.AO0 - response: DECP.VRef.AI29 - step_number: 6 - - description: Set the reference voltage offset if it has not already been set - fcodes: - - direct_operate - optional: I - point_name: DECP.VRefOfs.AO1 - response: DECP.VRefOfs.AI30 - step_number: 7 - - description: Identify the index of the curve being used - fcodes: - - direct_operate - func_ref: curve - optional: M - point_name: DVWC.VWCrv.AO173 - response: DVWC.VWCrv.AI248 - step_number: 8 - - action: publish - description: Enable the Volt-Watt Mode - fcodes: - - select - - operate - optional: M - point_name: DVWC.ModEna.BO25 - response: DVWC.ModEna.BI77 - step_number: 9 - - description: Read the maximum active power the outstation will attempt to generate - or absorb based on the voltage and the curve in use. - fcodes: - - read - optional: O - point_name: n/a - response: DVWC.ReqWLim.AI249 - step_number: 10 - - description: Read the actual active power produced or absorbed - fcodes: - - read - optional: O - point_name: n/a - response: MMXU.TotW.AI537 - step_number: 11 -- id: enable_frequency-watt_curve_mode - mode_types: - curve: - - 3 - schedule: - - 22 - - 23 - - 24 - name: Enable Frequency-Watt Curve Mode - ref: AN2018 Spec section 2.6.8 Table 48 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DHFW.ModPrio.AO181 - response: DHFW.ModPrio.AI257 - step_number: 1 - - description: Set time window - fcodes: - - direct_operate - optional: I - point_name: DHFW.WinTms.AO182 - response: DHFW.WinTms.AI258 - step_number: 2 - - description: Set ramp time - fcodes: - - direct_operate - optional: I - point_name: DHFW.RvrtTms.AO184 - response: DHFW.RmpTms.AI259 - step_number: 3 - - description: Set reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DHFW.RmpTms.AO183 - response: DHFW.RvrtTms.AI260 - step_number: 4 - - description: Identify the meter used to measure the Frequency. By default this - is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DHFW.EcpRef.AO185 - response: DHFW.EcpRef.AI261 - step_number: 5 - - description: Set the Nominal Grid Frequency if it is not already set - fcodes: - - direct_operate - optional: I - point_name: DECP.EcpNomHz.AO2 - response: DECP.EcpNomHz.AI31 - step_number: 6 - - description: Set the starting delays - fcodes: - - direct_operate - optional: I - point_name: DHFW.ActStrDlTmms.AO189 - response: DHFW.ActStrDlTmms.AI266 - step_number: 7 - - description: Set the stopping delays - fcodes: - - direct_operate - optional: I - point_name: DHFW.ActStopDlTmms.AO190 - response: DHFW.ActStopDlTmms.AI267 - step_number: 8 - - description: Set the frequency-watt curve ramp up time constant for the output - fcodes: - - direct_operate - optional: I - point_name: DHFW.OpnLoopMax.AO191 - response: DHFW.OpnLoopMax.AI268 - step_number: 9 - - description: Set the frequency-watt curve ramp down time constant for the output - fcodes: - - direct_operate - optional: I - point_name: DHFW.OpnLoopMax.AO192 - response: DHFW.OpnLoopMax.AI269 - step_number: 10 - - description: Set the frequency-watt curve discharge ramp up rate - fcodes: - - direct_operate - optional: I - point_name: DHFW.RpuRte.AO193 - response: DHFW.RpuRte.AI270 - step_number: 11 - - description: Set the frequency-watt curve discharge ramp down rate - fcodes: - - direct_operate - optional: I - point_name: DHFW.RpdRte.AO194 - response: DHFW.RpdRte.AI271 - step_number: 12 - - description: Set the frequency-watt curve charge ramp up rate - fcodes: - - direct_operate - optional: I - point_name: DHFW.RpuChaRte.AO195 - response: DHFW.RpuChaRte.AI272 - step_number: 13 - - description: Set the frequency-watt curve charge ramp down rate - fcodes: - - direct_operate - optional: I - point_name: DHFW.RpdChaRte.AO196 - response: DHFW.RpdChaRte.AI273 - step_number: 14 - - description: Identify the index of the main curve being used - fcodes: - - direct_operate - func_ref: curve - optional: I - point_name: DHFW.HzWCrv.AO186 - response: DHFW.HzWCrv.AI263 - step_number: 15 - - description: Identify the index of the high frequency hyteresis curve being used - fcodes: - - direct_operate - func_ref: curve - optional: I - point_name: DHFW.HysCrv.AO187 - response: DHFW.HysCrv.AI264 - step_number: 16 - - description: Identify the index of the low frequency hyteresis curve being used - fcodes: - - direct_operate - func_ref: curve - optional: I - point_name: DLFW.HysCrv.AO188 - response: DLFW.HysCrv.AI265 - step_number: 17 - - description: Set the minimum state of charge in which this mode shall operate - fcodes: - - direct_operate - optional: I - point_name: DHFW.SocUseMinPct.AO197 - response: DHFW.SocUseMinPct.AI275 - step_number: 18 - - description: Set the maximum state of charge in which this mode shall operate - fcodes: - - direct_operate - optional: I - point_name: DHFW.SocUseMaxPct.AO198 - response: DHFW.SocUseMaxPct.AI276 - step_number: 19 - - description: Choose whether to use hysteresis - fcodes: - - direct_operate - optional: I - point_name: DLFW.HysEna.BO36 - response: DLFW.HysEna.BI88 - step_number: 20 - - description: Choose whether to snapshot power - fcodes: - - direct_operate - optional: I - point_name: DLFW.SnptEna.BO37 - response: DLFW.SnptEna.BI89 - step_number: 21 - - action: publish - description: Enable the Frequency-Watt Curve Mode - fcodes: - - select - - operate - optional: M - point_name: DHFW.ModEna.BO26 - response: DHFW.ModEna.BI78 - step_number: 22 -- id: set_constant_var_output - name: Set Constant Var Output - ref: AN2018 Spec section 2.7.1 Table 50 - mode_types: - schedule: - - 25 - steps: - - description: Set the priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DVAR.ModPrio.AO199 - response: DVAR.ModPrio.AI277 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DVAR.WinTms.AO200 - response: DVAR.WinTms.AI278 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DVAR.RmpTms.AO201 - response: DVAR.RmpTms.AI279 - step_number: 3 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DVAR.RvrtTms.AO202 - response: DVAR.RvrtTms.AI280 - step_number: 4 - - description: Set the ramp up Time Constants - fcodes: - - direct_operate - optional: I - point_name: DVAR.OpnLoopMax.AO204 - response: DVAR.OpnLoopMax.AI282 - step_number: 5 - - description: Set the ramp down Time Constants - fcodes: - - direct_operate - optional: I - point_name: DVAR.OpnLoopMax.AO205 - response: DVAR.OpnLoopMax.AI283 - step_number: 6 - - description: Choose whether the time constants represent 3-Tau limits or Open - Loop Response Times - fcodes: - - direct_operate - optional: I - point_name: DSTO.OpnLoopTau.BO9 - response: DSTO.OpnLoopTau.BI28 - step_number: 7 - - description: If Open Loop Response Times are selected, choose the percentage of - final output represented by the time constant (e.g. 90% or 95%) - fcodes: - - direct_operate - optional: I - point_name: DSTO.OpnLoopPct.AO3 - response: DGEN.OpnLoopPct.AI40 - step_number: 8 - - description: Select the meaning of the Constant VArs Reactive Power Target - fcodes: - - direct_operate - optional: I - point_name: DSTO.VArRef.AO5 - response: DGEN.VArSetRef.AI42 - step_number: 9 - - description: 'If DSTO.VArRef = 1: Read percent of maximum active generation power' - fcodes: - - read - - response - optional: O - point_name: n/a - response: DGEN.WMax.AI32 - step_number: 10 - - description: 'If DSTO.VArRef = 1: Read percent of maximum active charging power' - fcodes: - - read - - response - optional: O - point_name: n/a - response: DSTO.ChaWMax.AI33 - step_number: 11 - - description: 'If DSTO.VArRef = 2: Read percent of maximum reactive injection power' - fcodes: - - read - - response - optional: O - point_name: n/a - response: DGEN.IvarMax.AI34 - step_number: 12 - - description: 'If DSTO.VArRef = 2: Read percent of maximum reactive absorption - power' - fcodes: - - read - - response - optional: O - point_name: n/a - response: DGEN.AvarMax.AI35 - step_number: 13 - - description: 'If DSTO.VArRef = 3: Read percent of system reactive injection power' - fcodes: - - read - - response - optional: O - point_name: n/a - response: DGEN.AvarAvl.AI45 - step_number: 14 - - description: 'If DSTO.VArRef = 3: Read percent of system reactive absorption power' - fcodes: - - read - - response - optional: O - point_name: n/a - response: DGEN.IvarAvl.AI46 - step_number: 15 - - description: Set the Constant VArs Reactive Power Target in percent - fcodes: - - direct_operate - optional: M - point_name: DVAR.VArTgtPct.AO203 - response: DVAR.VArTgtPct.AI281 - step_number: 16 - - action: publish - description: Enable Constant VArs mode and receive response - fcodes: - - select - - operate - optional: M - point_name: DVAR.ModEna.BO27 - response: DVAR.ModEna.BI79 - step_number: 17 -- id: set_a_fixed_power_factor - name: Set a Fixed Power Factor - ref: AN2018 Spec section 2.7.2 Table 52 - mode_types: - schedule: - - 26 - steps: - - description: Set the priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DFPF.ModPrio.AO206 - response: DFPF.ModPrio.AI284 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DFPF.WinTms.AO207 - response: DFPF.WinTms.AI285 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DFPF.RmpTms.AO208 - response: DFPF.RmpTms.AI286 - step_number: 3 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DFPF.RvrtTms.AO209 - response: DFPF.RvrtTms.AI287 - step_number: 4 - - description: Set the requirement for whether to inject or absorb VARs (PFExt) - when discharging / generating - fcodes: - - direct_operate - optional: I - point_name: DFPF.PFGnExtSet.BO10 - response: DFPF.PFGnExtSet.BI29 - step_number: 5 - - description: Set the requirement for whether to inject or absorb VARs (PFExt) - when charging - fcodes: - - direct_operate - optional: I - point_name: DFPF.PFLodExtSet.BO11 - response: DFPF.PFLodExtSet.BI30 - step_number: 6 - - description: Set Fixed Power Factor Setpoint to use when generating / discharging - fcodes: - - direct_operate - optional: M - point_name: DFPF.PFGnTgt.AO210 - response: DFPF.PFGnTgt.AI288 - step_number: 7 - - description: Set Fixed Power Factor Setpoint to use when charging - fcodes: - - direct_operate - optional: M - point_name: DFPF.PFLodTgt.AO211 - response: DFPF.PFLodTgt.AI289 - step_number: 8 - - action: publish - description: Enable fixed power factor mode and receive response - fcodes: - - select - - operate - optional: M - point_name: DFPF.ModEna.BO28 - response: DFPF.BI47 - step_number: 9 -- id: change_and_select_volt-var_control_mode - mode_types: - curve: - - 2 - schedule: - - 27 - name: Change and Select Volt-Var Control Mode - ref: AN2018 Spec section 2.7.3 Table 54 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DVVR.ModPrio.AO212 - response: DVVR.ModPrio.AI290 - step_number: 1 - - description: Set the enabling time window - fcodes: - - direct_operate - optional: I - point_name: DVVR.WinTms.AO213 - response: DVVR.WinTms.AI291 - step_number: 2 - - description: Set the enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DVVR.RmpTms.AO214 - response: DVVR.RmpTms.AI292 - step_number: 3 - - description: Set the enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DVVR.RvrtTms.AO215 - response: DVVR.RvrtTms.AI293 - step_number: 4 - - description: Identify the meter used to measure the voltage. By default this - is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DVVR.EcpRef.AO216 - response: DVVR.EcpRef.AI294 - step_number: 5 - - description: If using a fixed Voltage reference, set the reference voltage if - it has not already been set - fcodes: - - direct_operate - optional: O - point_name: DECP.VRef.AO0 - response: DECP.VRef.AI29 - step_number: 6 - - description: If using a fixed Voltage reference, set the reference voltage offset - if it has not already been set - fcodes: - - direct_operate - optional: O - point_name: DECP.VRefOfs.AO1 - response: DECP.VRefOfs.AI30 - step_number: 7 - - description: If autonomously adjusting the Voltage reference, set the time constant - for the lowpass filter - fcodes: - - direct_operate - optional: O - point_name: DVVR.VRefTmms.AO220 - response: DVVR.VRefTmms.AI300 - step_number: 8 - - description: If autonomously adjusting the Voltage reference, enable autonomous - adjustment - fcodes: - - direct_operate - optional: O - point_name: DVVR.VRefAdjEna.BO41 - response: DVVR.VRefAdjEna.BI93 - step_number: 9 - - description: Set the ramp up time constant for the output of the curve - fcodes: - - direct_operate - optional: I - point_name: DVVR.OpnLoopMax.AO218 - response: DVVR.OpnLoopMax.AI298 - step_number: 10 - - description: Set the ramp down time constant for the output of the curve - fcodes: - - direct_operate - optional: I - point_name: DVVR.OpnLoopMax.AO219 - response: DVVR.OpnLoopMax.AI299 - step_number: 11 - - description: Identify the index of the curve being used - fcodes: - - direct_operate - func_ref: curve - optional: M - point_name: DVVR.VVArCrv.AO217 - response: DVVR.VVArCrv.AI297 - step_number: 12 - - action: publish - description: Enable the Volt-VAr Control Mode - fcodes: - - select - - operate - optional: M - point_name: DVVC.ModEna.BO29 - response: DVVC.BI48 - step_number: 13 - - description: Read the adjusted reference voltage, if it is not fixed - fcodes: - - read - optional: O - point_name: n/a - response: DVVR.VRefSet.AI296 - step_number: 14 - - description: Read the measured Voltage - fcodes: - - read - optional: O - point_name: n/a - response: MMXN.Vol.AI295 - step_number: 15 - - description: Read the attempted VArs - fcodes: - - read - optional: O - point_name: n/a - response: DVVR.ReqVAr.AI301 - step_number: 16 - - description: Read the actual VArs (if using system meter) - fcodes: - - read - optional: O - point_name: n/a - response: MMXU.TotVAr.AI541 - step_number: 17 -- id: enable_watt-var_power_mode - mode_types: - curve: - - 4 - schedule: - - 28 - name: Enable Watt-Var Power Mode - ref: AN2018 Spec section 2.7.4 Table 55 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DWVR.ModPrio.AO221 - response: DWVR.ModPrio.AI302 - step_number: 1 - - description: Set the enabling time window - fcodes: - - direct_operate - optional: I - point_name: DWVR.WinTms.AO222 - response: DWVR.WinTms.AI303 - step_number: 2 - - description: Set the enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DWVR.RmpTms.AO223 - response: DWVR.RmpTms.AI304 - step_number: 3 - - description: Set the enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DWVR.RvrtTms.AO224 - response: DWVR.RvrtTms.AI305 - step_number: 4 - - description: Identify the meter used to measure the active power. By default - this is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DWVR.EcpRef.AO225 - response: DWVR.EcpRef.AI306 - step_number: 5 - - description: Read the maximum generation power used as the reference for percent - Watts - fcodes: - - read - - response - optional: O - point_name: n/a - response: DGEN.WMax.AI32 - step_number: 6 - - description: Read the maximum charging power used as the reference for percent - Watts - fcodes: - - read - - response - optional: O - point_name: n/a - response: DSTO.ChaWMax.AI33 - step_number: 7 - - description: Set the ramp up time constant for the output of the curve - fcodes: - - direct_operate - optional: I - point_name: DWVR.OpnLoopMax.AO227 - response: DWVR.OpnLoopMax.AI309 - step_number: 8 - - description: Set the ramp down time constant for the output of the curve - fcodes: - - direct_operate - optional: I - point_name: DWVR.OpnLoopMax.AO228 - response: DWVR.OpnLoopMax.AI310 - step_number: 9 - - description: Identify the index of the curve being used - fcodes: - - direct_operate - func_ref: curve - optional: M - point_name: DWVR.WVArCrv.AO226 - response: DWVR.WVArCrv.AI308 - step_number: 10 - - action: publish - description: Enable the Watt-VAr Power Mode - fcodes: - - select - - operate - optional: M - point_name: DWVR.ModEna.BO30 - response: DWVR.BI49 - step_number: 11 -- id: enable_power_factor_correction_mode - name: Enable Power Factor Correction Mode - ref: AN2018 Spec section 2.7.5 Table 56 - mode_types: - schedule: - - 29 - steps: - - description: Set the priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DPFC.ModPrio.AO229 - response: DPFC.ModPrio.AI312 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DPFC.WinTms.AO230 - response: DPFC.WinTms.AI313 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DPFC.RmpRte.AO231 - response: DPFC.RmpTms.AI314 - step_number: 3 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DPFC.RvrtTms.AO232 - response: DPFC.RvrtTms.AI315 - step_number: 4 - - description: Identify the meter used to measure the active power. By default - this is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DPFC.EcpRef.AO233 - response: DPFC.EcpRef.AI316 - step_number: 5 - - description: Set the requirement for whether to inject or absorb VARs (PFExt) - when discharging - fcodes: - - direct_operate - optional: I - point_name: DFPF.PFGnExtSet.BO10 - response: DFPF.PFGnExtSet.BI29 - step_number: 6 - - description: Set the requirement for whether to inject or absorb VARs (PFExt) - when charging - fcodes: - - direct_operate - optional: I - point_name: DFPF.PFLodExtSet.BO11 - response: DFPF.PFLodExtSet.BI30 - step_number: 7 - - description: Set the Average PF Target - fcodes: - - direct_operate - optional: M - point_name: DPFC.PFTrg.AO234 - response: MMXU.TotPF.AI317 - step_number: 8 - - description: Set the Lower PF Limit - fcodes: - - direct_operate - optional: M - point_name: DPFC.PFCorRef.rangeC.AO235 - response: DPFC.PFTrg.AI318 - step_number: 9 - - description: Set the Upper PF Limit - fcodes: - - direct_operate - optional: M - point_name: DPFC.PFCorRef.rangeC.AO236 - response: DPFC.PFCorRef.rangeC.AI319 - step_number: 10 - - action: publish - description: Enable Power Factor Correction Mode - fcodes: - - select - - operate - optional: M - point_name: DPFC.ModEna.BO31 - response: DPFC.ModEna.BI83 - step_number: 11 -- id: signal_a_price_change - name: Signal a Price Change - ref: AN2018 Spec section 2.8 Table 57 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DPRG.ModPrio.AO237 - response: DPRG.ModPrio.AI321 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DPRG.WinTms.AO238 - response: DPRG.WinTms.AI322 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DPRG.RmpTms.AO239 - response: DPRG.RmpTms.AI323 - step_number: 3 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DPRG.RvrtTms.AO240 - response: DPRG.RvrtTms.AI324 - step_number: 4 - - description: Set pricing mode time constant for ramping up - fcodes: - - direct_operate - optional: I - point_name: DPRG.OpnLoopMax.AO242 - response: DPRG.OpnLoopMax.AI326 - step_number: 5 - - description: Set pricing mode time constant for ramping down - fcodes: - - direct_operate - optional: I - point_name: DPRG.OpnLoopMax.AO243 - response: DPRG.OpnLoopMax.AI327 - step_number: 6 - - description: Set pricing signal and receive response - fcodes: - - select - - operate - optional: M - point_name: DPRG.PrcRef.AO241 - response: DPRG.PrcRef.AI325 - step_number: 7 - - action: publish - description: Enable pricing signal mode and receive response - fcodes: - - select - - operate - optional: M - point_name: DPRG.ModEna.BO32 - response: DPRG.ModEna.BI84 - step_number: 8 -- id: enable_schedules - name: Enable Schedules - ref: AN2018 Spec section 2.9 Table 59 - steps: - - description: Enable the Schedule by changing its state to ready - fcodes: - - select - - operate - optional: M - point_name: FSCHxx.Mod.BO42 - response: FSCH.SchdSt.3.BI108 - step_number: 1 - - description: Select shedule index - fcodes: - - direct_operate - func_ref: schedule - optional: M - point_name: FSCC.Schd.AO461 - response: FSCC.Schd.AI570 - step_number: 2 - - description: Check that outstation validates the selected schedule - fcodes: - - read - optional: O - point_name: n/a - response: FSCH.SchdSt.2.BI109 - step_number: 3 - - description: Set selected schedule repeat weekly Sunday - fcodes: - - select - - operate - optional: I - point_name: FSCHxx.SchdReuse.BO43 - response: FSCHxx.SchdReuse.BI110 - step_number: 4 - - description: Set selected schedule repeat weekly Monday - fcodes: - - select - - operate - optional: I - point_name: FSCHxx.SchdReuse.BO44 - response: FSCHxx.SchdReuse.BI111 - step_number: 5 - - description: Set selected schedule repeat weekly Tuesday - fcodes: - - select - - operate - optional: I - point_name: FSCHxx.SchdReuse.BO45 - response: FSCHxx.SchdReuse.BI112 - step_number: 6 - - description: Set selected schedule repeat weekly Wednesday - fcodes: - - select - - operate - optional: I - point_name: FSCHxx.SchdReuse.BO46 - response: FSCHxx.SchdReuse.BI113 - step_number: 7 - - description: Set selected schedule repeat weekly Thursday - fcodes: - - select - - operate - optional: I - point_name: FSCHxx.SchdReuse.BO47 - response: FSCHxx.SchdReuse.BI114 - step_number: 8 - - description: Set selected schedule repeat weekly Friday - fcodes: - - select - - operate - optional: I - point_name: FSCHxx.SchdReuse.BO48 - response: FSCHxx.SchdReuse.BI115 - step_number: 9 - - action: publish - description: Set selected schedule repeat weekly Saturday - fcodes: - - select - - operate - optional: I - point_name: FSCHxx.SchdReuse.BO49 - response: FSCHxx.SchdReuse.BI116 - step_number: 10 - - description: Be notified when the schedule is running - fcodes: - - read - optional: O - point_name: n/a - response: FSCH1.SchdSt.AI579 - step_number: 11 -- id: curve - name: Curve - ref: AN2018 Spec Curve Definition - steps: - - description: Select which curve to edit - fcodes: - - direct_operate - optional: M - point_name: DGSMn.InCrv.AO244 - response: DGSMn.InCrv.AI328 - step_number: 1 - - description: Specify the Curve Mode Type - fcodes: - - direct_operate - optional: M - point_name: DGSMn.ModTyp.AO245 - response: DGSMn.ModTyp.AI329 - step_number: 2 - - description: Specify that the Independent (X-Value) units for the curve - fcodes: - - direct_operate - optional: M - point_name: FMARn.IndpUnits.AO247 - response: FMARn.IndpUnits.AI331 - step_number: 3 - - description: Specify the Dependent (Y-Value) units for the curve - fcodes: - - direct_operate - optional: M - point_name: FMARn.DepRef.AO248 - response: FMARn.DepRef.AI332 - step_number: 4 - - description: Set X-Value and Y-Values pairs for the curve - fcodes: - - direct_operate - optional: M - point_name: FMARn.PairArr.CrvPts.AO249 - response: FMARn.PairArr.CrvPts.AI333 - step_number: 5 - - action: publish - description: Set number of points used for the curve - fcodes: - - direct_operate - optional: M - point_name: FMARn.PairArr.NumPts.AO246 - response: FMARn.PairArr.NumPts.AI330 - step_number: 6 -- id: schedule - name: Schedule - ref: AN2018 Spec Schedule Definition - steps: - - description: Select which schedule to edit. This is the index of the schedule, - not its identity. The indexes shall be the monotonically increasing integers - 12, 13, 14 .etc. while the curve identities may be any unique number. - fcodes: - - direct_operate - optional: M - point_name: FSCC.Schd.AO461 - response: FSCC.Schd.AI570 - step_number: 1 - - description: Set the identity of the schedule to a unique number - fcodes: - - direct_operate - optional: M - point_name: FSCC.Schd.AO462 - response: FSCC.Schd.AI571 - step_number: 2 - - description: Set the priority for the schedule - fcodes: - - direct_operate - optional: M - point_name: FSCH1.SchdPrio.AO463 - response: FSCH.SchdPrio.AI572 - step_number: 3 - - description: Set the meaning of the Y-values of the schedule, i.e. the schedule - type. Refer to Table 58. - fcodes: - - direct_operate - optional: M - point_name: FSCH.SchdVal.valEq.AO464 - response: AI573 - step_number: 4 - - description: Set the date for the schedule to start - fcodes: - - direct_operate - optional: M - point_name: FSCH.StrTm.AO465 - response: FSCH.StrTm.AI574 - step_number: 5 - - description: Set the time for the schedule to start - fcodes: - - direct_operate - optional: M - point_name: FSCH.StrTm.AO466 - response: FSCH.StrTm.AI575 - step_number: 6 - - description: Set the repetition interval - fcodes: - - direct_operate - optional: M - point_name: FSCH.NxtStrTm.AO467 - response: FSCH.NxtStrTm.AI576 - step_number: 7 - - description: Set the units of the repetition interval - fcodes: - - direct_operate - optional: M - point_name: FSCH.SchdReuse.AO468 - response: FSCH.SchdReuse.AI577 - step_number: 8 - - description: Set the Time Offset (X-Values) and Schedule Value (Y-Values) for - each schedule point - fcodes: - - direct_operate - optional: M - point_name: FSCHn.SchdEntr.AO470 - response: FSCHn.SchdEntr.AI581 - step_number: 9 - - action: publish - description: Set the number of points used for the schedule. - fcodes: - - direct_operate - optional: M - point_name: FSCH.NumEntr.AO469 - response: FSCH.NumEntr.AI580 - step_number: 10 \ No newline at end of file diff --git a/services/core/DNP3Agent/dnp3/mesa_points.config b/services/core/DNP3Agent/dnp3/mesa_points.config deleted file mode 100644 index a5561e411d..0000000000 --- a/services/core/DNP3Agent/dnp3/mesa_points.config +++ /dev/null @@ -1,10270 +0,0 @@ -[ - { - "index": 0, - "description": "Reference Voltage", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DECP", - "units": "Volts", - "minimum": 0, - "data_object": "VRef", - "name": "DECP.VRef.AO0" - }, - { - "index": 1, - "description": "Reference Voltage Offset", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DECP", - "units": "Volts", - "data_object": "VRefOfs", - "name": "DECP.VRefOfs.AO1" - }, - { - "index": 2, - "description": "Nominal Grid Frequency", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DECP", - "units": "Hz", - "minimum": 0, - "data_object": "EcpNomHz", - "name": "DECP.EcpNomHz.AO2" - }, - { - "index": 3, - "description": "Open Loop Response Time Percentage. Percent of target to reach within the open loop response time. Default is 90%.", - "data_type": "AO", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DSTO", - "units": "Percent", - "minimum": 0, - "data_object": "OpnLoopPct", - "name": "DSTO.OpnLoopPct.AO3" - }, - { - "index": 4, - "description": "Power Factor Sign convention", - "data_type": "AO", - "common_data_class": "ENG", - "maximum": 2, - "ln_class": "MMXU", - "units": "None", - "minimum": 1, - "data_object": "PFSign", - "allowed_values": { - "1": "IEC active power", - "2": "IEEE lead/lag" - }, - "type": "enumerated", - "name": "MMXU.PFSign.AO4" - }, - { - "index": 5, - "description": "Reference for Reactive Power Setpoints. Selects which setpoint is active. Default is <3>.", - "data_type": "AO", - "common_data_class": "ENG", - "maximum": 3, - "ln_class": "DSTO", - "units": "None (list)", - "minimum": 0, - "data_object": "VArRef", - "allowed_values": { - "0": "Not applicable / Unknown", - "1": "Percent of Maximum Active Power (WMax)", - "2": "Percent of Maximum Reactive Power (VArMax)", - "3": "Percent of Available Reactive Power (VArAval)" - }, - "type": "enumerated", - "name": "DSTO.VArRef.AO5" - }, - { - "index": 6, - "description": "DER Start (Return to Service) Voltage High Limit. Percent of Reference Voltage.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 20000, - "ln_class": "DCTE", - "units": "Percent", - "minimum": 0, - "data_object": "VHiLim", - "name": "DCTE.VHiLim.AO6" - }, - { - "index": 7, - "description": "DER Start (Return to Service) Voltage Low Limit. Percent of Reference Voltage.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 10000, - "ln_class": "DCTE", - "units": "Percent", - "minimum": 0, - "data_object": "VLoLim", - "name": "DCTE.VLoLim.AO7" - }, - { - "index": 8, - "description": "DER Start (Return to Service) Frequency High Limit", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DCTE", - "units": "Hz", - "minimum": 0, - "data_object": "HzHiLim", - "name": "DCTE.HzHiLim.AO8" - }, - { - "index": 9, - "description": "DER Start (Return to Service) Frequency Low Limit", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DCTE", - "units": "Hz", - "minimum": 0, - "data_object": "HzLoLim", - "name": "DCTE.HzLoLim.AO9" - }, - { - "index": 10, - "description": "DER Start (Return to Service) Delay", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RtnDlyTmms", - "name": "DCTE.RtnDlyTmms.AO10" - }, - { - "index": 11, - "description": "DER Start (Return to Service) Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DCTE.WinTms.AO11" - }, - { - "index": 12, - "description": "DER Start (Return to Service) Ramp Up Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RtnRmpTmms", - "name": "DCTE.RtnRmpTmms.AO12" - }, - { - "index": 13, - "description": "DER Stop (Cease to Energize) Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DCTE.WinTms.AO13" - }, - { - "index": 14, - "description": "DER Stop (Cease to Energize) Ramp Down Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DCTE.RmpTms.AO14" - }, - { - "index": 15, - "description": "DER Stop (Cease to Energize) Reversion Timeout Period. Time to revert from the stopped state and return to service.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DCTE.RvrtTms.AO15" - }, - { - "index": 16, - "description": "Connect/Disconnect Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DCTE.WinTms.AO16" - }, - { - "index": 17, - "description": "Connect/Disconnect Reversion Timeout Period. Timeout (reversion time is for the Disconnect only).", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DCTE.RvrtTms.AO17" - }, - { - "index": 18, - "description": "Requested Settings Group", - "data_type": "AO", - "common_data_class": "ENG", - "maximum": 255, - "ln_class": "DECP", - "units": "None (list)", - "minimum": 0, - "data_object": "EcpIsldSt", - "allowed_values": { - "0": "Not Used", - "1": "Unspecified / Autonomously Determined (see BO Enable Sensed Grid Config Detection)", - "2": "Factory Configuration", - "3": "Default Configuration / Comms Lost", - "4": "Normal Grid-Connected Configuration", - "5": "Islanded Condition 1 (small, local island)", - "6": "Islanded Condition 2 (larger, area island)", - "7": "Islanded Condition 3 (largest, regional island)", - "8": "1st Alternate Grid-Connected Configuration", - "9": "2nd Alternate Grid-Connected Configuration", - "10": "3rd Alternate Grid-Connected Configuration" - }, - "type": "enumerated", - "name": "DECP1.EcpIsldSt.AO18" - }, - { - "index": 19, - "description": "Settings Group Being Edited", - "data_type": "AO", - "common_data_class": "ENG", - "maximum": 255, - "ln_class": "DRCC", - "units": "None (list)", - "minimum": 0, - "data_object": "EcpIsldSt", - "name": "DRCC1.EcpIsldSt.AO19" - }, - { - "index": 20, - "description": "Freeze Counter Interval. Interval between freeze counter operations after the initial occurrence. A zero value means the free counter operation is not repeated.", - "data_type": "AO", - "minimum": 0, - "name": "AO20" - }, - { - "index": 21, - "description": "Freeze Counter Interval Units. Units of the interval between freeze counter operations.", - "data_type": "AO", - "maximum": 9, - "minimum": 0, - "units": "None (list)", - "allowed_values": { - "0": "The outstation does not repeat the action, regardless of the Interval count.", - "1": "Milliseconds - In this case the interval is always counted relative to the Start Time and is constant regardless of the clock time set at the Outstation.", - "2": "Seconds - At the same millisecond within the second that is specified in the Start Time.", - "3": "Minutes - At the same second and millisecond within the minute that is specified in the Start Time.", - "4": "Hours - At the same minute,second and B7millisecond within the hour that is specified in the Start Time.", - "5": "Days - At the same time of day that is specified in the Start Time.", - "6": "Weeks - On the same day of the week at the same time of day that is specified in the Start Time", - "7": "Months - On the same day of each month at the same time of day that is specified in the Start Time. If the Start Time falls on the 29th or greater day of the month, the outstation shall not perform the action in months that do not have such a day.", - "8": "Months on Same Day of Week from Start of Month - At the same time of the day on the same day of the week after the beginning of the month as the day specified in the Start Time. For instance, if the Start Time specifies the second Tuesday of February and the Interval Count is 2, the next action shall occur on the second Tuesday of April. In the same example, if the Interval Count is set to 12, this is the same as specifying, Every year on the second Tuesday in February. If the specified day does not occur in a given month when an action was scheduled to occur, the outstation shall not perform the action that month but shall perform it at the next valid scheduled time.", - "9": "Months on Same Day of Week from End of Month - The outstation shall interpret this setting as in <8>, but the day of the week shall be measured from the end of the month, e.g., the second-last Tuesday in February." - }, - "type": "enumerated", - "name": "AO21" - }, - { - "index": 22, - "description": "Low/High Voltage Ride-Through Signal Meter ID. Referenced ECP. This is the meter from which current is being read to evaluate and provide support.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DHVT", - "minimum": 0, - "data_object": "EcpRef", - "name": "DHVT.EcpRef.AO22" - }, - { - "index": 23, - "description": "Low/High Voltage Ride-Through High Must Trip Curve Index. Index of the Voltage Ride-through curve which specifies trip points when the voltage is high.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "PTOV", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTOV.BlkRef.AO23" - }, - { - "index": 24, - "description": "Low/High Voltage Ride-Through Low Must Trip Curve Index. Index of the Voltage Ride-through curve which specifies trip points when the voltage is low.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "PTUV", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTUV.BlkRef.AO24" - }, - { - "index": 25, - "description": "Low/High Voltage Ride-Through High Momentary Cessation Curve Index. Index of the Voltage Ride-through curve which specifies where generation/discharging must stop when the voltage is high.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "PTOV", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTOV.BlkRef.AO25" - }, - { - "index": 26, - "description": "Low/High Voltage Ride-Through Low Momentary Cessation Curve Index. Index of the Voltage Ride-through curve which specifies where generation/discharging must stop when the voltage is low.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "PTUV", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTUV.BlkRef.AO26" - }, - { - "index": 27, - "description": "Low/High Frequency Ride-Through Signal Meter ID. Referenced ECP. This is the meter from which current is being read to evaluate and provide support.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DHFT", - "minimum": 0, - "data_object": "EcpRef", - "name": "DHFT.EcpRef.AO27" - }, - { - "index": 28, - "description": "Low/High Frequency Ride-Through High Must Trip Curve Index. Index of the Frequency Ride-through curve which specifies trip points when the frequency is high.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "PTOF", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTOF.BlkRef.AO28" - }, - { - "index": 29, - "description": "Low/High Frequency Ride-Through Low Must Trip Curve Index. Index of the Frequency Ride-through curve which specifies trip points when the frequency is low.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "PTUF", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTUF.BlkRef.AO29" - }, - { - "index": 30, - "description": "Low/High Frequency Ride-Through High Momentary Cessation Curve Index. Index of the Frequency Ride-through curve which specifies where generation/discharging must stop when the frequency is high.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "PTOF", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTOF.BlkRef.AO30" - }, - { - "index": 31, - "description": "Low/High Frequency Ride-Through Low Momentary Cessation Curve Index. Index of the Frequency Ride-through curve which specifies where generation/discharging must stop when the frequency is low.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "PTUF", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTUF.BlkRef.AO31" - }, - { - "index": 32, - "description": "Dynamic Reactive Current Support Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DRGS", - "minimum": 0, - "data_object": "ModPrio", - "name": "DRGS.ModPrio.AO32" - }, - { - "index": 33, - "description": "Dynamic Reactive Current Support Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DRGS.WinTms.AO33" - }, - { - "index": 34, - "description": "Dynamic Reactive Current Support Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DRGS.RmpTms.AO34" - }, - { - "index": 35, - "description": "Dynamic Reactive Current Support Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DRGS.RvrtTms.AO35" - }, - { - "index": 36, - "description": "Dynamic Reactive Current Support Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DRGS", - "minimum": 0, - "data_object": "EcpRef", - "name": "DRGS.EcpRef.AO36" - }, - { - "index": 37, - "description": "Dynamic Reactive Current Support - Gradient Mode.", - "data_type": "AO", - "common_data_class": "ENG", - "maximum": 2, - "ln_class": "DRGS", - "units": "None (list)", - "minimum": 0, - "data_object": "ArGraMod", - "allowed_values": { - "0": "Undefined", - "1": "Gradients reach 0 at the moving average Voltage", - "2": "Gradients reach 0 at the Voltage deadbands" - }, - "type": "enumerated", - "name": "DRGS.ArGraMod.AO37" - }, - { - "index": 38, - "description": "Dynamic Reactive Current Support Deadband Minimum Voltage. Percentage of the nominal voltage (DRCT.Vref), measured from the moving average voltage (RDGS.VAv). Support is no longer applied when the voltage stays above this value for the length of the Hold Time.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 0, - "ln_class": "DRGS", - "units": "Percent", - "minimum": -10000, - "data_object": "DbVMin", - "name": "DRGS.DbVMin.AO38" - }, - { - "index": 39, - "description": "Dynamic Reactive Current Support Deadband Maximum Voltage. Percentage of the nominal voltage (DRCT.Vref), measured from the moving average voltage (RDGS.VAv). Support is no longer applied when the voltage stays below this value for the length of the Hold Time.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 10000, - "ln_class": "DRGS", - "units": "Percent", - "minimum": 0, - "data_object": "DbVMax", - "name": "DRGS.DbVMax.AO39" - }, - { - "index": 40, - "description": "Dynamic Reactive Current Support Gradient for Sags. Percentage of the rated current (DRAT.ARtg) to apply capacitively per percentage of the negative deviation from the moving average voltage (RDGS.Av). It is a ratio of percent and is therefore unitless.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DRGS", - "units": "Percent current per percent voltage deviation", - "data_object": "ArGraSag", - "name": "DRGS.ArGraSag.AO40" - }, - { - "index": 41, - "description": "Dynamic Reactive Current Support Gradient for Swells. Percentage of the rated current (DRAT.ARtg) to apply inductively per percentage of the positive deviation from the moving average voltage (RDGS.Av). It is a ratio of percent and is therefore unitless.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DRGS", - "units": "Percent current per percent voltage deviation", - "data_object": "ArGraSwl", - "name": "DRGS.ArGraSwl.AO41" - }, - { - "index": 42, - "description": "Dynamic Reactive Current Support Filter Time for Moving Average Voltage (RDGS.VAv). Used to determine amount of dynamic reactive current support.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "Seconds", - "minimum": 0, - "data_object": "FilTms", - "name": "DRGS.FilTms.AO42" - }, - { - "index": 43, - "description": "Dynamic Reactive Current Support Block Zone Voltage. Percentage of the nominal voltage (DRCT.VRef) below which no reactive current support shall be applied.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DRGS", - "units": "Percent", - "minimum": 0, - "data_object": "BlkZnV", - "name": "DRGS.BlkZnV.AO43" - }, - { - "index": 44, - "description": "Dynamic Reactive Current Support Hysteresis Block Zone Voltage. Percentage of the nominal voltage (DRCT.VRef). After being blocked, reactive current support shall not resume until the voltage has been above BlkZnV + HysBlkZnV.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DRGS", - "units": "Percent", - "minimum": 0, - "data_object": "HysBlkZnV", - "name": "DRGS.HysBlkZnV.AO44" - }, - { - "index": 45, - "description": "Dynamic Reactive Current Support Block Zone Time. Time in milliseconds from the beginning of any \"sag\" event,before which dynamic reactive current support will always continue,regardless of how low voltage may sag.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "ms", - "minimum": 0, - "data_object": "BlkZnTmms", - "name": "DRGS.BlkZnTmms.AO45" - }, - { - "index": 46, - "description": "Dynamic Reactive Current Support Start Hold Time. When the voltage exceeds the deadband limits for this length of time (measured in milliseconds),the \"sag\" or \"swell\" event begins and the DER may begin altering active power output.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "ms", - "minimum": 0, - "data_object": "HoldTmms", - "name": "DRGS.HoldTmms.AO46" - }, - { - "index": 47, - "description": "Dynamic Reactive Current Support End Hold Time. When the voltage returns to within the deadband limits for this length of time (measured in milliseconds),the \"sag\" or \"swell\" event is considered to be over. Reactive current support ends,frozen values are unfrozen,and a new event can begin.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "ms", - "minimum": 0, - "data_object": "HoldTmms", - "name": "DRGS.HoldTmms.AO47" - }, - { - "index": 48, - "description": "Dynamic Volt-Watt Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWD", - "minimum": 0, - "data_object": "ModPrio", - "name": "DVWD.ModPrio.AO48" - }, - { - "index": 49, - "description": "Dynamic Volt-Watt Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWD", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DVWD.WinTms.AO49" - }, - { - "index": 50, - "description": "Dynamic Volt-Watt Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWD", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DVWD.RmpTms.AO50" - }, - { - "index": 51, - "description": "Dynamic Volt-Watt Reversion Timeout period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWD", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DVWD.RvrtTms.AO51" - }, - { - "index": 52, - "description": "Dynamic Volt-Watt Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DVWD", - "minimum": 0, - "data_object": "EcpRef", - "name": "DVWD2.EcpRef.AO52" - }, - { - "index": 53, - "description": "Dynamic Volt-Watt Gradient. Signed unit-less quantity that establishes the ratio of additional Watts supplied (expressed in terms of % DRCT.WMax) to the present difference from the moving average voltage (expressed as % DRCT.VRef).", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DVWD", - "units": "Percent watts per percent voltage difference", - "data_object": "DynVWGra", - "name": "DVWD.DynVWGra.AO53" - }, - { - "index": 54, - "description": "Dynamic Volt-Watt Filter Time. The time in seconds used to calculate the moving average voltage for dynamic Volt-Watt support.", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DVWD", - "units": "Seconds", - "minimum": 0, - "data_object": "VWFilTms", - "name": "DVWD.VWFilTms.AO54" - }, - { - "index": 55, - "description": "Dynamic Volt-Watt Lower Deadband. Percentage of the nominal voltage (DRCT.Vref) measured below the moving average voltage. If the present voltage is above this value, no additional Watts shall be supplied.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DVWD", - "units": "Percent", - "minimum": 0, - "data_object": "DbVWLo", - "name": "DVWD.DbVWLo.AO55" - }, - { - "index": 56, - "description": "Dynamic Volt-Watt Upper Deadband. Percentage of the nominal voltage (DRCT.Vref) measured above the moving average voltage. If the present voltage is below this value,no additional Watts shall be supplied.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DVWD", - "units": "Percent", - "minimum": 0, - "data_object": "DbVWHi", - "name": "DVWD.DbVWHi.AO56" - }, - { - "index": 57, - "description": "Frequency-Watt Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "ModPrio", - "name": "DHFW2.ModPrio.AO57" - }, - { - "index": 58, - "description": "Frequency-Watt Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DHFW.WinTms.AO58" - }, - { - "index": 59, - "description": "Frequency-Watt Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DHFW.RmpTms.AO59" - }, - { - "index": 60, - "description": "Frequency-Watt Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DHFW.RvrtTms.AO60" - }, - { - "index": 61, - "description": "Frequency-Watt Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "EcpRef", - "name": "DHFW.EcpRef.AO61" - }, - { - "index": 62, - "description": "Frequency-Watt High Starting Frequency", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DHFW", - "units": "Hz", - "minimum": 0, - "data_object": "HzStr", - "name": "DHFW.HzStr.AO62" - }, - { - "index": 63, - "description": "Frequency-Watt High Stopping Frequency", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DHFW", - "units": "Hz", - "minimum": 0, - "data_object": "HzStop", - "name": "DHFW.HzStop.AO63" - }, - { - "index": 64, - "description": "Frequency-Watt High Discharging/Generating Gradient", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DHFW", - "units": "Percent watts per percent frequency difference", - "data_object": "WGra", - "name": "DHFW.WGra.AO64" - }, - { - "index": 65, - "description": "Frequency-Watt High Charging Gradient", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DHFW", - "units": "Percent watts per percent frequency difference", - "data_object": "WChaGra", - "name": "DHFW.WChaGra.AO65" - }, - { - "index": 66, - "description": "Frequency-Watt Low Starting Frequency", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 0, - "ln_class": "DLFW", - "units": "Hz", - "minimum": -70000, - "data_object": "HzStr", - "name": "DLFW.HzStr.AO66" - }, - { - "index": 67, - "description": "Frequency-Watt Low Stopping Frequency", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 0, - "ln_class": "DLFW", - "units": "Hz", - "minimum": -70000, - "data_object": "HzStop", - "name": "DLFW.HzStop.AO67" - }, - { - "index": 68, - "description": "Frequency-Watt Low Discharging/Generating Gradient", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DLFW", - "units": "Percent watts per percent frequency difference", - "data_object": "WGra", - "name": "DLFW.WGra.AO68" - }, - { - "index": 69, - "description": "Frequency-Watt Low Charging Gradient", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DLFW", - "units": "Percent watts per percent frequency difference", - "data_object": "WChaGra", - "name": "DLFW.WChaGra.AO69" - }, - { - "index": 70, - "description": "Frequency-Watt Start Delay", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "ActStrDlTmms", - "name": "DHFW2.ActStrDlTmms.AO70" - }, - { - "index": 71, - "description": "Frequency-Watt Stop Delay", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "ActStopDlTmms", - "name": "DHFW2.ActStopDlTmms.AO71" - }, - { - "index": 72, - "description": "Frequency-Watt Ramp Up Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoop", - "name": "DHFW.OpnLoop.AO72" - }, - { - "index": 73, - "description": "Frequency-Watt Ramp Down Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoop", - "name": "DHFW.OpnLoop.AO73" - }, - { - "index": 74, - "description": "Frequency-Watt Discharge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpuRte", - "name": "DHFW.DschRpuRte.AO74" - }, - { - "index": 75, - "description": "Frequency-Watt Discharge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpdRte", - "name": "DHFW.DschRpdRte.AO75" - }, - { - "index": 76, - "description": "Frequency-Watt Charge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpuRte", - "name": "DHFW.ChaRpuRte.AO76" - }, - { - "index": 77, - "description": "Frequency-Watt Charge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpdRte", - "name": "DHFW.ChaRpdRte.AO77" - }, - { - "index": 78, - "description": "Frequency-Watt Hi Return Gradient", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DHFW", - "units": "Percent watts per percent frequency difference", - "data_object": "RtnRmpRte", - "name": "DHFW.RtnRmpRte.AO78" - }, - { - "index": 79, - "description": "Frequency-Watt Low Return Gradient", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DLFW", - "units": "Percent watts per percent frequency difference", - "data_object": "RtnRmpRte", - "name": "DLFW.RtnRmpRte.AO79" - }, - { - "index": 80, - "description": "Frequency-Watt Minimum Usable SOC", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DHFW", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMin", - "name": "DHFW.SocUseMin.AO80" - }, - { - "index": 81, - "description": "Frequency-Watt Maximum Usable SOC", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DHFW", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMax", - "name": "DHFW.SocUseMax.AO81" - }, - { - "index": 82, - "description": "Active Power Limit Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWMX", - "minimum": 0, - "data_object": "ModPrio", - "name": "DWMX.ModPrio.AO82" - }, - { - "index": 83, - "description": "Active Power Limit Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWMX", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DWMX.WinTms.AO83" - }, - { - "index": 84, - "description": "Active Power Limit Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWMX", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DWMX.RmpTms.AO84" - }, - { - "index": 85, - "description": "Active Power Limit Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWMX", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DWMX.RvrtTms.AO85" - }, - { - "index": 86, - "description": "Active Power Limit Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DWMX", - "minimum": 0, - "data_object": "EcpRef", - "name": "DWMX.EcpRef.AO86" - }, - { - "index": 87, - "description": "Active Power Limit Charge Setpoint. Maximum allowed Watts as a percentage of Maximum Active Power capability.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWMX", - "units": "Percent", - "minimum": 0, - "data_object": "WLimPct", - "name": "DWMX.WLimPct.AO87" - }, - { - "index": 88, - "description": "Active Power Limit Discharge Setpoint. Maximum allowed Watts as a percentage of Maximum Active Power capability.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWMN", - "units": "Percent", - "minimum": 0, - "data_object": "WLimPct", - "name": "DWMN.WLimPct.AO88" - }, - { - "index": 89, - "description": "Charge/Discharge Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWGC", - "minimum": 0, - "data_object": "ModPrio", - "name": "DWGC.ModPrio.AO89" - }, - { - "index": 90, - "description": "Charge/Discharge Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DWGC.WinTms.AO90" - }, - { - "index": 91, - "description": "Charge/Discharge Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DWGC.RmpTms.AO91" - }, - { - "index": 92, - "description": "Charge/Discharge Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DWGC.RvrtTms.AO92" - }, - { - "index": 93, - "description": "Charge/Discharge Active Power Target. Percentage of maxmum active power.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWGC", - "units": "Percent", - "minimum": -1000, - "data_object": "GnWPctSpt", - "name": "DWGC.GnWPctSpt.AO93" - }, - { - "index": 94, - "description": "Charge/Discharge Ramp Up Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoop", - "name": "DWGC.OpnLoop.AO94" - }, - { - "index": 95, - "description": "Charge/Discharge Ramp Down Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoop", - "name": "DWGC.OpnLoop.AO95" - }, - { - "index": 96, - "description": "Charge/Discharge Discharge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpuRte", - "name": "DWGC.DschRpuRte.AO96" - }, - { - "index": 97, - "description": "Charge/Discharge Discharge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpdRte", - "name": "DWGC.DschRpdRte.AO97" - }, - { - "index": 98, - "description": "Charge/Discharge Charge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpuRte", - "name": "DWGC.ChaRpuRte.AO98" - }, - { - "index": 99, - "description": "Charge/Discharge Charge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpdRte", - "name": "DWGC.ChaRpdRte.AO99" - }, - { - "index": 100, - "description": "Charge/Discharge Minimum Reserve for Storage. The minimum level to which the storage system may be discharged,expressed as a percentage of the total usable storage.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWGC", - "units": "Percent", - "minimum": -1000, - "data_object": "SocUseMinPct", - "name": "DWGC.SocUseMinPct.AO100" - }, - { - "index": 101, - "description": "Charge/Discharge Maximum Reserve for Storage. The maximum level to which the storage system may be discharged,expressed as a percentage of the total usable storage.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWGC", - "units": "Percent", - "minimum": -1000, - "data_object": "SocUseMaxPct", - "name": "DWGC.SocUseMaxPct.AO101" - }, - { - "index": 102, - "description": "Coordinated Charge/Discharge Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "minimum": 0, - "data_object": "ModPrio", - "name": "DTCD.ModPrio.AO102" - }, - { - "index": 103, - "description": "Coordinated Charge/Discharge Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DTCD.WinTms.AO103" - }, - { - "index": 104, - "description": "Coordinated Charge/Discharge Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DTCD.RmpTms.AO104" - }, - { - "index": 105, - "description": "Coordinated Charge/Discharge Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DTCD.RvrtTms.AO105" - }, - { - "index": 106, - "description": "Coordinated Charge/Discharge Target State of Charge. Charge that the system is expected to achieve,as a percentage of the usable capacity.", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DTCD", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseTgtPct", - "name": "DTCD.SocUseTgtPct.AO106" - }, - { - "index": 107, - "description": "Coordinated Charge/Discharge Target Date. Date by which the storage system must reach the target SOC. Expressed as number of days since January 1, 1970, UTC.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Days", - "minimum": 0, - "data_object": "DateTgt", - "name": "DTCD.DateTgt.AO107" - }, - { - "index": 108, - "description": "Coordinated Charge/Discharge Target Time. Time by which storage system must reach the target SOC. Expressed as number of milliseconds since the start of Target Date.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "DateTgtTms", - "name": "DTCD.DateTgtTms.AO108" - }, - { - "index": 109, - "description": "Coordinated Charge/Discharge Energy Request", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Watt-hours", - "minimum": 0, - "data_object": "SocWReq", - "name": "DTCD.SocWReq.AO109" - }, - { - "index": 110, - "description": "Coordinated Charge/Discharge Minimum Charging Duration", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "ChaDurTms", - "name": "DTCD.ChaDurTms.AO110" - }, - { - "index": 111, - "description": "Coordinated Charge/Discharge Date of Reference", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Days", - "minimum": 0, - "data_object": "DateTgt", - "name": "DTCD.DateTgt.AO111" - }, - { - "index": 112, - "description": "Coordinated Charge/Discharge Time of Reference", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "SocDateTms", - "name": "DTCD.SocDateTms.AO112" - }, - { - "index": 113, - "description": "Coordinated Charge/Discharge Duration at Maximum Charge Rate", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "ChaDurMax", - "name": "DTCD.ChaDurMax.AO113" - }, - { - "index": 114, - "description": "Coordinated Charge/Discharge Duration at Maximum Discharge Rate", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "DschDurMax", - "name": "DTCD.DschDurMax.AO114" - }, - { - "index": 115, - "description": "Active Power Response Mode #1 Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPKP", - "minimum": 0, - "data_object": "ModPrio", - "name": "DPKP.ModPrio.AO115" - }, - { - "index": 116, - "description": "Active Power Response Mode #1 Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPKP", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DPKP.WinTms.AO116" - }, - { - "index": 117, - "description": "Active Power Response Mode #1 Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPKP", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DPKP.RmpTms.AO117" - }, - { - "index": 118, - "description": "Active Power Response Mode #1 Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPKP", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DPKP.RvrtTms.AO118" - }, - { - "index": 119, - "description": "Active Power Response Mode #1 Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DPKP", - "minimum": 0, - "data_object": "EcpRef", - "name": "DPKP.EcpRef.AO119" - }, - { - "index": 120, - "description": "Active Power Response Mode #1 Power Threshold", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DPKP", - "units": "Watts", - "data_object": "PkPwrWLim", - "name": "DPKP.PkPwrWLim.AO120" - }, - { - "index": 121, - "description": "Active Power Response Mode #1 Ratio", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DPKP", - "units": "Percent", - "minimum": 0, - "data_object": "PkPwrFolPct", - "name": "DPKP.PkPwrFolPct.AO121" - }, - { - "index": 122, - "description": "Active Power Response Mode #1 Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DPKP", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DPKP.RpuRte.AO122" - }, - { - "index": 123, - "description": "Active Power Response Mode #1 Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DPKP", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DPKP.RpdRte.AO123" - }, - { - "index": 124, - "description": "Active Power Response Mode #2 Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DGFL", - "minimum": 0, - "data_object": "ModPrio", - "name": "DGFL.ModPrio.AO124" - }, - { - "index": 125, - "description": "Active Power Response Mode #2 Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DGFL", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DGFL.WinTms.AO125" - }, - { - "index": 126, - "description": "Active Power Response Mode #2 Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DGFL", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DGFL.RmpTms.AO126" - }, - { - "index": 127, - "description": "Active Power Response Mode #2 Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DGFL", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DGFL.RvrtTms.AO127" - }, - { - "index": 128, - "description": "Active Power Response Mode #2 Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DGFL", - "minimum": 0, - "data_object": "EcpRef", - "name": "DGFL.EcpRef.AO128" - }, - { - "index": 129, - "description": "Active Power Response Mode #2 Power Threshold", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DGFL", - "units": "Watts", - "data_object": "PkPwrWLim", - "name": "DGFL.PkPwrWLim.AO129" - }, - { - "index": 130, - "description": "Active Power Response Mode #2 Ratio", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DGFL", - "units": "Percent", - "minimum": 0, - "data_object": "PkPwrFolPct", - "name": "DGFL.PkPwrFolPct.AO130" - }, - { - "index": 131, - "description": "Active Power Response Mode #2 Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DGFL", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DGFL.RpuRte.AO131" - }, - { - "index": 132, - "description": "Active Power Response Mode #2 Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DGFL", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DGFL.RpdRte.AO132" - }, - { - "index": 133, - "description": "Active Power Response Mode #3 Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DLFL", - "minimum": 0, - "data_object": "ModPrio", - "name": "DLFL.ModPrio.AO133" - }, - { - "index": 134, - "description": "Active Power Response Mode #3 Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DLFL", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DLFL.WinTms.AO134" - }, - { - "index": 135, - "description": "Active Power Response Mode #3 Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DLFL", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DLFL.RmpTms.AO135" - }, - { - "index": 136, - "description": "Active Power Response Mode #3 Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DLFL", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DLFL.RvrtTms.AO136" - }, - { - "index": 137, - "description": "Active Power Response Mode #3 Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DLFL", - "minimum": 0, - "data_object": "EcpRef", - "name": "DLFL.EcpRef.AO137" - }, - { - "index": 138, - "description": "Active Power Response Mode #3 Power Threshold", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DLFL", - "units": "Watts", - "data_object": "PkPwrWLim", - "name": "DLFL.PkPwrWLim.AO138" - }, - { - "index": 139, - "description": "Active Power Response Mode #3 Ratio", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DLFL", - "units": "Percent", - "minimum": 0, - "data_object": "PkPwrFolPct", - "name": "DLFL.PkPwrFolPct.AO139" - }, - { - "index": 140, - "description": "Active Power Response Mode #3 Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DLFL", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DLFL.RpuRte.AO140" - }, - { - "index": 141, - "description": "Active Power Response Mode #3 Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DLFL", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DLFL.RpdRte.AO141" - }, - { - "index": 142, - "description": "AGC Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DAGC", - "minimum": 0, - "data_object": "ModPrio", - "name": "DAGC.ModPrio.AO142" - }, - { - "index": 143, - "description": "AGC Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DAGC.WinTms.AO143" - }, - { - "index": 144, - "description": "AGC Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DAGC.RmpTms.AO144" - }, - { - "index": 145, - "description": "AGC Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DAGC.RvrtTms.AO145" - }, - { - "index": 146, - "description": "AGC Active Power Target", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DAGC", - "units": "Watts", - "data_object": "GnWSpt", - "name": "DAGC.GnWSpt.AO146" - }, - { - "index": 147, - "description": "AGC Ramp Time Constant Up Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoop", - "name": "DAGC.OpnLoop.AO147" - }, - { - "index": 148, - "description": "AGC Ramp Time Constant Down Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoop", - "name": "DAGC.OpnLoop.AO148" - }, - { - "index": 149, - "description": "AGC Discharge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DAGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpuRte", - "name": "DAGC.DschRpuRte.AO149" - }, - { - "index": 150, - "description": "AGC Discharge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DAGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpdRte", - "name": "DAGC.DschRpdRte.AO150" - }, - { - "index": 151, - "description": "AGC Charge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DAGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpuRte", - "name": "DAGC.ChaRpuRte.AO151" - }, - { - "index": 152, - "description": "AGC Charge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DAGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpdRte", - "name": "DAGC.ChaRpdRte.AO152" - }, - { - "index": 153, - "description": "AGC Minimum Usable SOC", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DAGC", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMinPct", - "name": "DAGC.SocUseMinPct.AO153" - }, - { - "index": 154, - "description": "AGC Maximum Usable SOC", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DAGC", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMaxPct", - "name": "DAGC.SocUseMaxPct.AO154" - }, - { - "index": 155, - "description": "Active Power Smoothing Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWSM", - "minimum": 0, - "data_object": "ModPrio", - "name": "DWSM.ModPrio.AO155" - }, - { - "index": 156, - "description": "Active Power Smoothing Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWSM", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DWSM.WinTms.AO156" - }, - { - "index": 157, - "description": "Active Power Smoothing Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWSM", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DWSM.RmpTms.AO157" - }, - { - "index": 158, - "description": "Active Power Smoothing Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWSM", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DWSM.RvrtTms.AO158" - }, - { - "index": 159, - "description": "Active Power Smoothing Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DWSM", - "minimum": 0, - "data_object": "EcpRef", - "name": "DWSM.EcpRef.AO159" - }, - { - "index": 160, - "description": "Active Power Smoothing Gradient", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DWSM", - "units": "Watts per delta-watt", - "data_object": "WSmthGra", - "name": "DWSM.WSmthGra.AO160" - }, - { - "index": 161, - "description": "Active Power Smoothing Lower Limit. Difference in Watts from the moving average of the reference power (MMXN1.Watt) above which no smoothing shall be applied.", - "data_type": "AO", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DWSM", - "units": "Watts", - "data_object": "WSmthLoLim", - "name": "DWSM.WSmthLoLim.AO161" - }, - { - "index": 162, - "description": "Active Power Smoothing Upper Limit. Difference in Watts from the moving average of the reference power (MMXN.Watt) below which no smoothing shall be applied.", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DWSM", - "units": "Watts", - "minimum": 0, - "data_object": "WSmthHiLim", - "name": "DWSM.WSmthHiLim.AO162" - }, - { - "index": 163, - "description": "Active Power Smoothing Filter Time (Seconds). Time in seconds used to calculate the moving average of the reference load or generation (MMXN1.Watt) being smoothed.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWSM", - "units": "Seconds", - "minimum": 0, - "data_object": "FilTms", - "name": "DWSM.FilTms.AO163" - }, - { - "index": 164, - "description": "Active Power Smoothing Discharge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWSM", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpuRte", - "name": "DWSM.DschRpuRte.AO164" - }, - { - "index": 165, - "description": "Active Power Smoothing Discharge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWSM", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpdRte", - "name": "DWSM.DschRpdRte.AO165" - }, - { - "index": 166, - "description": "Active Power Smoothing Charge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWSM", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpuRte", - "name": "DWSM.ChaRpuRte.AO166" - }, - { - "index": 167, - "description": "Active Power Smoothing Charge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWSM", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpdRte", - "name": "DWSM.ChaRpdRte.AO167" - }, - { - "index": 168, - "description": "Volt-Watt Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWC", - "minimum": 0, - "data_object": "ModPrio", - "name": "DVWC.ModPrio.AO168" - }, - { - "index": 169, - "description": "Volt-Watt Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DVWC.WinTms.AO169" - }, - { - "index": 170, - "description": "Volt-Watt Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DVWC.RmpTms.AO170" - }, - { - "index": 171, - "description": "Volt-Watt Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DVWC.RvrtTms.AO171" - }, - { - "index": 172, - "description": "Volt-Watt Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DVWC", - "minimum": 0, - "data_object": "EcpRef", - "name": "DVWC.EcpRef.AO172" - }, - { - "index": 173, - "description": "Volt-Watt Curve Index", - "data_type": "AO", - "common_data_class": "CSG", - "ln_class": "DVWC", - "minimum": 0, - "data_object": "VWCrv", - "name": "DVWC.VWCrv.AO173" - }, - { - "index": 174, - "description": "Volt-Watt Filter Time (Seconds)", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "FilTms", - "name": "DVWC.FilTms.AO174" - }, - { - "index": 175, - "description": "Volt-Watt Ramp Up Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoop", - "name": "DVWC.OpnLoop.AO175" - }, - { - "index": 176, - "description": "Volt-Watt Ramp Down Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoop", - "name": "DVWC.OpnLoop.AO176" - }, - { - "index": 177, - "description": "Volt-Watt Discharging Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DVWC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpuRte", - "name": "DVWC.DschRpuRte.AO177" - }, - { - "index": 178, - "description": "Volt-Watt Discharging Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DVWC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpdRte", - "name": "DVWC.DschRpdRte.AO178" - }, - { - "index": 179, - "description": "Volt-Watt Charging Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DVWC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpuRte", - "name": "DVWC.ChaRpuRte.AO179" - }, - { - "index": 180, - "description": "Volt-Watt Charging Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DVWC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpdRte", - "name": "DVWC.ChaRpdRte.AO180" - }, - { - "index": 181, - "description": "Frequency-Watt Curve Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "ModPrio", - "name": "DHFW.ModPrio.AO181" - }, - { - "index": 182, - "description": "Frequency-Watt Curve Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DHFW.WinTms.AO182" - }, - { - "index": 183, - "description": "Frequency-Watt Curve Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DHFW.RmpTms.AO183" - }, - { - "index": 184, - "description": "Frequency-Watt Curve Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DHFW.RvrtTms.AO184" - }, - { - "index": 185, - "description": "Frequency-Watt Curve Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "EcpRef", - "name": "DHFW.EcpRef.AO185" - }, - { - "index": 186, - "description": "Frequency-Watt Curve - Curve Index", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "HzWCrv", - "name": "DHFW.HzWCrv.AO186" - }, - { - "index": 187, - "description": "Frequency-Watt Curve - High Frequency Hysteresis Curve Index", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "HysCrv", - "name": "DHFW.HysCrv.AO187" - }, - { - "index": 188, - "description": "Frequency-Watt Curve - Low Frequency Hysteresis Curve Index", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DLFW", - "minimum": 0, - "data_object": "HysCrv", - "name": "DLFW.HysCrv.AO188" - }, - { - "index": 189, - "description": "Frequency-Watt Curve Start Delay", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "ActStrDlTmms", - "name": "DHFW.ActStrDlTmms.AO189" - }, - { - "index": 190, - "description": "Frequency-Watt Curve Stop Delay", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "ActStopDlTmms", - "name": "DHFW.ActStopDlTmms.AO190" - }, - { - "index": 191, - "description": "Frequency-Watt Curve Ramp Up Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DHFW.OpnLoopMax.AO191" - }, - { - "index": 192, - "description": "Frequency-Watt Curve Ramp Down Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DHFW.OpnLoopMax.AO192" - }, - { - "index": 193, - "description": "Frequency-Watt Curve Discharge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DHFW.RpuRte.AO193" - }, - { - "index": 194, - "description": "Frequency-Watt Curve Discharge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DHFW.RpdRte.AO194" - }, - { - "index": 195, - "description": "Frequency-Watt Curve Charge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuChaRte", - "name": "DHFW.RpuChaRte.AO195" - }, - { - "index": 196, - "description": "Frequency-Watt Curve Charge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdChaRte", - "name": "DHFW.RpdChaRte.AO196" - }, - { - "index": 197, - "description": "Frequency-Watt Curve Minimum Usable SOC", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DHFW", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMinPct", - "name": "DHFW.SocUseMinPct.AO197" - }, - { - "index": 198, - "description": "Frequency-Watt Curve Maximum Usable SOC", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DHFW", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMaxPct", - "name": "DHFW.SocUseMaxPct.AO198" - }, - { - "index": 199, - "description": "Constant VArs Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVAR", - "minimum": 0, - "data_object": "ModPrio", - "name": "DVAR.ModPrio.AO199" - }, - { - "index": 200, - "description": "Constant VArs Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DVAR.WinTms.AO200" - }, - { - "index": 201, - "description": "Constant VArs Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DVAR.RmpTms.AO201" - }, - { - "index": 202, - "description": "Constant VArs Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DVAR.RvrtTms.AO202" - }, - { - "index": 203, - "description": "Constant VArs Reactive Power Target. Percentage of maxmum reactive power.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DVAR", - "units": "Percent", - "minimum": -1000, - "data_object": "VArTgtPct", - "name": "DVAR.VArTgtPct.AO203" - }, - { - "index": 204, - "description": "Constant VArs Ramp Up Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVAR.OpnLoopMax.AO204" - }, - { - "index": 205, - "description": "Constant VArs Ramp Down Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVAR.OpnLoopMax.AO205" - }, - { - "index": 206, - "description": "Fixed Power Factor Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DFPF", - "minimum": 0, - "data_object": "ModPrio", - "name": "DFPF.ModPrio.AO206" - }, - { - "index": 207, - "description": "Fixed Power Factor Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DFPF", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DFPF.WinTms.AO207" - }, - { - "index": 208, - "description": "Fixed Power Factor Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DFPF", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DFPF.RmpTms.AO208" - }, - { - "index": 209, - "description": "Fixed Power Factor Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DFPF", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DFPF.RvrtTms.AO209" - }, - { - "index": 210, - "description": "Fixed Power Factor Setpoint - Generation/Discharging", - "data_type": "AO", - "common_data_class": "APC", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DFPF", - "units": "None", - "minimum": 0, - "data_object": "PFGnTgt", - "name": "DFPF.PFGnTgt.AO210" - }, - { - "index": 211, - "description": "Fixed Power Factor Setpoint - Charging", - "data_type": "AO", - "common_data_class": "APC", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DFPF", - "units": "None", - "minimum": 0, - "data_object": "PFLodTgt", - "name": "DFPF.PFLodTgt.AO211" - }, - { - "index": 212, - "description": "Volt-VAr Control Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVVR", - "minimum": 0, - "data_object": "ModPrio", - "name": "DVVR.ModPrio.AO212" - }, - { - "index": 213, - "description": "Volt-VAr Control Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DVVR.WinTms.AO213" - }, - { - "index": 214, - "description": "Volt-VAr Control Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DVVR.RmpTms.AO214" - }, - { - "index": 215, - "description": "Volt-VAr Control Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DVVR.RvrtTms.AO215" - }, - { - "index": 216, - "description": "Volt-VAr Control Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DVVR", - "minimum": 0, - "data_object": "EcpRef", - "name": "DVVR.EcpRef.AO216" - }, - { - "index": 217, - "description": "Volt-VAr Curve Index", - "data_type": "AO", - "common_data_class": "CSG", - "ln_class": "DVVR", - "minimum": 0, - "data_object": "VVArCrv", - "name": "DVVR.VVArCrv.AO217" - }, - { - "index": 218, - "description": "Volt-VAr Ramp Up Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVVR.OpnLoopMax.AO218" - }, - { - "index": 219, - "description": "Volt-VAr Ramp Down Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVVR.OpnLoopMax.AO219" - }, - { - "index": 220, - "description": "Volt-VAr Autonomous Voltage Reference Adjustment Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "VRefTmms", - "name": "DVVR.VRefTmms.AO220" - }, - { - "index": 221, - "description": "Watt-VAr Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWVR", - "minimum": 0, - "data_object": "ModPrio", - "name": "DWVR.ModPrio.AO221" - }, - { - "index": 222, - "description": "Watt-VAr Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DWVR.WinTms.AO222" - }, - { - "index": 223, - "description": "Watt-VAr Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DWVR.RmpTms.AO223" - }, - { - "index": 224, - "description": "Watt-VAr Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DWVR.RvrtTms.AO224" - }, - { - "index": 225, - "description": "Watt-VAr Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DWVR", - "minimum": 0, - "data_object": "EcpRef", - "name": "DWVR.EcpRef.AO225" - }, - { - "index": 226, - "description": "Watt-VAr Curve Index", - "data_type": "AO", - "common_data_class": "CSG", - "ln_class": "DWVR", - "minimum": 0, - "data_object": "WVArCrv", - "name": "DWVR.WVArCrv.AO226" - }, - { - "index": 227, - "description": "Watt-VAr Ramp Up Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DWVR.OpnLoopMax.AO227" - }, - { - "index": 228, - "description": "Watt-VAr Ramp Down Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DWVR.OpnLoopMax.AO228" - }, - { - "index": 229, - "description": "Power Factor Correction Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPFC", - "minimum": 0, - "data_object": "ModPrio", - "name": "DPFC.ModPrio.AO229" - }, - { - "index": 230, - "description": "Power Factor Correction Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPFC", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DPFC.WinTms.AO230" - }, - { - "index": 231, - "description": "Power Factor Correction Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DPFC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpRte", - "name": "DPFC.RmpRte.AO231" - }, - { - "index": 232, - "description": "Power Factor Correction Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPFC", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DPFC.RvrtTms.AO232" - }, - { - "index": 233, - "description": "Power Factor Correction Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DPFC", - "minimum": 0, - "data_object": "EcpRef", - "name": "DPFC.EcpRef.AO233" - }, - { - "index": 234, - "description": "Power Factor Correction Average PF Target", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DPFC", - "units": "None", - "minimum": -100, - "data_object": "PFTrg", - "name": "DPFC.PFTrg.AO234" - }, - { - "index": 235, - "description": "Power Factor Correction Lower PF Limit", - "data_type": "AO", - "common_data_class": "Int", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DPFC", - "units": "None", - "minimum": -100, - "data_object": "PFCorRef.rangeC", - "name": "DPFC.PFCorRef.rangeC.AO235" - }, - { - "index": 236, - "description": "Power Factor Correction Upper PF Limit", - "data_type": "AO", - "common_data_class": "Int", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DPFC", - "units": "None", - "minimum": -100, - "data_object": "PFCorRef.rangeC", - "name": "DPFC.PFCorRef.rangeC.AO236" - }, - { - "index": 237, - "description": "Pricing Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPRG", - "units": "None", - "minimum": 0, - "data_object": "ModPrio", - "name": "DPRG.ModPrio.AO237" - }, - { - "index": 238, - "description": "Pricing Mode Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DPRG.WinTms.AO238" - }, - { - "index": 239, - "description": "Pricing Mode Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DPRG.RmpTms.AO239" - }, - { - "index": 240, - "description": "Pricing Mode Reversion Timeout period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DPRG.RvrtTms.AO240" - }, - { - "index": 241, - "description": "Pricing Mode Setpoint. Hundredths of local currency per Kilowatt-Hr.", - "data_type": "AO", - "common_data_class": "MV", - "scaling_multiplier": 0.01, - "ln_class": "DPRG", - "units": "100ths of local currency", - "data_object": "PrcRef", - "name": "DPRG.PrcRef.AO241" - }, - { - "index": 242, - "description": "Pricing Mode Ramp Up Time Constant", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DPRG.OpnLoopMax.AO242" - }, - { - "index": 243, - "description": "Pricing Mode Ramp Down Time Constant", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DPRG.OpnLoopMax.AO243" - }, - { - "index": 244, - "description": "Curve Edit Selector. Writing to this point selects which of the curves can currently be viewed and changed.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DGSM", - "minimum": 1, - "data_object": "InCrv", - "name": "DGSMn.InCrv.AO244", - "type": "selector_block", - "selector_block_start": 244, - "selector_block_end": 448 - }, - { - "index": 245, - "description": "Curve Mode Type", - "data_type": "AO", - "common_data_class": "ENG", - "maximum": 20, - "ln_class": "DGSM", - "units": "None (list)", - "minimum": 0, - "data_object": "ModTyp", - "allowed_values": { - "0": "Curve disabled", - "1": "Not applicable / Unknown", - "2": "Volt-Var modes VV11-VV12", - "3": "Frequency-Watt mode FW22", - "4": "Watt-VAr mode WP42", - "5": "Voltage-Watt modes VW51-VW52", - "6": "Remain Connected", - "7": "Temperature mode", - "8": "Pricing signal mode", - "9": "HVRT Must Trip", - "10": "HVRT Momentary Cessation", - "11": "LVRT Must Trip", - "12": "LVRT Momentary Cessation", - "13": "HFRT Must Trip", - "14": "HFRT Momentary Cessation", - "15": "LFRT Must Trip", - "16": "LFRT Mandatory Operation" - }, - "type": "enumerated", - "name": "DGSMn.ModTyp.AO245" - }, - { - "index": 246, - "description": "Curve Number of Points", - "data_type": "AO", - "common_data_class": "CSG", - "maximum": 100, - "ln_class": "FMAR", - "minimum": 0, - "data_object": "PairArr.NumPts", - "name": "FMARn.PairArr.NumPts.AO246" - }, - { - "index": 247, - "description": "Independent (X-Value) Units for Curve", - "data_type": "AO", - "common_data_class": "ENG", - "maximum": 255, - "ln_class": "FMAR", - "units": "None (list)", - "minimum": 0, - "data_object": "IndpUnits", - "allowed_values": { - "0": "Curve disabled", - "1": "Not applicable / Unknown", - "4": "Time", - "23": "Celsius Temperature", - "29": "Voltage", - "33": "Frequency", - "38": "Watts", - "100": "Price in hundredths of local currency", - "129": "Percent Voltage", - "133": "Percent Frequency", - "138": "Percent Watts", - "233": "Frequency Deviation" - }, - "type": "enumerated", - "name": "FMARn.IndpUnits.AO247" - }, - { - "index": 248, - "description": "Dependent (Y-Value) Units for Curve", - "data_type": "AO", - "common_data_class": "ENG", - "maximum": 255, - "ln_class": "FMAR", - "units": "None (list)", - "minimum": 0, - "data_object": "DepRef", - "allowed_values": { - "0": "Curve disabled", - "1": "Not applicable / unknown", - "2": "VArs as percent of max VArs (VARMax)", - "3": "VArs as percent of max available VArs (VArAval)", - "4": "Vars as percent of max Watts (Wmax) not used", - "5": "Watts as percent of max Watts (Wmax)", - "6": "Watts as percent of frozen active power (DeptSnptRef)", - "7": "Power Factor in EEI notation", - "8": "Volts as a percent of the nominal voltage (VRef)", - "9": "Frequency as a percent of the nominal grid frequency (ECPNomHz)" - }, - "type": "enumerated", - "name": "FMARn.DepRef.AO248" - }, - { - "index": 249, - "description": "Curve X-Value and Y-Value pairs for curve points 1 - 100", - "data_type": "AO", - "common_data_class": "CSG", - "ln_class": "FMAR", - "units": "Varies", - "data_object": "PairArr.CrvPts", - "name": "FMARn.PairArr.CrvPts.AO249", - "type": "array", - "array_times_repeated": 100, - "array_points": [ - { - "name": "FMARn.PairArr.CrvPts.AO249.xVal" - }, - { - "name": "FMARn.PairArr.CrvPts.AO249.yVal" - } - ] - }, - { - "index": 449, - "description": "System Meter Active Power - High Threshold", - "data_type": "AO", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "minimum": 0, - "data_object": "TotW.rangeC.hLim", - "name": "MMXU.TotW.rangeC.hLim.AO449" - }, - { - "index": 450, - "description": "System Meter Active Power - Low Threshold", - "data_type": "AO", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "minimum": 0, - "data_object": "TotW.rangeC.lLim", - "name": "MMXU.TotW.rangeC.lLim.AO450" - }, - { - "index": 451, - "description": "System Meter Reactive Power - High Threshold", - "data_type": "AO", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "VARs", - "minimum": 0, - "data_object": "TotVAr.rangeC.hLim", - "name": "MMXU.TotVAr.rangeC.hLim.AO451" - }, - { - "index": 452, - "description": "System Meter at Reactive Power - Low Threshold", - "data_type": "AO", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "VARs", - "minimum": 0, - "data_object": "TotVAr.rangeC.lLim", - "name": "MMXU.TotVAr.rangeC.lLim.AO452" - }, - { - "index": 453, - "description": "System Meter at Power Factor - High Threshold", - "data_type": "AO", - "common_data_class": "MV", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "MMXU", - "units": "None", - "minimum": -100, - "data_object": "TotPF.rangeC.hLim", - "name": "MMXU.TotPF.rangeC.hLim.AO453" - }, - { - "index": 454, - "description": "System Meter at Power Factor - Low Threshold", - "data_type": "AO", - "common_data_class": "MV", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "MMXU", - "units": "None", - "minimum": -100, - "data_object": "TotPF.rangeC.lLim", - "name": "MMXU.TotPF.rangeC.lLim.AO454" - }, - { - "index": 455, - "description": "System Meter Phase A Volts - High Threshold", - "data_type": "AO", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsA.rangeC.hLim", - "name": "MMXU.PhV.phsA.rangeC.hLim.AO455" - }, - { - "index": 456, - "description": "System Meter Phase A Volts - Low Threshold", - "data_type": "AO", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsA.rangeC.lLim", - "name": "MMXU.PhV.phsA.rangeC.lLim.AO456" - }, - { - "index": 457, - "description": "System Meter Phase B Volts High Threshold", - "data_type": "AO", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsB.rangeC.hLim", - "name": "MMXU.PhV.phsB.rangeC.hLim.AO457" - }, - { - "index": 458, - "description": "System Meter Phase B Volts - Low Threshold", - "data_type": "AO", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsB.rangeC.lLim", - "name": "MMXU.PhV.phsB.rangeC.lLim.AO458" - }, - { - "index": 459, - "description": "System Meter Phase C Volts - High Threshold", - "data_type": "AO", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsC.rangeC.hLim", - "name": "MMXU.PhV.phsC.rangeC.hLim.AO459" - }, - { - "index": 460, - "description": "System Meter Phase C Volts - Low Threshold", - "data_type": "AO", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsC.rangeC.lLim", - "name": "MMXU.PhV.phsC.rangeC.lLim.AO460" - }, - { - "index": 461, - "description": "Schedule to Edit Selector. Selects which of the schedules can be currently viewed and changed.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "FSCC", - "minimum": 0, - "data_object": "Schd", - "name": "FSCC.Schd.AO461", - "type": "selector_block", - "selector_block_start": 461, - "selector_block_end": 669 - - }, - { - "index": 462, - "description": "Selected Schedule Identity", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "FSCC", - "minimum": 0, - "data_object": "Schd", - "name": "FSCC.Schd.AO462" - }, - { - "index": 463, - "description": "Selected Schedule Priority. Priority of the schedule relative to other running schedules. Lower values have higher priority over higher values.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "FSCH", - "minimum": 0, - "data_object": "SchdPrio", - "name": "FSCH1.SchdPrio.AO463" - }, - { - "index": 464, - "description": "Selected Schedule Type", - "data_type": "AO", - "common_data_class": "SCR", - "maximum": 30, - "ln_class": "FSCH", - "units": "None (list)", - "minimum": 0, - "data_object": "SchdVal.valEq", - "allowed_values": { - "1": "Low/High Voltage Ride-Through - Hi Must Trip", - "2": "Low/High Voltage Ride-Through - Low Must Trip", - "3": "Low/High Voltage Ride-Through - Hi Momentary", - "4": "Low/High Voltage Ride-Through - Lo Momentary", - "5": "Low/High Frequency Ride-Through - Hi Must Trip", - "6": "Low/High Frequency Ride-Through - Lo Must Trip", - "7": "Low/High Frequency Ride-Through - Hi Momentary", - "8": "Low/High Frequency Ride-Through - Low Momentary", - "9": "Dynamic Reactive Current Support - On/Off", - "10": "Dynamic Volt-Watt - On/Off", - "11": "Frequency-Watt - On/Off", - "12": "Active Power Limit - Charging", - "13": "Active Power Limit - Generating", - "14": "Charge/Discharge - Percent of Maximum", - "15": "Coordinated Charge/Discharge - SOC Target", - "16": "Active Power Response #1 - On/Off", - "17": "Active Power Response #2 - On/Off", - "18": "Active Power Response #3 - On/Off", - "19": "AGC - Watts", - "20": "Active Power Smoothing - On/Off", - "21": "Volt-Watt - Curve Index", - "22": "Frequency-Watt Curve - Curve Index", - "23": "Frequency-Watt Curve - High Hysteresis", - "24": "Frequency-Watt Curve - Low Hysteresis", - "25": "Constant VArs - Percent of Maximum", - "26": "Fixed Power Factor - Power Factor", - "27": "Volt-VAr - Curve Index", - "28": "Watt-VAr - Curve Index", - "29": "Power Factor Correction - On/Off", - "30": "Reserved - For pricing mode" - }, - "type": "enumerated", - "name": "FSCH.SchdVal.valEq.AO464" - }, - { - "index": 465, - "description": "Selected Schedule Start Date. Number of days since January 1, 1970, UTC.", - "data_type": "AO", - "common_data_class": "TSG", - "ln_class": "FSCH", - "units": "Days", - "minimum": 0, - "data_object": "StrTm", - "name": "FSCH.StrTm.AO465" - }, - { - "index": 466, - "description": "Selected Schedule Start Time. Milliseconds since the start of Schedule Start Date.", - "data_type": "AO", - "common_data_class": "TSG", - "maximum": 86400000, - "ln_class": "FSCH", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "StrTm", - "name": "FSCH.StrTm.AO466" - }, - { - "index": 467, - "description": "Selected Schedule Repeat Interval. Interval between actions after the initial occurrence. A zero value means the schedule is not repeated.", - "data_type": "AO", - "common_data_class": "TCS", - "ln_class": "FSCH", - "minimum": 0, - "data_object": "NxtStrTm", - "name": "FSCH.NxtStrTm.AO467" - }, - { - "index": 468, - "description": "Selected Schedule Repeat Interval Units", - "data_type": "AO", - "common_data_class": "SPG", - "maximum": 8, - "ln_class": "FSCH", - "units": "None (list)", - "minimum": 0, - "data_object": "SchdReuse", - "allowed_values": { - "0": "No Repeat", - "1": "Seconds", - "2": "Minutes", - "3": "Hours", - "4": "Days", - "5": "Weeks", - "6": "Months", - "7": "Months on Same Day of Week", - "8": "Months on Same Day of Week from End" - }, - "type": "enumerated", - "name": "FSCH.SchdReuse.AO468" - }, - { - "index": 469, - "description": "Selected Schedule Number of Points", - "data_type": "AO", - "common_data_class": "ING", - "maximum": 100, - "ln_class": "FSCH", - "minimum": 0, - "data_object": "NumEntr", - "name": "FSCH.NumEntr.AO469" - }, - { - "index": 470, - "description": "Select schedule time offset and value pairs for points 1 - 100", - "data_type": "AO", - "minimum": 0, - "name": "FSCHn.SchdEntr.AO470", - "type": "array", - "array_times_repeated": 100, - "array_points": [ - { - "name": "FSCHn.SchdEntr.AO470.time", - "description": "Number of seconds from the start of the schedule when this point becomes active", - "units": "Seconds" - }, - { - "name": "FSCHn.SchdEntr.AO470.val", - "ln_class": "FSCH", - "data_object": "SchdEntr" - } - ] - }, - { - "index": 0, - "description": "DER Profile Version Number. Always the number 1.00 for this specification.", - "data_type": "AI", - "scaling_multiplier": 0.01, - "maximum": 100, - "minimum": 100, - "event_class": 3, - "name": "AI0" - }, - { - "index": 1, - "description": "DER Profile Implementation Level. 1, 2 or 3 to indicate support for Level 1, Level 2 or Level 3 respectively.", - "data_type": "AI", - "maximum": 3, - "minimum": 1, - "event_class": 3, - "name": "AI1" - }, - { - "index": 2, - "description": "Nameplate Minimum Voltage Rating", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DGEN", - "units": "Volts", - "minimum": 0, - "data_object": "VMinRtg", - "event_class": 3, - "name": "DGEN.VMinRtg.AI2" - }, - { - "index": 3, - "description": "Nameplate Maximum Voltage Rating", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DGEN", - "units": "Volts", - "minimum": 0, - "data_object": "VMaxRtg", - "event_class": 3, - "name": "DGEN.VMaxRtg.AI3" - }, - { - "index": 4, - "description": "Nameplate Active Generation Power Rating at Unity Power Factor", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "Watts", - "minimum": 0, - "data_object": "WMaxRtg", - "event_class": 3, - "name": "DGEN.WMaxRtg.AI4" - }, - { - "index": 5, - "description": "Nameplate Active Charging Power Rating at Unity Power Factor", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DSTO", - "units": "Watts", - "data_object": "ChaWMaxRtg", - "event_class": 3, - "name": "DSTO.ChaWMaxRtg.AI5" - }, - { - "index": 6, - "description": "Nameplate Active Generation Power Rating at Specified Over-Excited Power Factor", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "Watts", - "minimum": 0, - "data_object": "WOvPFRtg", - "event_class": 3, - "name": "DGEN.WOvPFRtg.AI6" - }, - { - "index": 7, - "description": "Nameplate Active Charging Power Rating at Specified Over-Excited Power Factor", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DSTO", - "units": "Watts", - "data_object": "ChaWOvPFRtg", - "event_class": 3, - "name": "DSTO.ChaWOvPFRtg.AI7" - }, - { - "index": 8, - "description": "Specified Over-Excited Power Factor", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DGEN", - "units": "None", - "minimum": -100, - "data_object": "OvPFRtg", - "event_class": 3, - "name": "DGEN.OvPFRtg.AI8" - }, - { - "index": 9, - "description": "Nameplate Active Generation Power Rating at Specified Under-Excited Power Factor", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "Watts", - "minimum": 0, - "data_object": "WUnPFRtg", - "event_class": 3, - "name": "DGEN.WUnPFRtg.AI9" - }, - { - "index": 10, - "description": "Nameplate Active Charging Power Rating at Specified Under-Excited Power Factor", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DSTO", - "units": "Watts", - "data_object": "ChaWUnPFRtg", - "event_class": 3, - "name": "DSTO.ChaWUnPFRtg.AI10" - }, - { - "index": 11, - "description": "Specified Under-Excited Power Factor", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DGEN", - "units": "None", - "minimum": -100, - "data_object": "UnPFRtg", - "event_class": 3, - "name": "DGEN.UnPFRtg.AI11" - }, - { - "index": 12, - "description": "Nameplate Reactive Supply (Injection) Power Rating", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "VARs", - "minimum": 0, - "data_object": "IvarMaxRtg", - "event_class": 3, - "name": "DGEN.IvarMaxRtg.AI12" - }, - { - "index": 13, - "description": "Nameplate Reactive Absorption Power Rating", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DGEN", - "units": "VARs", - "data_object": "AvarMaxRtg", - "event_class": 3, - "name": "DGEN.AvarMaxRtg.AI13" - }, - { - "index": 14, - "description": "Nameplate Apparent Generation Power Rating", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "VAs", - "minimum": 0, - "data_object": "VAMaxRtg", - "event_class": 3, - "name": "DGEN.VAMaxRtg.AI14" - }, - { - "index": 15, - "description": "Nameplate Apparent Charging Power Rating", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DSTO", - "units": "VAs", - "data_object": "ChaVAMaxRtg", - "event_class": 3, - "name": "DSTO.ChaVAMaxRtg.AI15" - }, - { - "index": 16, - "description": "Nameplate Storage Actual Energy Capacity. Nameplate (original) actual total energy capacity of the storage system expressed in Storage Capacity Units.", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DSTO", - "units": "Amp-hrs or Watt-hrs", - "minimum": 0, - "data_object": "WhRtg", - "event_class": 3, - "name": "DSTO.WhRtg.AI16" - }, - { - "index": 17, - "description": "Storage Effective Actual Energy Capacity. Present actual total energy capacity of the storage system expressed in Storage Capacity Units.", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DSTO", - "units": "Amp-hrs or Watt-hrs", - "minimum": 0, - "data_object": "EffWh", - "event_class": 3, - "name": "DSTO.EffWh.AI17" - }, - { - "index": 18, - "description": "Storage Usable Energy Capacity. Usable energy capacity of the storage system expressed in Storage Capacity Units.", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DSTO", - "units": "Amp-hrs or Watt-hrs", - "minimum": 0, - "data_object": "UseWh", - "event_class": 3, - "name": "DSTO.UseWh.AI18" - }, - { - "index": 19, - "description": "Nameplate AC Current Maximum Generation Rating", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DGEN", - "units": "Amps", - "minimum": 0, - "data_object": "AMaxRtg", - "event_class": 3, - "name": "DGEN.AMaxRtg.AI19" - }, - { - "index": 20, - "description": "Nameplate AC Current Maximum Charging Rating", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 0, - "ln_class": "DSTO", - "units": "Amps", - "data_object": "ChaAMaxRtg", - "event_class": 3, - "name": "DSTO.ChaAMaxRtg.AI20" - }, - { - "index": 21, - "description": "Remaining Reactive Susceptance", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "Siemens", - "data_object": "SuscRtg", - "event_class": 3, - "name": "DGEN.SuscRtg.AI21" - }, - { - "index": 22, - "description": "IEEE 1547 Normal Operating Performance Category.", - "data_type": "AI", - "maximum": 2, - "minimum": 0, - "units": "None (list)", - "event_class": 3, - "allowed_values": { - "0": "unknown", - "1": "Category A", - "2": "Category B" - }, - "type": "enumerated", - "name": "AI22" - }, - { - "index": 23, - "description": "IEEE 1547 Abnormal Operating Performance Category.", - "data_type": "AI", - "maximum": 3, - "minimum": 0, - "units": "None (list)", - "event_class": 3, - "allowed_values": { - "0": "unknown", - "1": "Category I", - "2": "Category II", - "3": "Category III" - }, - "type": "enumerated", - "name": "AI23" - }, - { - "index": 24, - "description": "Number of System Schedules", - "data_type": "AI", - "minimum": 0, - "units": "None", - "event_class": 3, - "name": "AI24" - }, - { - "index": 25, - "description": "Number of Meters", - "data_type": "AI", - "minimum": 0, - "units": "None", - "event_class": 3, - "name": "AI25" - }, - { - "index": 26, - "description": "Number of Inverters", - "data_type": "AI", - "minimum": 0, - "units": "None", - "event_class": 3, - "name": "AI26" - }, - { - "index": 27, - "description": "Number of Batteries", - "data_type": "AI", - "minimum": 0, - "units": "None", - "event_class": 3, - "name": "AI27" - }, - { - "index": 28, - "description": "Number of DER units connected to controller", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DSTO", - "units": "None", - "minimum": 0, - "data_object": "InclDER", - "event_class": 3, - "name": "DSTO.InclDER.AI28" - }, - { - "index": 29, - "description": "Reference Voltage", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DECP", - "units": "Volts", - "minimum": 0, - "data_object": "VRef", - "name": "DECP.VRef.AI29" - }, - { - "index": 30, - "description": "Reference Voltage Offset", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DECP", - "units": "Volts", - "data_object": "VRefOfs", - "name": "DECP.VRefOfs.AI30" - }, - { - "index": 31, - "description": "Nominal Grid Frequency", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DECP", - "units": "Hz", - "minimum": 0, - "data_object": "EcpNomHz", - "name": "DECP.EcpNomHz.AI31" - }, - { - "index": 32, - "description": "Maximum Active Generation Power", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "Watts", - "minimum": 0, - "data_object": "WMax", - "name": "DGEN.WMax.AI32" - }, - { - "index": 33, - "description": "Maximum Active Charging Power", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DSTO", - "units": "Watts", - "data_object": "ChaWMax", - "name": "DSTO.ChaWMax.AI33" - }, - { - "index": 34, - "description": "Maximum Reactive Injection Power", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "VARs", - "minimum": 0, - "data_object": "IvarMax", - "name": "DGEN.IvarMax.AI34" - }, - { - "index": 35, - "description": "Maximum Reactive Absorption Power", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DGEN", - "units": "VARs", - "data_object": "AvarMax", - "name": "DGEN.AvarMax.AI35" - }, - { - "index": 36, - "description": "Maximum Apparent Generation Power", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "VA", - "minimum": 0, - "data_object": "VAMax", - "name": "DGEN.VAMax.AI36" - }, - { - "index": 37, - "description": "Maximum Apparent Charging Power", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DSTO", - "units": "VA", - "data_object": "ChaVAMax", - "name": "DSTO.ChaVAMax.AI37" - }, - { - "index": 38, - "description": "Minimum Voltage", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DECP", - "units": "Volts", - "minimum": 0, - "data_object": "VMin", - "name": "DECP.VMin.AI38" - }, - { - "index": 39, - "description": "Maximum Voltage", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DECP", - "units": "Volts", - "minimum": 0, - "data_object": "VMax", - "name": "DECP.VMax.AI39" - }, - { - "index": 40, - "description": "Open Loop Response Time Percentage. Percent of target to reach within the open loop response time. Default is 90%.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DGEN", - "units": "Percent", - "minimum": 0, - "data_object": "OpnLoopPct", - "name": "DGEN.OpnLoopPct.AI40" - }, - { - "index": 41, - "description": "Power Factor Sign Convention.", - "data_type": "AI", - "common_data_class": "ENG", - "maximum": 2, - "ln_class": "MMXU", - "units": "None", - "minimum": 1, - "data_object": "PFSign", - "allowed_values": { - "1": "IEC active power", - "2": "IEEE lead/lag" - }, - "type": "enumerated", - "name": "MMXU.PFSign.AI41" - }, - { - "index": 42, - "description": "Reference for Reactive Power Setpoints. Selects which setpoint is active. Default is <3>.", - "data_type": "AI", - "common_data_class": "ENG", - "maximum": 3, - "ln_class": "DGEN", - "units": "None (list)", - "minimum": 0, - "data_object": "VArSetRef", - "allowed_values": { - "0": "Not applicable / Unknown", - "1": "Percent of Maximum Active Power (WMax)", - "2": "Percent of Maximum Reactive Power (VArMax)", - "3": "Percent of Available Reactive Power (VArAvl)" - }, - "type": "enumerated", - "name": "DGEN.VArSetRef.AI42" - }, - { - "index": 43, - "description": "System Available Active Generation Power", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "minimum": 0, - "data_object": "TotW", - "name": "MMXU.TotW.AI43" - }, - { - "index": 44, - "description": "System Available Active Charging Power", - "data_type": "AI", - "common_data_class": "MV", - "maximum": 0, - "ln_class": "MMXU", - "units": "Watts", - "data_object": "TotChaW", - "name": "MMXU.TotChaW.AI44" - }, - { - "index": 45, - "description": "System Available Reactive Injection Power", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DGEN", - "units": "VARs", - "minimum": 0, - "data_object": "AvarAvl", - "name": "DGEN.AvarAvl.AI45" - }, - { - "index": 46, - "description": "System Available Reactive Absorption Power", - "data_type": "AI", - "common_data_class": "MV", - "maximum": 0, - "ln_class": "DGEN", - "units": "VARs", - "data_object": "IvarAvl", - "name": "DGEN.IvarAvl.AI46" - }, - { - "index": 47, - "description": "System Available Actual State of Charge - Present energy in the DER as a percentage of Storage Effective Actual Capacity", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DSTO", - "units": "Percent", - "minimum": 0, - "data_object": "SocPct", - "name": "DSTO.SocPct.AI47" - }, - { - "index": 48, - "description": "System Usable State of Charge - Present usable energy in the DER as a percentage of Nameplate Storage Usable Capacity", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DSTO", - "units": "Percent", - "minimum": 0, - "data_object": "UseSocPct", - "name": "DSTO.UseSocPct.AI48" - }, - { - "index": 49, - "description": "System Start-up Status", - "data_type": "AI", - "common_data_class": "ENS", - "maximum": 99, - "ln_class": "DGEN", - "units": "None (list)", - "minimum": -1, - "data_object": "DEROpSt", - "name": "DGEN.DEROpSt.AI49" - }, - { - "index": 50, - "description": "DER Start (Return to Service) Voltage High Limit. Percent of Reference Voltage.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 20000, - "ln_class": "DCTE", - "units": "Percent", - "minimum": 0, - "data_object": "VHiLim", - "name": "DCTE.VHiLim.AI50" - }, - { - "index": 51, - "description": "DER Start (Return to Service) Voltage Low Limit. Percent of Reference Voltage.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 10000, - "ln_class": "DCTE", - "units": "Percent", - "minimum": 0, - "data_object": "VLoLim", - "name": "DCTE.VLoLim.AI51" - }, - { - "index": 52, - "description": "DER Start (Return to Service) Frequency High Limit", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DCTE", - "units": "Hz", - "minimum": 0, - "data_object": "HzHiLim", - "name": "DCTE.HzHiLim.AI52" - }, - { - "index": 53, - "description": "DER Start (Return to Service) Frequency Low Limit", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DCTE", - "units": "Hz", - "minimum": 0, - "data_object": "HzLoLim", - "name": "DCTE.HzLoLim.AI53" - }, - { - "index": 54, - "description": "DER Start (Return to Service) Delay", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RtnDlTmms", - "name": "DCTE.RtnDlTmms.AI54" - }, - { - "index": 55, - "description": "DER Start (Return to Service) Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DCTE.WinTms.AI55" - }, - { - "index": 56, - "description": "DER Start (Return to Service) Ramp Up Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RtnRmpTmms", - "name": "DCTE.RtnRmpTmms.AI56" - }, - { - "index": 57, - "description": "DER Stop (Cease to Energize) Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DCTE.WinTms.AI57" - }, - { - "index": 58, - "description": "DER Stop (Cease to Energize) Ramp Down Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DCTE.RmpTms.AI58" - }, - { - "index": 59, - "description": "DER Stop (Cease to Energize) Reversion Timeout Period. Time to revert from the stopped state and return to service.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DCTE.RvrtTms.AI59" - }, - { - "index": 60, - "description": "Connect/Disconnect Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DCTE.WinTms.AI60" - }, - { - "index": 61, - "description": "Connect/Disconnect Reversion Timeout Period. Timeout (reversion time is for the Disconnect only).", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DCTE.RvrtTms.AI61" - }, - { - "index": 62, - "description": "Maximum Generation Ramp Up Rate. The maximum generation ramp up rate expressed as a percentage of the Maximum Generation Rate (WMax) per second.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DCTE", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRteMax", - "name": "DCTE.RpuRteMax.AI62" - }, - { - "index": 63, - "description": "Maximum Generation Ramp Down Rate. The maximum generation ramp down rate expressed as a percentage of the Maximum Generation Rate (WMax) per second.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DCTE", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRteMax", - "name": "DCTE.RpdRteMax.AI63" - }, - { - "index": 64, - "description": "Maximum Charging Ramp Up Rate. The maximum charging ramp up rate expressed as a percentage of the Maximum Charging Rate (WChaMax) per second.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DCTE", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuChaRteMax", - "name": "DCTE.RpuChaRteMax.AI64" - }, - { - "index": 65, - "description": "Maximum Charging Ramp Down Rate. The maximum charging ramp down rate expressed as a percentage of the Maximum Charnging Rate (WChaMax) per second.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DCTE", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdChaRteMax", - "name": "DCTE.RpdChaRteMax.AI65" - }, - { - "index": 66, - "description": "Requested Settings Group.", - "data_type": "AI", - "common_data_class": "ENG", - "maximum": 255, - "ln_class": "DECP", - "units": "None (list)", - "minimum": 0, - "data_object": "EcpIsldSt", - "allowed_values": { - "0": "Not Used", - "1": "Unspecified / Autonomously Determined (see BO Enable Sensed Grid Config Detection)", - "2": "Factory Configuration", - "3": "Default Configuration / Comms Lost", - "4": "Normal Grid-Connected Configuration", - "5": "Islanded Condition 1 (small, local island)", - "6": "Islanded Condition 2 (larger, area island)", - "7": "Islanded Condition 3 (largest, regional island)", - "8": "1st Alternate Grid-Connected Configuration", - "9": "2nd Alternate Grid-Connected Configuration", - "10": "3rd Alternate Grid-Connected Configuration" - }, - "type": "enumerated", - "name": "DECP.EcpIsldSt.AI66" - }, - { - "index": 67, - "description": "Settings Group Being Edited.", - "data_type": "AI", - "common_data_class": "ENG", - "maximum": 255, - "ln_class": "DECP", - "units": "None (list)", - "minimum": 0, - "data_object": "EcpIsldSt", - "allowed_values": { - "0": "Not Used", - "1": "Unspecified / Autonomously Determined (see BO Enable Sensed Grid Config Detection)", - "2": "Factory Configuration", - "3": "Default Configuration / Comms Lost", - "4": "Normal Grid-Connected Configuration", - "5": "Islanded Condition 1 (small, local island)", - "6": "Islanded Condition 2 (larger, area island)", - "7": "Islanded Condition 3 (largest, regional island)", - "8": "1st Alternate Grid-Connected Configuration", - "9": "2nd Alternate Grid-Connected Configuration", - "10": "3rd Alternate Grid-Connected Configuration" - }, - "type": "enumerated", - "name": "DECP.EcpIsldSt.AI67" - }, - { - "index": 68, - "description": "Active Settings Group. Note this may differ from the Requested Settings Group or Settings Group Being Edited analog outputs depending on whether communications has been lost and how the Enable Sensed Grid Config Detection binary output is set.", - "data_type": "AI", - "common_data_class": "ENS", - "maximum": 255, - "ln_class": "DECP", - "units": "None (list)", - "minimum": 0, - "data_object": "EcpIsldSt", - "allowed_values": { - "0": "Not Used", - "1": "Unspecified / Autonomously Determined (see BO42)", - "2": "Factory Configuration", - "3": "Default Configuration / Comms Lost", - "4": "Normal Grid-Connected Configuration", - "5": "Islanded Condition 1 (small, local island)", - "6": "Islanded Condition 2 (larger, area island)", - "7": "Islanded Condition 3 (largest, regional island)", - "8": "1st Alternate Grid-Connected Configuration", - "9": "2nd Alternate Grid-Connected Configuration", - "10": "3rd Alternate Grid-Connected Configuration" - }, - "type": "enumerated", - "name": "DECP.EcpIsldSt.AI68" - }, - { - "index": 69, - "description": "Freeze Counter Interval. interval between freeze counter operations after the initial occurrence. A zero value means the free counter operation is not repeated.", - "data_type": "AI", - "minimum": 0, - "name": "AI69" - }, - { - "index": 70, - "description": "Freeze Counter Interval Units. Units of the interval between freeze counter operations.", - "data_type": "AI", - "maximum": 9, - "minimum": 0, - "units": "None (list)", - "allowed_values": { - "0": "The outstation does not repeat the action,regardless of the Interval count.", - "1": "Milliseconds - In this case the interval is always counted relative to the Start Time and is constant regardless of the clock time set at the Outstation.", - "2": "Seconds - At the same millisecond within the second that is specified in the Start Time.", - "3": "Minutes - At the same second and millisecond within the minute that is specified in the Start Time.", - "4": "Hours - At the same minute,second and B7millisecond within the hour that is specified in the Start Time.", - "5": "Days - At the same time of day that is specified in the Start Time.", - "6": "Weeks - On the same day of the week at the same time of day that is specified in the Start Time", - "7": "Months - On the same day of each month at the same time of day that is specified in the Start Time. If the Start Time falls on the 29th or greater day of the month,the outstation shall not perform the action in months that do not have such a day", - "8": "Months on Same Day of Week from Start of Month - At the same timeof day on the same day of the week after the beginning of the month as the day specified in the Start Time. For instance,if the Start Time specifies the second Tuesday of February and the Interval Count is 2,the next action shall occur on the second Tuesday of April. In the same example,if the Interval Count is set to 12,this is the same as specifying,Every year on the second Tuesday in February. If the specified day does not occur in a given month when an action was scheduled to occur,the outstation shall not perform the action that month but shall perform it at the next valid scheduled time.", - "9": "Months on Same Day of Week from End of Month - The outstation shall interpret this setting as in <8>,but the day of the week shall be measured from the end of the month,e.g.,the second-last Tuesday in February." - }, - "type": "enumerated", - "name": "AI70" - }, - { - "index": 71, - "description": "Low/High Voltage Ride-Through Signal Meter ID. Referenced ECP. This is the meter from which current is being read to evaluate and provide support.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DHVT", - "minimum": 0, - "data_object": "EcpRef", - "name": "DHVT.EcpRef.AI71" - }, - { - "index": 72, - "description": "Low/High Voltage Ride-Through Voltage Reference Input. Active voltage measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "MMXN", - "units": "Volts", - "minimum": 0, - "data_object": "Vol", - "name": "MMXN.Vol.AI72" - }, - { - "index": 73, - "description": "Low/High Voltage Ride-Through High Must Trip Curve Index. Index of the Voltage Ride-through curve which specifies trip points when the voltage is high.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "PTOV", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTOV.BlkRef.AI73" - }, - { - "index": 74, - "description": "Low/High Voltage Ride-Through Low Must Trip Curve Index. Index of the Voltage Ride-through curve which specifies trip points when the voltage is low.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "PTUV", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTUV.BlkRef.AI74" - }, - { - "index": 75, - "description": "Low/High Voltage Ride-Through High Momentary Cessation Curve Index. Index of the Voltage Ride-through curve which specifies where generation/discharging must stop when the voltage is high.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "PTOV", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTOV.BlkRef.AI75" - }, - { - "index": 76, - "description": "Low/High Voltage Ride-Through Low Momentary Cessation Curve Index. Index of the Voltage Ride-through curve which specifies where generation/discharging must stop when the voltage is low.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "PTUV", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTUV.BlkRef.AI76" - }, - { - "index": 77, - "description": "Low/High Frequency Ride-Through Signal Meter ID. Referenced ECP. This is the meter from which current is being read to evaluate and provide support.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DHFT", - "minimum": 0, - "data_object": "EcpRef", - "name": "DHFT.EcpRef.AI77" - }, - { - "index": 78, - "description": "Low/High Frequency Ride-Through Frequency Reference Input. Active frequency measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "Hz", - "name": "MMXU.Hz.AI78" - }, - { - "index": 79, - "description": "Low/High Frequency Ride-Through High Must Trip Curve Index. Index of the Frequency Ride-through curve which specifies trip points when the frequency is high.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "PTOF", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTOF.BlkRef.AI79" - }, - { - "index": 80, - "description": "Low/High Frequency Ride-Through Low Must Trip Curve Index. Index of the Frequency Ride-through curve which specifies trip points when the frequency is low.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "PTUF", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTUF.BlkRef.AI80" - }, - { - "index": 81, - "description": "Low/High Frequency Ride-Through High Momentary Cessation Curve Index. Index of the Frequency Ride-through curve which specifies where generation/discharging must stop when the frequency is high.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "PTOF", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTOF.BlkRef.AI81" - }, - { - "index": 82, - "description": "Low/High Frequency Ride-Through Low Momentary Cessation Curve Index. Index of the Frequency Ride-through curve which specifies where generation/discharging must stop when the frequency is low.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "PTUF", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTUF.BlkRef.AI82" - }, - { - "index": 83, - "description": "Dynamic Reactive Current Support Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DRGS", - "minimum": 0, - "data_object": "ModPrio", - "name": "DRGS.ModPrio.AI83" - }, - { - "index": 84, - "description": "Dynamic Reactive Current Support Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DRGS.WinTms.AI84" - }, - { - "index": 85, - "description": "Dynamic Reactive Current Support Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DRGS.RmpTms.AI85" - }, - { - "index": 86, - "description": "Dynamic Reactive Current Support Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DRGS.RvrtTms.AI86" - }, - { - "index": 87, - "description": "Dynamic Reactive Current Support Signal Meter ID. Referenced ECP. This is the meter from which current is being read to evaluate and provide support.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DRGS", - "minimum": 0, - "data_object": "EcpRef", - "name": "DRGS.EcpRef.AI87" - }, - { - "index": 88, - "description": "Dynamic Reactive Current Support Voltage Reference Input. Votltage measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "MMXN", - "units": "Volts", - "minimum": 0, - "data_object": "Vol", - "name": "MMXN2.Vol.AI88" - }, - { - "index": 89, - "description": "Dynamic Reactive Current Support Moving Average Voltage", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "DRGS", - "units": "Volts", - "minimum": 0, - "data_object": "VAv", - "name": "DRGS.VAv.AI89" - }, - { - "index": 90, - "description": "Dynamic Reactive Current Support Present Delta Voltage. Difference in Volts between the present measured Voltage and the Moving Average Voltage (RDGS.Vav) as a percentage of the reference voltage (VRef).", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "maximum": 10000, - "ln_class": "DRGS", - "units": "Percent", - "minimum": -10000, - "data_object": "DelV", - "name": "DRGS.DelV.AI90" - }, - { - "index": 91, - "description": "Dynamic Reactive Current Support - Gradient Mode.", - "data_type": "AI", - "common_data_class": "SPG", - "maximum": 2, - "ln_class": "DRGS", - "units": "None (list)", - "minimum": 0, - "data_object": "ArGraMod", - "allowed_values": { - "0": "Undefined", - "1": "Gradients reach 0 at the moving average Voltage", - "2": "Gradients reach 0 at the Voltage deadbands" - }, - "type": "enumerated", - "name": "DRGS.ArGraMod.AI91" - }, - { - "index": 92, - "description": "Dynamic Reactive Current Support Deadband Minimum Voltage. Percentage of the nominal voltage (DRCT.Vref), measured from the moving average voltage (RDGS.VAv). Support is no longer applied when the voltage stays above this value for the length of the Hold Time.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 0, - "ln_class": "DRGS", - "units": "Percent", - "minimum": -10000, - "data_object": "DbVMin", - "name": "DRGS.DbVMin.AI92" - }, - { - "index": 93, - "description": "Dynamic Reactive Current Support Deadband Maximum Voltage. Percentage of the nominal voltage (DRCT.Vref), measured from the moving average voltage (RDGS.VAv). Support is no longer applied when the voltage stays below this value for the length of the Hold Time.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 10000, - "ln_class": "DRGS", - "units": "Percent", - "minimum": 0, - "data_object": "DbVMax", - "name": "DRGS.DbVMax.AI93" - }, - { - "index": 94, - "description": "Dynamic Reactive Current Support Gradient for Sags. Percentage of the rated current (DRAT.ARtg) to apply capacitively per percentage of the negative deviation from the moving average voltage (RDGS.Av). It is a ratio of percent and is therefore unitless.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DRGS", - "units": "Percent current per percent voltage deviation", - "data_object": "ArGraSag", - "name": "DRGS.ArGraSag.AI94" - }, - { - "index": 95, - "description": "Dynamic Reactive Current Support Gradient for Swells. Percentage of the rated current (DRAT.ARtg) to apply inductively per percentage of the positive deviation from the moving average voltage (RDGS.Av). It is a ratio of percent and is therefore unitless.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DRGS", - "units": "Percent current per percent voltage deviation", - "data_object": "ArGraSwl", - "name": "DRGS.ArGraSwl.AI95" - }, - { - "index": 96, - "description": "Dynamic Reactive Current Support Filter Time for Moving Average Voltage (RDGS.VAv). Used to determine amount of dynamic reactive current support.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "Seconds", - "minimum": 0, - "data_object": "FilTms", - "name": "DRGS.FilTms.AI96" - }, - { - "index": 97, - "description": "Dynamic Reactive Current Support Block Zone Voltage. Percentage of the nominal voltage (DRCT.VRef) below which no reactive current support shall be applied.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DRGS", - "units": "Percent", - "minimum": 0, - "data_object": "BlkZnV", - "name": "DRGS.BlkZnV.AI97" - }, - { - "index": 98, - "description": "Dynamic Reactive Current Support Hysteresis Block Zone Voltage. Percentage of the nominal voltage (DRCT.VRef). After being blocked,reactive current support shall not resume until the voltage has been above BlkZnV + HysBlkZnV.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DRGS", - "units": "Percent", - "minimum": 0, - "data_object": "HysBlkZnV", - "name": "DRGS.HysBlkZnV.AI98" - }, - { - "index": 99, - "description": "Dynamic Reactive Current Support Block Zone Time. Time in milliseconds from the beginning of any \"sag\" event, before which dynamic reactive current support will always continue, regardless of how low voltage may sag.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "ms", - "minimum": 0, - "data_object": "BlkZnTmms", - "name": "DRGS.BlkZnTmms.AI99" - }, - { - "index": 100, - "description": "Dynamic Reactive Current Support Hold Time. When the voltage returns to within the deadband limits (RDGS.dbVMin annd RDGS.dbVMax) for this length of time (measured in milliseconds), the \"sag\" or \"swell\" event is considered to be over. Reactive current support ends, frozen values are unfrozen, and a new event can begin.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "ms", - "minimum": 0, - "data_object": "HoldTmms", - "name": "DRGS.HoldTmms.AI100" - }, - { - "index": 101, - "description": "Dynamic Reactive Current Attempted Output. Current output that the mode is attempting to achieve based on the Voltage input and other parameters.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "DRGS", - "units": "Amps", - "minimum": 0, - "data_object": "ReqA", - "name": "DRGS.ReqA.AI101" - }, - { - "index": 102, - "description": "Dynamic Volt-Watt Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWD", - "minimum": 0, - "data_object": "ModPrio", - "name": "DVWD.ModPrio.AI102" - }, - { - "index": 103, - "description": "Dynamic Volt-Watt Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWD", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DVWD.WinTms.AI103" - }, - { - "index": 104, - "description": "Dynamic Volt-Watt Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWD", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DVWD.RmpTms.AI104" - }, - { - "index": 105, - "description": "Dynamic Volt-Watt Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWD", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DVWD.RvrtTms.AI105" - }, - { - "index": 106, - "description": "Dynamic Volt-Watt Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DVWD", - "minimum": 0, - "data_object": "EcpRef", - "name": "DVWD2.EcpRef.AI106" - }, - { - "index": 107, - "description": "Dynamic Volt-Watt Voltage Reference Input. Votltage measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "MMXN", - "units": "Volts", - "minimum": 0, - "data_object": "Vol", - "name": "MMXN2.Vol.AI107" - }, - { - "index": 108, - "description": "Dynamic Volt-Watt Moving Average Voltage", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "DVWD", - "units": "Volts", - "minimum": 0, - "data_object": "VAv", - "name": "DVWD2.VAv.AI108" - }, - { - "index": 109, - "description": "Dynamic Volt-Watt Present Delta Voltage", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "DVWD", - "units": "Volts", - "minimum": 0, - "data_object": "DelV", - "name": "DVWD2.DelV.AI109" - }, - { - "index": 110, - "description": "Dynamic Volt-Watt Gradient. Signed quantity that establishes the ratio of additional Watts supplied (expressed in terms of % DRCT.WMax) to the present difference from the moving average voltage (expressed as % DRCT.VRef).", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DVWD", - "units": "Percent watts per percent voltage difference", - "data_object": "DynVWGra", - "name": "DVWD.DynVWGra.AI110" - }, - { - "index": 111, - "description": "Dynamic Volt-Watt Filter Time. The time in seconds used to calculate the moving average voltage for dynamic Volt-Watt support.", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DVWD", - "units": "Seconds", - "minimum": 0, - "data_object": "VWFilTms", - "name": "DVWD.VWFilTms.AI111" - }, - { - "index": 112, - "description": "Dynamic Volt-Watt Lower Deadband. Percentage of the nominal voltage (DRCT.Vref) measured below the moving average voltage. If the present voltage is above this value,no additional Watts shall be supplied.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 0, - "ln_class": "DVWD", - "units": "Percent", - "minimum": -1000, - "data_object": "DbVWLo", - "name": "DVWD.DbVWLo.AI112" - }, - { - "index": 113, - "description": "Dynamic Volt-Watt Upper Deadband. Percentage of the nominal voltage (DRCT.Vref) measured above the moving average voltage. If the present voltage is below this value, no additional Watts shall be supplied.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DVWD", - "units": "Percent", - "minimum": 0, - "data_object": "DbVWHi", - "name": "DVWD.DbVWHi.AI113" - }, - { - "index": 114, - "description": "Dynamic Volt-Watt Attempted Output. Watt output that the mode is attempting to achieve based on the Voltage input and other parameters.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DVWD", - "units": "Watts", - "data_object": "ReqWSet", - "name": "DVWD.ReqWSet.AI114" - }, - { - "index": 115, - "description": "Frequency-Watt Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "ModPrio", - "name": "DHFW2.ModPrio.AI115" - }, - { - "index": 116, - "description": "Frequency-Watt Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DHFW.WinTms.AI116" - }, - { - "index": 117, - "description": "Frequency-Watt Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DHFW.RmpTms.AI117" - }, - { - "index": 118, - "description": "Frequency-Watt Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DHFW.RvrtTms.AI118" - }, - { - "index": 119, - "description": "Frequency-Watt Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "EcpRef", - "name": "DHFW2.EcpRef.AI119" - }, - { - "index": 120, - "description": "Frequency-Watt Frequency Reference Input. Frequency measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "MMXU", - "units": "Hz", - "minimum": 0, - "data_object": "Hz", - "name": "MMXU2.Hz.AI120" - }, - { - "index": 121, - "description": "Frequency-Watt High Starting Frequency. Delta frequency between start frequency and nominal grid frequency for high frequency events.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DHFW", - "units": "Hz", - "minimum": 0, - "data_object": "HzStr", - "name": "DHFW2.HzStr.AI121" - }, - { - "index": 122, - "description": "Frequency-Watt High Stopping Frequency. Delta frequency between stop frequency and nominal grid frequency for high frequency events.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DHFW", - "units": "Hz", - "minimum": 0, - "data_object": "HzStop", - "name": "DHFW2.HzStop.AI122" - }, - { - "index": 123, - "description": "Frequency-Watt High Discharging/Generating Gradient", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DHFW", - "units": "Percent watts per percent frequency difference", - "data_object": "WGra", - "name": "DHFW.WGra.AI123" - }, - { - "index": 124, - "description": "Frequency-Watt High Charging Gradient", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DHFW", - "units": "Percent watts per percent frequency difference", - "data_object": "WChaGra", - "name": "DHFW.WChaGra.AI124" - }, - { - "index": 125, - "description": "Frequency-Watt Low Starting Frequency. Delta frequency between start frequency and nominal grid frequency for low frequency events.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 0, - "ln_class": "DLFW", - "units": "Hz", - "minimum": -70000, - "data_object": "HzStr", - "name": "DLFW2.HzStr.AI125" - }, - { - "index": 126, - "description": "Frequency-Watt Low Stopping Frequency. Delta frequency between stop frequency and nominal grid frequency for low frequency events.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 0, - "ln_class": "DLFW", - "units": "Hz", - "minimum": -70000, - "data_object": "HzStop", - "name": "DLFW2.HzStop.AI126" - }, - { - "index": 127, - "description": "Frequency-Watt Low Discharging/Generating Gradient", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DLFW", - "units": "Percent watts per percent frequency difference", - "data_object": "WGra", - "name": "DLFW.WGra.AI127" - }, - { - "index": 128, - "description": "Frequency-Watt Low Charging Gradient", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DLFW", - "units": "Percent watts per percent frequency difference", - "data_object": "WChaGra", - "name": "DLFW.WChaGra.AI128" - }, - { - "index": 129, - "description": "Frequency-Watt Start Delay", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "ActStrDlTmms", - "name": "DHFW2.ActStrDlTmms.AI129" - }, - { - "index": 130, - "description": "Frequency-Watt Stop Delay", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "ActStopDlTmms", - "name": "DHFW2.ActStopDlTmms.AI130" - }, - { - "index": 131, - "description": "Frequency-Watt Ramp Up Time Constant. Time constant or open loop response time for moving from the current active power target to a higher active power target.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DLFW", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DLFW.OpnLoopMax.AI131" - }, - { - "index": 132, - "description": "Frequency-Watt Ramp Down Time Constant. Time constant or open loop response time for moving from the current active power target to a lower active power target.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DHFW.OpnLoopMax.AI132" - }, - { - "index": 133, - "description": "Frequency-Watt Discharge Ramp Up Rate", - "data_type": "AI", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DHFW.RpuRte.AI133" - }, - { - "index": 134, - "description": "Frequency-Watt Discharge Ramp Down Rate", - "data_type": "AI", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRteMax", - "name": "DHFW.RpdRteMax.AI134" - }, - { - "index": 135, - "description": "Frequency-Watt Charge Ramp Up Rate", - "data_type": "AI", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuChaRte", - "name": "DHFW.RpuChaRte.AI135" - }, - { - "index": 136, - "description": "Frequency-Watt Charge Ramp Down Rate", - "data_type": "AI", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdChaRteMax", - "name": "DHFW.RpdChaRteMax.AI136" - }, - { - "index": 137, - "description": "Frequency-Watt High Return Gradient", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DHFW", - "units": "Percent watts per percent frequency difference", - "minimum": 0, - "data_object": "RtnRmpRte", - "name": "DHFW2.RtnRmpRte.AI137" - }, - { - "index": 138, - "description": "Frequency-Watt Low Return Gradient", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DLFW", - "units": "Percent watts per percent frequency difference", - "minimum": 0, - "data_object": "RtnRmpRte", - "name": "DLFW2.RtnRmpRte.AI138" - }, - { - "index": 139, - "description": "Frequency-Watt Attempted Output. Watt output that the mode is attempting to achieve based on the Frequency input and other parameters.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DHFW", - "units": "Watts", - "data_object": "ReqWLim", - "name": "DHFW.ReqWLim.AI139" - }, - { - "index": 140, - "description": "Frequency-Watt Minimum Usable SOC", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DAGC", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMinPct", - "name": "DAGC.SocUseMinPct.AI140" - }, - { - "index": 141, - "description": "Frequency-Watt Maximum Usable SOC", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DAGC", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMaxPct", - "name": "DAGC.SocUseMaxPct.AI141" - }, - { - "index": 142, - "description": "Active Power Limit Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWMX", - "minimum": 0, - "data_object": "ModPrio", - "name": "DWMX.ModPrio.AI142" - }, - { - "index": 143, - "description": "Active Power Limit Enabling Time Window. Time window (in seconds) within which to randomly execute a command. If the time window is zero, the command will be executed immediately.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWMX", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DWMX.WinTms.AI143" - }, - { - "index": 144, - "description": "Active Power Limit Enabling Ramp Time. Ramp time, in seconds, for moving from current operational mode settings to new operational mode settings.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWMX", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DWMX.RmpTms.AI144" - }, - { - "index": 145, - "description": "Active Power Limit Reversion Timeout Period. Reversion Timeout Period (in seconds), after which the device will revert to its default status, such as closing the switch to reconnect to the grid or allowing maximum watts output, in case communications are lost or mitigating messages are not received.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWMX", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DWMX.RvrtTms.AI145" - }, - { - "index": 146, - "description": "Active Power Limit Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DWMX", - "minimum": 0, - "data_object": "EcpRef", - "name": "DWMX.EcpRef.AI146" - }, - { - "index": 147, - "description": "Active Power Limit Reference Input. Active Power measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "data_object": "TotW", - "name": "MMXU.TotW.AI147" - }, - { - "index": 148, - "description": "Active Power Limit Charge Setpoint", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWMX", - "units": "Percent", - "minimum": 0, - "data_object": "WLimPct", - "name": "DWMX.WLimPct.AI148" - }, - { - "index": 149, - "description": "Active Power Limit Generation Setpoint", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWMN", - "units": "Percent", - "minimum": 0, - "data_object": "WLimPct", - "name": "DWMN.WLimPct.AI149" - }, - { - "index": 150, - "description": "Charge/Discharge Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWGC", - "minimum": 0, - "data_object": "ModPrio", - "name": "DWGC.ModPrio.AI150" - }, - { - "index": 151, - "description": "Charge/Discharge Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DWGC.WinTms.AI151" - }, - { - "index": 152, - "description": "Charge/Discharge Enabling Ramp Time. Ramp time, in seconds, for moving from current operational mode settings to new operational mode settings.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DWGC.RmpTms.AI152" - }, - { - "index": 153, - "description": "Charge/Discharge Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DWGC.RvrtTms.AI153" - }, - { - "index": 154, - "description": "Charge/Discharge Active Power Target. Percentage of maximum active power.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWGC", - "units": "Percent", - "minimum": -1000, - "data_object": "GnWPctSpt", - "name": "DWGC.GnWPctSpt.AI154" - }, - { - "index": 155, - "description": "Charge/Discharge Ramp Up Time Constant. Ramp time, in seconds, for moving from the current active power target to a higher active power target.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DWGC.OpnLoopMax.AI155" - }, - { - "index": 156, - "description": "Charge/Discharge Ramp Down Time Constant. Ramp time, in seconds, for moving from the current active power target to a lower active power target.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DWGC.OpnLoopMax.AI156" - }, - { - "index": 157, - "description": "Charge/Discharge Discharge Ramp Up Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DWGC.RpuRte.AI157" - }, - { - "index": 158, - "description": "Charge/Discharge Discharge Ramp Down Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRteMax", - "name": "DWGC.RpdRteMax.AI158" - }, - { - "index": 159, - "description": "Charge/Discharge Charge Ramp Up Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuChaRte", - "name": "DWGC.RpuChaRte.AI159" - }, - { - "index": 160, - "description": "Charge/Discharge Charge Ramp Down Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdChaRteMax", - "name": "DWGC.RpdChaRteMax.AI160" - }, - { - "index": 161, - "description": "Charge/Discharge Minimum Reserve for Storage. The reserve level below which the storage system may be only be discharged in emergency situations, expressed as a percentage of the usable capacity.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWGC", - "units": "Percent", - "minimum": -1000, - "data_object": "SocUseMinPct", - "name": "DWGC.SocUseMinPct.AI161" - }, - { - "index": 162, - "description": "Charge/Discharge Maximum Reserve for Storage. The reserve level above which the storage system may be only be charged in emergency situations, expressed as a percentage of the usable capacity.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWGC", - "units": "Percent", - "minimum": -1000, - "data_object": "SocUseMaxPct", - "name": "DWGC.SocUseMaxPct.AI162" - }, - { - "index": 163, - "description": "Coordinated Charge/Discharge Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "minimum": 0, - "data_object": "ModPrio", - "name": "DTCD.ModPrio.AI163" - }, - { - "index": 164, - "description": "Coordinated Charge/Discharge Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DTCD.WinTms.AI164" - }, - { - "index": 165, - "description": "Coordinated Charge/Discharge Enabling Ramp Time. Ramp time, in seconds, for moving from current operational mode settings to new operational mode settings", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DTCD.RmpTms.AI165" - }, - { - "index": 166, - "description": "Coordinated Charge/Discharge Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DTCD.RvrtTms.AI166" - }, - { - "index": 167, - "description": "Coordinated Charge/Discharge Target State of Charge. Charge that the system is expected to achieve, as a percentage of the usable capacity.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DTCD", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseTgtPct", - "name": "DTCD.SocUseTgtPct.AI167" - }, - { - "index": 168, - "description": "Coordinated Charge/Discharge Target Date. Date by which the storage system must reach the target SOC. Days since January 1, 1970, UTC.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Days", - "minimum": 0, - "data_object": "DateTgt", - "name": "DTCD.DateTgt.AI168" - }, - { - "index": 169, - "description": "Coordinated Charge/Discharge Target Time. Time by when the storage system must reach the target SOC. Expressed as the number of seconds since the start of Target Date.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Milliseconds", - "minimum": 0, - "data_object": "DateTgtTms", - "name": "DTCD.DateTgtTms.AI169" - }, - { - "index": 170, - "description": "Coordinated Charge/Discharge Energy Request. Amount of energy that must be transferred from the grid to the charger to move the SOC from the value at the specific time of reference to the target SOC.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Watt-hours", - "minimum": 0, - "data_object": "SocWReq", - "name": "DTCD.SocWReq.AI170" - }, - { - "index": 171, - "description": "Coordinated Charge/Discharge Minimum Charging Duration. Minimum duration to move from the SOC at the time of reference to the target SOC.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "ChaDurTms", - "name": "DTCD.ChaDurTms.AI171" - }, - { - "index": 172, - "description": "Coordinated Charge/Discharge Date of Reference. Date that the SOC is measured or computed by the storage system and is the basis for the Energy Request, Minimum Charging Duration, and other parameters.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Days", - "minimum": 0, - "data_object": "DateTgt", - "name": "DTCD.DateTgt.AI172" - }, - { - "index": 173, - "description": "Coordinated Charge/Discharge Time of Reference. Time that the SOC is measured or computed by the storage system and is the basis for the Energy Request, Minimum Charging Duration, and other parameters.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Milliseconds", - "minimum": 0, - "data_object": "SocDateTms", - "name": "DTCD.SocDateTms.AI173" - }, - { - "index": 174, - "description": "Coordinated Charge/Discharge Duration at Maximum Charge Rate. Duration that energy can be stored at the Maximum Charge Rate.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "ChaDurMax", - "name": "DTCD.ChaDurMax.AI174" - }, - { - "index": 175, - "description": "Coordinated Charge/Discharge Duration Maximum Discharge Rate. Duration that energy can be delivered at the Maximum Discharge Rate.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "DschDurMax", - "name": "DTCD.DschDurMax.AI175" - }, - { - "index": 176, - "description": "Active Power Response Mode #1 Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPKP", - "minimum": 0, - "data_object": "ModPrio", - "name": "DPKP.ModPrio.AI176" - }, - { - "index": 177, - "description": "Active Power Response Mode #1 Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPKP", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DPKP.WinTms.AI177" - }, - { - "index": 178, - "description": "Active Power Response Mode #1 Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPKP", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DPKP.RmpTms.AI178" - }, - { - "index": 179, - "description": "Active Power Response Mode #1 Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPKP", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DPKP.RvrtTms.AI179" - }, - { - "index": 180, - "description": "Active Power Response Mode #1 Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DPKP", - "minimum": 0, - "data_object": "EcpRef", - "name": "DPKP.EcpRef.AI180" - }, - { - "index": 181, - "description": "Active Power Response Mode #1 Reference Power Measured", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "data_object": "TotW", - "name": "MMXU.TotW.AI181" - }, - { - "index": 182, - "description": "Active Power Response Mode #1 Power Threshold", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DPKP", - "units": "Watts", - "data_object": "PkPwrWLim", - "name": "DPKP.PkPwrWLim.AI182" - }, - { - "index": 183, - "description": "Active Power Response Mode #1 Ratio", - "data_type": "AI", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DPKP", - "units": "Percent", - "minimum": 0, - "data_object": "PkPwrFolPct", - "name": "DPKP.PkPwrFolPct.AI183" - }, - { - "index": 184, - "description": "Active Power Response Mode #1 Ramp Up Rate. Maximum ramp up rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DPKP", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DPKP.RpuRte.AI184" - }, - { - "index": 185, - "description": "Active Power Response Mode #1 Ramp Down Rate. Maximum ramp down rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DPKP", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DPKP.RpdRte.AI185" - }, - { - "index": 186, - "description": "Active Power Response Mode #1 Attempted Output. Watt output that the mode is attempting to achieve based on the Watts input and other parameters.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DPKP", - "units": "Watts", - "data_object": "ReqWSet", - "name": "DPKP.ReqWSet.AI186" - }, - { - "index": 187, - "description": "Active Power Response Mode #2 Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DGFL", - "minimum": 0, - "data_object": "ModPrio", - "name": "DGFL.ModPrio.AI187" - }, - { - "index": 188, - "description": "Active Power Response Mode #2 Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DGFL", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DGFL.WinTms.AI188" - }, - { - "index": 189, - "description": "Active Power Response Mode #2 Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DGFL", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DGFL.RmpTms.AI189" - }, - { - "index": 190, - "description": "Active Power Response Mode #2 Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DGFL", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DGFL.RvrtTms.AI190" - }, - { - "index": 191, - "description": "Active Power Response Mode #2 Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DGFL", - "minimum": 0, - "data_object": "EcpRef", - "name": "DGFL.EcpRef.AI191" - }, - { - "index": 192, - "description": "Active Power Response Mode #2 Reference Power Measured", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "data_object": "TotW", - "name": "MMXU.TotW.AI192" - }, - { - "index": 193, - "description": "Active Power Response Mode #2 Power Threshold", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGFL", - "units": "Watts", - "data_object": "PkPwrWLim", - "name": "DGFL.PkPwrWLim.AI193" - }, - { - "index": 194, - "description": "Active Power Response Mode #2 Ratio", - "data_type": "AI", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DGFL", - "units": "Percent", - "minimum": 0, - "data_object": "PkPwrFolPct", - "name": "DGFL.PkPwrFolPct.AI194" - }, - { - "index": 195, - "description": "Active Power Response Mode #2 Ramp Up Rate. Maximum ramp up rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DGFL", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DGFL.RpuRte.AI195" - }, - { - "index": 196, - "description": "Active Power Response Mode #2 Ramp Down Rate. Maximum ramp down rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DGFL", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DGFL.RpdRte.AI196" - }, - { - "index": 197, - "description": "Active Power Response Mode #2 Attempted Output. Watt output that the mode is attempting to achieve based on the Watts input and other parameters.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DGFL", - "units": "Watts", - "data_object": "ReqWSet", - "name": "DGFL.ReqWSet.AI197" - }, - { - "index": 198, - "description": "Active Power Response Mode #3 Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DLFL", - "minimum": 0, - "data_object": "ModPrio", - "name": "DLFL.ModPrio.AI198" - }, - { - "index": 199, - "description": "Active Power Response Mode #3 Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DLFL", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DLFL.WinTms.AI199" - }, - { - "index": 200, - "description": "Active Power Response Mode #3 Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DLFL", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DLFL.RmpTms.AI200" - }, - { - "index": 201, - "description": "Active Power Response Mode #3 Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DLFL", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DLFL.RvrtTms.AI201" - }, - { - "index": 202, - "description": "Active Power Response Mode #3 Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DLFL", - "minimum": 0, - "data_object": "EcpRef", - "name": "DLFL.EcpRef.AI202" - }, - { - "index": 203, - "description": "Active Power Response Mode #3 Reference Power Measured", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "data_object": "TotW", - "name": "MMXU.TotW.AI203" - }, - { - "index": 204, - "description": "Active Power Response Mode #3 Power Threshold", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DLFL", - "units": "Watts", - "data_object": "PkPwrWLim", - "name": "DLFL.PkPwrWLim.AI204" - }, - { - "index": 205, - "description": "Active Power Response Mode #3 Ratio", - "data_type": "AI", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DLFL", - "units": "Percent", - "minimum": 0, - "data_object": "PkPwrFolPct", - "name": "DLFL.PkPwrFolPct.AI205" - }, - { - "index": 206, - "description": "Active Power Response Mode #3 Ramp Up Rate. Maximum ramp up rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DLFL", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DLFL.RpuRte.AI206" - }, - { - "index": 207, - "description": "Active Power Response Mode #3 Ramp Down Rate. Maximum ramp down rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DLFL", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DLFL.RpdRte.AI207" - }, - { - "index": 208, - "description": "Active Power Response Mode #3 Attempted Output. Watt output that the mode is attempting to achieve based on the Watts input and other parameters.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DLFL", - "units": "Watts", - "data_object": "ReqWSet", - "name": "DLFL.ReqWSet.AI208" - }, - { - "index": 209, - "description": "AGC Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DAGC", - "minimum": 0, - "data_object": "ModPrio", - "name": "DAGC.ModPrio.AI209" - }, - { - "index": 210, - "description": "AGC Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DAGC.WinTms.AI210" - }, - { - "index": 211, - "description": "AGC Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DAGC.RmpTms.AI211" - }, - { - "index": 212, - "description": "AGC Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DAGC.RvrtTms.AI212" - }, - { - "index": 213, - "description": "AGC Active Power Target", - "data_type": "AI", - "common_data_class": "APC", - "ln_class": "DAGC", - "units": "Watts", - "data_object": "GnWSpt", - "name": "DAGC.GnWSpt.AI213" - }, - { - "index": 214, - "description": "AGC Ramp Up Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpUpTms", - "name": "DAGC.RmpUpTms.AI214" - }, - { - "index": 215, - "description": "AGC Ramp Down Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpDnTms", - "name": "DAGC.RmpDnTms.AI215" - }, - { - "index": 216, - "description": "AGC Discharge Ramp Up Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DAGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DAGC.RpuRte.AI216" - }, - { - "index": 217, - "description": "AGC Discharge Ramp Down Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DAGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DAGC.RpdRte.AI217" - }, - { - "index": 218, - "description": "AGC Charge Ramp Up Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DAGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuChaRte", - "name": "DAGC.RpuChaRte.AI218" - }, - { - "index": 219, - "description": "AGC Charge Ramp Down Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DAGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdChaRte", - "name": "DAGC.RpdChaRte.AI219" - }, - { - "index": 220, - "description": "AGC Minimum Usable SOC", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DAGC", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMinPct", - "name": "DAGC.SocUseMinPct.AI220" - }, - { - "index": 221, - "description": "AGC Maximum Usable SOC", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DAGC", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMaxPct", - "name": "DAGC.SocUseMaxPct.AI221" - }, - { - "index": 222, - "description": "AGC Maximum Watts Available", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DAGC", - "units": "Watts", - "data_object": "WMaxAvl", - "name": "DAGC.WMaxAvl.AI222" - }, - { - "index": 223, - "description": "AGC Minimum Watts Available", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DAGC", - "units": "Watts", - "data_object": "WMinAvl", - "name": "DAGC.WMinAvl.AI223" - }, - { - "index": 224, - "description": "AGC Expected State of Charge", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DAGC", - "units": "Percent", - "minimum": 0, - "data_object": "SocExpc", - "name": "DAGC.SocExpc.AI224" - }, - { - "index": 225, - "description": "AGC Expected State of Energy", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DAGC", - "units": "Percent", - "minimum": 0, - "data_object": "SoeExpc", - "name": "DAGC.SoeExpc.AI225" - }, - { - "index": 226, - "description": "AGC Expected State of Charge Time Interval", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "SocExpcTms", - "name": "DAGC.SocExpcTms.AI226" - }, - { - "index": 227, - "description": "Active Power Smoothing Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWSM", - "minimum": 0, - "data_object": "ModPrio", - "name": "DWSM.ModPrio.AI227" - }, - { - "index": 228, - "description": "Active Power Smoothing Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWSM", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DWSM.WinTms.AI228" - }, - { - "index": 229, - "description": "Active Power Smoothing Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWSM", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DWSM.RmpTms.AI229" - }, - { - "index": 230, - "description": "Active Power Smoothing Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWSM", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DWSM.RvrtTms.AI230" - }, - { - "index": 231, - "description": "Active Power Smoothing Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DWSM", - "minimum": 0, - "data_object": "EcpRef", - "name": "DWSM.EcpRef.AI231" - }, - { - "index": 232, - "description": "Active Power Smoothing Reference Power Input. Active Power measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "minimum": 0, - "data_object": "TotW", - "name": "MMXU.TotW.AI232" - }, - { - "index": 233, - "description": "Active Power Smoothing Gradient. Signed quantity that establishes the ratio of additional smoothing Watts provided to the present delta-watts of the reference load or generation. Delta Watts is the difference between the moving average and the present value of the reference power. Positive values of this gradient are for following load (increased reference load results in a dynamic increase in DER output), and negative values are for following generation (increased reference generation results in a dynamic decrease in DER output).", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DWSM", - "units": "Watts per Delta-watt", - "data_object": "WSmthGra", - "name": "DWSM.WSmthGra.AI233" - }, - { - "index": 234, - "description": "Active Power Smoothing Lower Limit. Difference in Watts from the moving average of the reference power above which no smoothing shall be applied.", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DWSM", - "units": "Watts", - "data_object": "WSmthLoLim", - "name": "DWSM.WSmthLoLim.AI234" - }, - { - "index": 235, - "description": "Active Power Smoothing Upper Limit. Difference in Watts from the moving average of the reference power below which no smoothing shall be applied.", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DWSM", - "units": "Watts", - "minimum": 0, - "data_object": "WSmthHiLim", - "name": "DWSM.WSmthHiLim.AI235" - }, - { - "index": 236, - "description": "Active Power Smoothing Filter Time (Seconds). Time in seconds used to calculate the moving average of the reference load or generation being smoothed.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWSM", - "units": "Seconds", - "minimum": 0, - "data_object": "FilTms", - "name": "DWSM.FilTms.AI236" - }, - { - "index": 237, - "description": "Active Power Smoothing Discharge Ramp Up Rate. The maximum generation ramp up rate expressed as a percentage of the Maximum Generation Rate (WMax) per second.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWSM", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DWSM.RpuRte.AI237" - }, - { - "index": 238, - "description": "Active Power Smoothing Discharge Ramp Down Rate. The maximum generation ramp down rate expressed as a percentage of the Maximum Generation Rate (WMax) per second.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWSM", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DWSM.RpdRte.AI238" - }, - { - "index": 239, - "description": "Active Power Smoothing Charge Ramp Up Rate. The maximum charging ramp up rate expressed as a percentage of the Maximum Charging Rate (WChaMax) per second.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWSM", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuChaRte", - "name": "DWSM.RpuChaRte.AI239" - }, - { - "index": 240, - "description": "Active Power Smoothing Charge Ramp Down Rate. The maximum charging ramp down rate expressed as a percentage of the Maximum Charnging Rate (WChaMax) per second.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWSM", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdChaRte", - "name": "DWSM.RpdChaRte.AI240" - }, - { - "index": 241, - "description": "Active Power Smoothing Attempted Output. Watt output that the mode is attempting to achieve based on the Watt input and other parameters.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DWSM", - "units": "Watts", - "data_object": "ReqWSet", - "name": "DWSM.ReqWSet.AI241" - }, - { - "index": 242, - "description": "Volt-Watt Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWC", - "minimum": 0, - "data_object": "ModPrio", - "name": "DVWC.ModPrio.AI242" - }, - { - "index": 243, - "description": "Volt-Watt Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DVWC.WinTms.AI243" - }, - { - "index": 244, - "description": "Volt-Watt Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DVWC.RmpTms.AI244" - }, - { - "index": 245, - "description": "Volt-Watt Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DVWC.RvrtTms.AI245" - }, - { - "index": 246, - "description": "Volt-Watt Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DVWC", - "minimum": 0, - "data_object": "EcpRef", - "name": "DVWC.EcpRef.AI246" - }, - { - "index": 247, - "description": "Volt-Watt Reference Voltage Input. Voltage measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "MMXN", - "units": "Volts", - "minimum": 0, - "data_object": "Vol", - "name": "MMXN.Vol.AI247" - }, - { - "index": 248, - "description": "Volt-Watt Curve Index. Index of the Volt-Watt curve that should be used by the mode.", - "data_type": "AI", - "common_data_class": "CSG", - "ln_class": "DVWC", - "minimum": 0, - "data_object": "VWCrv", - "name": "DVWC.VWCrv.AI248" - }, - { - "index": 249, - "description": "Volt-Watt Attempted Output. Maximum active power the outstation will attempt to generate or absorb based on the Voltage input and selected curve.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DVWC", - "units": "Watts", - "data_object": "ReqWLim", - "name": "DVWC.ReqWLim.AI249" - }, - { - "index": 250, - "description": "Volt-Watt Filter Time (Seconds)", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "FilTms", - "name": "DVWC.FilTms.AI250" - }, - { - "index": 251, - "description": "Volt-Watt Ramp Up Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVWC.OpnLoopMax.AI251" - }, - { - "index": 252, - "description": "Volt-Watt Ramp Down Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVWC.OpnLoopMax.AI252" - }, - { - "index": 253, - "description": "Volt-Watt Discharging Ramp Up Rate. Maximum ramp up rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DVWC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DVWC.RpuRte.AI253" - }, - { - "index": 254, - "description": "Volt-Watt Discharging Ramp Down Rate. Maximum ramp down rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DVWC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DVWC.RpdRte.AI254" - }, - { - "index": 255, - "description": "Volt-Watt Charging Ramp Up Rate. Maximum charging ramp up rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DVWC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuChaRte", - "name": "DVWC.RpuChaRte.AI255" - }, - { - "index": 256, - "description": "Volt-Watt Charging Ramp Down Rate. Maximum charging ramp down rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DVWC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdChaRte", - "name": "DVWC.RpdChaRte.AI256" - }, - { - "index": 257, - "description": "Frequency-Watt Curve Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "ModPrio", - "name": "DHFW.ModPrio.AI257" - }, - { - "index": 258, - "description": "Frequency-Watt Curve Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DHFW.WinTms.AI258" - }, - { - "index": 259, - "description": "Frequency-Watt Curve Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DHFW.RmpTms.AI259" - }, - { - "index": 260, - "description": "Frequency-Watt Curve Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DHFW.RvrtTms.AI260" - }, - { - "index": 261, - "description": "Frequency-Watt Curve Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "EcpRef", - "name": "DHFW.EcpRef.AI261" - }, - { - "index": 262, - "description": "Frequency-Watt Curve Frequency Reference Input. Frequency measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "MMXU", - "units": "Hz", - "minimum": 0, - "data_object": "Hz", - "name": "MMXU.Hz.AI262" - }, - { - "index": 263, - "description": "Frequency-Watt Curve - Curve Index. Index of the Frequency-Watt curve that should be used by the mode.", - "data_type": "AI", - "common_data_class": "CSG", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "HzWCrv", - "name": "DHFW.HzWCrv.AI263" - }, - { - "index": 264, - "description": "Frequency-Watt Curve - High Frequency Hysteresis Curve Index", - "data_type": "AI", - "common_data_class": "CSG", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "HysCrv", - "name": "DHFW.HysCrv.AI264" - }, - { - "index": 265, - "description": "Frequency-Watt Curve - Low Frequency Hysteresis Curve Index", - "data_type": "AI", - "common_data_class": "CSG", - "ln_class": "DLFW", - "minimum": 0, - "data_object": "HysCrv", - "name": "DLFW.HysCrv.AI265" - }, - { - "index": 266, - "description": "Frequency-Watt Curve Start Delay", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "ActStrDlTmms", - "name": "DHFW.ActStrDlTmms.AI266" - }, - { - "index": 267, - "description": "Frequency-Watt Curve Stop Delay", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "ActStopDlTmms", - "name": "DHFW.ActStopDlTmms.AI267" - }, - { - "index": 268, - "description": "Frequency-Watt Curve Ramp Up Time Constant. Time constant or open loop response time for moving from the current active power target to a higher active power target.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DHFW.OpnLoopMax.AI268" - }, - { - "index": 269, - "description": "Frequency-Watt Curve Ramp Down Time Constant. Time constant or open loop response time for moving from the current active power target to a lower active power target.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DHFW.OpnLoopMax.AI269" - }, - { - "index": 270, - "description": "Frequency-Watt Curve Discharge Ramp Up Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DHFW.RpuRte.AI270" - }, - { - "index": 271, - "description": "Frequency-Watt Curve Discharge Ramp Down Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DHFW.RpdRte.AI271" - }, - { - "index": 272, - "description": "Frequency-Watt Curve Charge Ramp Up Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuChaRte", - "name": "DHFW.RpuChaRte.AI272" - }, - { - "index": 273, - "description": "Frequency-Watt Curve Charge Ramp Down Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdChaRte", - "name": "DHFW.RpdChaRte.AI273" - }, - { - "index": 274, - "description": "Frequency-Watt Attempted Output. Watt output that the mode is attempting to achieve based on the Frequency input and selected curve. If Snapshot of Power is not enabled,this is the maximum active power the outstation will attempt to generate or absorb.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DHFW", - "units": "Watts", - "data_object": "ReqWLim", - "name": "DHFW.ReqWLim.AI274" - }, - { - "index": 275, - "description": "Frequency-Watt Curve Minimum Usable SOC", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DHFW", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMinPct", - "name": "DHFW.SocUseMinPct.AI275" - }, - { - "index": 276, - "description": "Frequency-Watt Curve Maximum Usable SOC", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DHFW", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMaxPct", - "name": "DHFW.SocUseMaxPct.AI276" - }, - { - "index": 277, - "description": "Constant VArs Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVAR", - "minimum": 0, - "data_object": "ModPrio", - "name": "DVAR.ModPrio.AI277" - }, - { - "index": 278, - "description": "Constant VArs Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DVAR.WinTms.AI278" - }, - { - "index": 279, - "description": "Constant VArs Enabling Ramp Time. Ramp time, in seconds, for moving from current operational mode settings to new operational mode settings.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DVAR.RmpTms.AI279" - }, - { - "index": 280, - "description": "Constant VArs Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DVAR.RvrtTms.AI280" - }, - { - "index": 281, - "description": "Constant VArs Reactive Power Target. Percentage of maximum reactive power.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DVAR", - "units": "Percent", - "minimum": -1000, - "data_object": "VArTgtPct", - "name": "DVAR.VArTgtPct.AI281" - }, - { - "index": 282, - "description": "Constant VArs Ramp Up Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVAR.OpnLoopMax.AI282" - }, - { - "index": 283, - "description": "Constant VArs Ramp Down Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVAR.OpnLoopMax.AI283" - }, - { - "index": 284, - "description": "Fixed Power Factor Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DFPF", - "minimum": 0, - "data_object": "ModPrio", - "name": "DFPF.ModPrio.AI284" - }, - { - "index": 285, - "description": "Fixed Power Factor Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DFPF", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DFPF.WinTms.AI285" - }, - { - "index": 286, - "description": "Fixed Power Factor Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DFPF", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DFPF.RmpTms.AI286" - }, - { - "index": 287, - "description": "Fixed Power Factor Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DFPF", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DFPF.RvrtTms.AI287" - }, - { - "index": 288, - "description": "Fixed Power Factor Setpoint - Generation/Discharging", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DFPF", - "units": "None", - "minimum": 0, - "data_object": "PFGnTgt", - "name": "DFPF.PFGnTgt.AI288" - }, - { - "index": 289, - "description": "Fixed Power Factor Setpoint - Charging", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DFPF", - "units": "None", - "minimum": 0, - "data_object": "PFLodTgt", - "name": "DFPF.PFLodTgt.AI289" - }, - { - "index": 290, - "description": "Volt-Var Control Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVVR", - "minimum": 0, - "data_object": "ModPrio", - "name": "DVVR.ModPrio.AI290" - }, - { - "index": 291, - "description": "Volt-VAr Control Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DVVR.WinTms.AI291" - }, - { - "index": 292, - "description": "Volt-VAr Control Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DVVR.RmpTms.AI292" - }, - { - "index": 293, - "description": "Volt-VAr Control Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DVVR.RvrtTms.AI293" - }, - { - "index": 294, - "description": "Volt-VAr Control Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DVVR", - "minimum": 0, - "data_object": "EcpRef", - "name": "DVVR.EcpRef.AI294" - }, - { - "index": 295, - "description": "Volt-VAr Control Voltage Input. Voltage measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "MMXN", - "units": "Volts", - "minimum": 0, - "data_object": "Vol", - "name": "MMXN.Vol.AI295" - }, - { - "index": 296, - "description": "Volt-VAr Control Adjusted Voltage Reference. The Voltage used as reference for Volt-VAr control. If Autonomous Voltage Reference Adjustment is disabled,this is the same fixed value as the Reference Voltage.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "DVVR", - "units": "Volts", - "minimum": 0, - "data_object": "VRefSet", - "name": "DVVR.VRefSet.AI296" - }, - { - "index": 297, - "description": "Volt-VAr Curve Index. Index of the Volt-VAr curve that should be used by the mode.", - "data_type": "AI", - "common_data_class": "CSG", - "ln_class": "DVVR", - "minimum": 0, - "data_object": "VVArCrv", - "name": "DVVR.VVArCrv.AI297" - }, - { - "index": 298, - "description": "Volt-VAr Ramp Up Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVVR.OpnLoopMax.AI298" - }, - { - "index": 299, - "description": "Volt-VAr Ramp Down Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVVR.OpnLoopMax.AI299" - }, - { - "index": 300, - "description": "Volt-VAr Autonomous Voltage Reference Adjustment Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "VRefTmms", - "name": "DVVR.VRefTmms.AI300" - }, - { - "index": 301, - "description": "Volt-VAr Attempted Output. VAr output that the mode is attempting to achieve based on the Voltage input and selected curve.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DVVR", - "units": "VARs", - "data_object": "ReqVAr", - "name": "DVVR.ReqVAr.AI301" - }, - { - "index": 302, - "description": "Watt-VAr Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWVR", - "minimum": 0, - "data_object": "ModPrio", - "name": "DWVR.ModPrio.AI302" - }, - { - "index": 303, - "description": "Watt-VAr Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DWVR.WinTms.AI303" - }, - { - "index": 304, - "description": "Watt-VAr Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DWVR.RmpTms.AI304" - }, - { - "index": 305, - "description": "Watt-VAr Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DWVR.RvrtTms.AI305" - }, - { - "index": 306, - "description": "Watt-VAr Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DWVR", - "minimum": 0, - "data_object": "EcpRef", - "name": "DWVR.EcpRef.AI306" - }, - { - "index": 307, - "description": "Watt-VAr Reference Power Input. Power measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "minimum": 0, - "data_object": "TotW", - "name": "MMXU.TotW.AI307" - }, - { - "index": 308, - "description": "Watt-VAr Curve Index. Index of the Watt-VAr curve that should be used by the mode.", - "data_type": "AI", - "common_data_class": "CSG", - "ln_class": "DWVR", - "minimum": 0, - "data_object": "WVArCrv", - "name": "DWVR.WVArCrv.AI308" - }, - { - "index": 309, - "description": "Watt-VAr Ramp Up Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DWVR.OpnLoopMax.AI309" - }, - { - "index": 310, - "description": "Watt-VAr Ramp Down Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DWVR.OpnLoopMax.AI310" - }, - { - "index": 311, - "description": "Watt-VAr Attempted Output. VAr output that the mode is attempting to achieve based on the Watt input and selected curve.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DWVR", - "units": "VARs", - "data_object": "ReqVAr", - "name": "DWVR.ReqVAr.AI311" - }, - { - "index": 312, - "description": "Power Factor Correction Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPFC", - "minimum": 0, - "data_object": "ModPrio", - "name": "DPFC.ModPrio.AI312" - }, - { - "index": 313, - "description": "Power Factor Correction Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPFC", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DPFC.WinTms.AI313" - }, - { - "index": 314, - "description": "Power Factor Correction Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DPFC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DPFC.RmpTms.AI314" - }, - { - "index": 315, - "description": "Power Factor Correction Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPFC", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DPFC.RvrtTms.AI315" - }, - { - "index": 316, - "description": "Power Factor Correction Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DPFC", - "minimum": 0, - "data_object": "EcpRef", - "name": "DPFC.EcpRef.AI316" - }, - { - "index": 317, - "description": "Power Factor Correction Reference Power Factor Input. Power factor measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "MMXU", - "units": "None", - "minimum": -100, - "data_object": "TotPF", - "name": "MMXU.TotPF.AI317" - }, - { - "index": 318, - "description": "Power Factor Correction Average PF Target", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DPFC", - "units": "None", - "minimum": -100, - "data_object": "PFTrg", - "name": "DPFC.PFTrg.AI318" - }, - { - "index": 319, - "description": "Power Factor Correction Lower PF Limit", - "data_type": "AI", - "common_data_class": "Int", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DPFC", - "units": "None", - "minimum": -100, - "data_object": "PFCorRef.rangeC", - "name": "DPFC.PFCorRef.rangeC.AI319" - }, - { - "index": 320, - "description": "Power Factor Correction Upper PF Limit", - "data_type": "AI", - "common_data_class": "Int", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DPFC", - "units": "None", - "minimum": -100, - "data_object": "PFCorRef.rangeC", - "name": "DPFC.PFCorRef.rangeC.AI320" - }, - { - "index": 321, - "description": "Pricing Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPRG", - "minimum": 0, - "data_object": "ModPrio", - "name": "DPRG.ModPrio.AI321" - }, - { - "index": 322, - "description": "Pricing Mode Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DPRG.WinTms.AI322" - }, - { - "index": 323, - "description": "Pricing Mode Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DPRG.RmpTms.AI323" - }, - { - "index": 324, - "description": "Pricing Mode Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DPRG.RvrtTms.AI324" - }, - { - "index": 325, - "description": "Pricing Mode Setpoint: Hundredths of local currency per Kilowatt-Hr.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.01, - "ln_class": "DPRG", - "units": "100ths of local currency", - "data_object": "PrcRef", - "name": "DPRG.PrcRef.AI325" - }, - { - "index": 326, - "description": "Pricing Mode Ramp Up Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DPRG.OpnLoopMax.AI326" - }, - { - "index": 327, - "description": "Pricing Mode Ramp Down Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DPRG.OpnLoopMax.AI327" - }, - { - "index": 328, - "description": "Curve Edit Selector Index of the curve which is currently being viewed and/or changed", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DGSM", - "minimum": 1, - "data_object": "InCrv", - "name": "DGSMn.InCrv.AI328", - "type": "selector_block", - "selector_block_start": 328, - "selector_block_end": 532 - - }, - { - "index": 329, - "description": "Curve Mode Type", - "data_type": "AI", - "common_data_class": "ENG", - "maximum": 20, - "ln_class": "DGSM", - "units": "None (list)", - "minimum": 0, - "data_object": "ModTyp", - "allowed_values": { - "0": "Curve is not defined", - "1": "None, dimensionless", - "2": "Volt-Var modes VV11-VV12", - "3": "Frequency-Watt mode FW22", - "4": "Watt-VAr mode WP42", - "5": "Voltage-Watt modes VW51-VW52", - "6": "Remain Connected", - "7": "Temperature mode", - "8": "Pricing signal mode", - "9": "HVRT Must Trip", - "10": "HVRT Momentary Cessation", - "11": "LVRT Must Trip", - "12": "LVRT Momentary Cessation", - "13": "HFRT Must Trip", - "14": "HFRT Momentary Cessation", - "15": "LFRT Must Trip", - "16": "LFRT Momentary Cessation" - }, - "type": "enumerated", - "name": "DGSMn.ModTyp.AI329" - }, - { - "index": 330, - "description": "Curve Number of Points", - "data_type": "AI", - "common_data_class": "CSG", - "maximum": 100, - "ln_class": "FMAR", - "minimum": 0, - "data_object": "PairArr.NumPts", - "name": "FMARn.PairArr.NumPts.AI330" - }, - { - "index": 331, - "description": "Independent (X-Value) Units for Curve", - "data_type": "AI", - "common_data_class": "ENG", - "maximum": 255, - "ln_class": "FMAR", - "units": "None (list)", - "allowed_values": { - "0": "Curve is not defined", - "1": "Not applicable / Unknown", - "4": "Time", - "23": "Celsius Temperature", - "29": "Voltage", - "33": "Frequency", - "38": "Watts", - "100": "Price in hundredths of local currency", - "129": "Percent Voltage", - "133": "Percent Frequency", - "138": "Percent Watts", - "233": "Frequency Deviation" - }, - "type": "enumerated", - "minimum": 0, - "data_object": "IndpUnits", - "name": "FMARn.IndpUnits.AI331" - }, - { - "index": 332, - "description": "Dependent (Y-Value) Units for Curve", - "data_type": "AI", - "common_data_class": "ENG", - "maximum": 255, - "ln_class": "FMAR", - "units": "None (list)", - "minimum": 0, - "data_object": "DepRef", - "allowed_values": { - "0": "Curve is not defined", - "1": "Not applicable / unknown", - "2": "VArs as percent of max VArs (VARMax)", - "3": "VArs as percent of max available VArs (VArAval)", - "4": "Vars as percent of max Watts (Wmax) - not used", - "5": "Watts as percent of max Watts (Wmax)", - "6": "Watts as percent of frozen active power (DeptSnptRef)", - "7": "Power Factor in EEI notation", - "8": "Volts as a percent of the nominal voltage (VRef)", - "9": "Frequency as a percentage of the Nominal Grid Frequency (ECPNomHz)" - }, - "type": "enumerated", - "name": "FMARn.DepRef.AI332" - }, - { - "index": 333, - "description": "Curve X-Value and Y-Value pairs for curve points 1 - 100", - "data_type": "AI", - "common_data_class": "CSG", - "ln_class": "FMAR", - "units": "Varies", - "data_object": "PairArr.CrvPts", - "name": "FMARn.PairArr.CrvPts.AI333", - "type": "array", - "array_times_repeated": 100, - "array_points": [ - { - "name": "FMARn.PairArr.CrvPts.AI333.xVal" - }, - { - "name": "FMARn.PairArr.CrvPts.AI333.yVal" - } - ] - }, - { - "index": 533, - "description": "System Meter Type of Connection Point", - "data_type": "AI", - "common_data_class": "ENS", - "maximum": 99, - "ln_class": "DECP", - "units": "None (list)", - "minimum": 0, - "data_object": "EcpConnType", - "event_class": 3, - "allowed_values": { - "0": "unknown", - "1": "DER to local EPS", - "2": "Internal to DER", - "3": "local EPS with load to area EPS (PCC with load)", - "4": "local EPS w/o load to area EPS (PCC without load)", - "5": "Load to local EPS", - "6": "External to DER beyond the PCC", - "7": "External to DER within the local EPS", - "8": "Auxiliary DER Load", - "9": "Group of DERs to the area EPS", - "99": "Other" - }, - "type": "enumerated", - "name": "DECP.EcpConnType.AI533" - }, - { - "index": 534, - "description": "System Meter Type of Circuit Phases", - "data_type": "AI", - "common_data_class": "ENS", - "maximum": 8, - "ln_class": "DECP", - "units": "None (list)", - "minimum": 0, - "data_object": "PhsConnTyp", - "event_class": 3, - "allowed_values": { - "0": "unknown", - "1": "Single phase", - "2": "Split phase", - "3": "2-phase", - "4": "3-phase delta", - "5": "3-phase wye", - "6": "3-phase wye grounded", - "7": "3-phase / 3-wire (inverter type)", - "8": "3-phase / 4-wire (inverter type)" - }, - "type": "enumerated", - "name": "DECP.PhsConnTyp.AI534" - }, - { - "index": 535, - "description": "System Meter Apparent Power Calculation Method. Calculation method for total apparent power calculation.", - "data_type": "AI", - "common_data_class": "ENG", - "maximum": 2, - "ln_class": "MMXU", - "units": "None (list)", - "minimum": 0, - "data_object": "ClcTotVA", - "allowed_values": { - "0": "unknown", - "1": "vector", - "2": "arithmetic" - }, - "type": "enumerated", - "name": "MMXU.ClcTotVA.AI535" - }, - { - "index": 536, - "description": "System Meter Frequency", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "MMXU", - "units": "Hz", - "minimum": 0, - "data_object": "Hz", - "event_class": 3, - "name": "MMXU.Hz.AI536" - }, - { - "index": 537, - "description": "System Meter Active Power", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "data_object": "TotW", - "event_class": 3, - "name": "MMXU.TotW.AI537" - }, - { - "index": 538, - "description": "System Meter Active Power A", - "data_type": "AI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "units": "Watts", - "data_object": "W.phsA.mag", - "event_class": 1, - "name": "MMXU.W.phsA.mag.AI538" - }, - { - "index": 539, - "description": "System Meter Active Power B", - "data_type": "AI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "units": "Watts", - "data_object": "W.phsB.mag", - "event_class": 1, - "name": "MMXU.W.phsB.mag.AI539" - }, - { - "index": 540, - "description": "System Meter Active Power C", - "data_type": "AI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "units": "Watts", - "data_object": "W.phsC.mag", - "event_class": 1, - "name": "MMXU.W.phsC.mag.AI540" - }, - { - "index": 541, - "description": "System Meter Reactive Power", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "VARs", - "data_object": "TotVAr", - "event_class": 3, - "name": "MMXU.TotVAr.AI541" - }, - { - "index": 542, - "description": "System Meter Reactive Power A", - "data_type": "AI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "units": "VAr", - "data_object": "VAr.phsA.mag", - "event_class": 1, - "name": "MMXU.VAr.phsA.mag.AI542" - }, - { - "index": 543, - "description": "System Meter Reactive Power B", - "data_type": "AI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "units": "VAr", - "data_object": "VAr.phsB.mag", - "event_class": 1, - "name": "MMXU.VAr.phsB.mag.AI543" - }, - { - "index": 544, - "description": "System Meter Reactive Power C", - "data_type": "AI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "units": "VAr", - "data_object": "VAr.phsC.mag", - "event_class": 1, - "name": "MMXU.VAr.phsC.mag.AI544" - }, - { - "index": 545, - "description": "System Meter Power Factor", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "MMXU", - "units": "None", - "minimum": -100, - "data_object": "TotPF", - "event_class": 3, - "name": "MMXU.TotPF.AI545" - }, - { - "index": 546, - "description": "System Meter Apparent Power", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "VA", - "data_object": "TotVA", - "event_class": 3, - "name": "MMXU.TotVA.AI546" - }, - { - "index": 547, - "description": "System Meter Phase A Volts", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsA.mag", - "event_class": 3, - "name": "MMXU.PhV.phsA.mag.AI547" - }, - { - "index": 548, - "description": "System Meter Phase A Angle", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "maximum": 3600, - "ln_class": "MMXU", - "units": "Degrees", - "minimum": 0, - "data_object": "PhV.phsA.ang", - "event_class": 3, - "name": "MMXU.PhV.phsA.ang.AI548" - }, - { - "index": 549, - "description": "System Meter Phase B Volts", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsB.mag", - "event_class": 3, - "name": "MMXU.PhV.phsB.mag.AI549" - }, - { - "index": 550, - "description": "System Meter Phase B Angle", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "maximum": 3600, - "ln_class": "MMXU", - "units": "Degrees", - "minimum": 0, - "data_object": "PhV.phsB.ang", - "event_class": 3, - "name": "MMXU.PhV.phsB.ang.AI550" - }, - { - "index": 551, - "description": "System Meter Phase C Volts", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsC.mag", - "event_class": 3, - "name": "MMXU.PhV.phsC.mag.AI551" - }, - { - "index": 552, - "description": "System Meter Phase C Angle", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "maximum": 3600, - "ln_class": "MMXU", - "units": "Degrees", - "minimum": 0, - "data_object": "PhV.phsC.ang", - "event_class": 3, - "name": "MMXU.PhV.phsC.ang.AI552" - }, - { - "index": 553, - "description": "System Meter Average Line to Line Voltage", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "AvPPVPhs", - "event_class": 1, - "name": "MMXU.AvPPVPhs.AI553" - }, - { - "index": 554, - "description": "System Meter Current A", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Amps", - "data_object": "A.phsA.mag", - "event_class": 1, - "name": "MMXU.A.phsA.mag.AI554" - }, - { - "index": 555, - "description": "System Meter Current B", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Amps", - "data_object": "A.phsB.mag", - "event_class": 1, - "name": "MMXU.A.phsB.mag.AI555" - }, - { - "index": 556, - "description": "System Meter Current C", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Amps", - "data_object": "A.phsC.mag", - "event_class": 1, - "name": "MMXU.A.phsC.mag.AI556" - }, - { - "index": 557, - "description": "System Meter Active Power - High Threshold", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "minimum": 0, - "data_object": "TotW.rangeC.hLim", - "event_class": 3, - "name": "MMXU.TotW.rangeC.hLim.AI557" - }, - { - "index": 558, - "description": "System Meter Active Power - Low Threshold", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "minimum": 0, - "data_object": "TotW.rangeC.lLim", - "event_class": 3, - "name": "MMXU.TotW.rangeC.lLim.AI558" - }, - { - "index": 559, - "description": "System Meter Reactive Power - High Threshold", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "VARs", - "minimum": 0, - "data_object": "TotVAr.rangeC.hLim", - "event_class": 3, - "name": "MMXU.TotVAr.rangeC.hLim.AI559" - }, - { - "index": 560, - "description": "System Meter Reactive Power - Low Threshold", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "VARs", - "minimum": 0, - "data_object": "TotVAr.rangeC.lLim", - "event_class": 3, - "name": "MMXU.TotVAr.rangeC.lLim.AI560" - }, - { - "index": 561, - "description": "System Meter Power Factor - High Threshold", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "MMXU", - "units": "None", - "minimum": -100, - "data_object": "TotPF.rangeC.hLim", - "event_class": 3, - "name": "MMXU.TotPF.rangeC.hLim.AI561" - }, - { - "index": 562, - "description": "System Meter Power Factor - Low Threshold", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "MMXU", - "units": "None", - "minimum": -100, - "data_object": "TotPF.rangeC.lLim", - "event_class": 3, - "name": "MMXU.TotPF.rangeC.lLim.AI562" - }, - { - "index": 563, - "description": "System Meter Phase A Volts - High Threshold", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsA.rangeC.hLim", - "event_class": 3, - "name": "MMXU.PhV.phsA.rangeC.hLim.AI563" - }, - { - "index": 564, - "description": "System Meter Phase A Volts - Low Threshold", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsA.rangeC.lLim", - "event_class": 3, - "name": "MMXU.PhV.phsA.rangeC.lLim.AI564" - }, - { - "index": 565, - "description": "System Meter Phase B Volts - High Threshold", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsB.rangeC.hLim", - "event_class": 3, - "name": "MMXU.PhV.phsB.rangeC.hLim.AI565" - }, - { - "index": 566, - "description": "System Meter Phase B Volts - Low Threshold", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsB.rangeC.lLim", - "event_class": 3, - "name": "MMXU.PhV.phsB.rangeC.lLim.AI566" - }, - { - "index": 567, - "description": "System Meter Phase C Volts - High Threshold", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsC.rangeC.hLim", - "event_class": 3, - "name": "MMXU.PhV.phsC.rangeC.hLim.AI567" - }, - { - "index": 568, - "description": "System Meter Phase C Volts - Low Threshold", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsC.rangeC.lLim", - "event_class": 3, - "name": "MMXU.PhV.phsC.rangeC.lLim.AI568" - }, - { - "index": 569, - "description": "Running Schedule Index. Index of the highest priority schedule that is currently running or 0 if no schedule is currently running.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "FSCC", - "minimum": 0, - "data_object": "ActSchdRef", - "name": "FSCC1.ActSchdRef.AI569" - }, - { - "index": 570, - "description": "Schedule to Edit Selector", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "FSCC", - "minimum": 0, - "data_object": "Schd", - "event_class": 3, - "name": "FSCC.Schd.AI570", - "type": "selector_block", - "selector_block_start": 570, - "selector_block_end": 780 - - }, - { - "index": 571, - "description": "Selected Schedule Identity", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "FSCC", - "minimum": 0, - "data_object": "Schd", - "event_class": 3, - "name": "FSCC.Schd.AI571" - }, - { - "index": 572, - "description": "Selected Schedule Priority. Priority of the schedule relative to other running schedules. Lower values have higher priority over higher values.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "FSCH", - "minimum": 1, - "data_object": "SchdPrio", - "event_class": 3, - "name": "FSCH.SchdPrio.AI572" - }, - { - "index": 573, - "description": "Selected Schedule Type.", - "data_type": "AI", - "maximum": 21, - "minimum": 0, - "units": "None (list)", - "event_class": 3, - "allowed_values": { - "1": "Low/High Voltage Ride-Through Hi Must Trip", - "2": "Low/High Voltage Ride-Through Low Must Trip", - "3": "Low/High Voltage Ride-Through Hi Momentary", - "4": "Low/High Voltage Ride-Through Lo Momentary", - "5": "Low/High Frequency Ride-Through Hi Must Trip", - "6": "Low/High Frequency Ride-Through Lo Must Trip", - "7": "Low/High Frequency Ride-Through Hi Momentary", - "8": "Low/High Frequency Ride-Through Low Momentary", - "9": "Dynamic Reactive Current Support - On/Off", - "10": "Dynamic Volt-Watt - On/Off", - "11": "Frequency-Watt - On/Off", - "12": "Active Power Limit - Charging", - "13": "Active Power Limit - Generating", - "14": "Charge/Discharge - Percent of Maximum", - "15": "Coordinated Charge/Discharge - SOC Target", - "16": "Active Power Response #1 - On/Off", - "17": "Active Power Response #2 - On/Off", - "18": "Active Power Response #3 - On/Off", - "19": "AGC Watts", - "20": "Active Power Smoothing - On/Off", - "21": "Volt-Watt Curve Index", - "22": "Frequency-Watt Curve Curve Index", - "23": "Frequency-Watt Curve High Hysteresis", - "24": "Frequency-Watt Curve Low Hysteresis", - "25": "Constant VArs - Percent of Maximum", - "26": "Fixed Power Factor - Power Factor", - "27": "Volt-VAr Curve Index", - "28": "Watt-VAr Curve Index", - "29": "Power Factor Correction - On/Off", - "30": "Reserved - For pricing mode" - }, - "type": "enumerated", - "name": "AI573" - }, - { - "index": 574, - "description": "Selected Schedule Start Date. Number of days since January 1, 1970, UTC.", - "data_type": "AI", - "common_data_class": "TSG", - "ln_class": "FSCH", - "units": "Days", - "minimum": 0, - "data_object": "StrTm", - "event_class": 3, - "name": "FSCH.StrTm.AI574" - }, - { - "index": 575, - "description": "Selected Schedule Start Time. Milliseconds since the start of Schedule Start Date.", - "data_type": "AI", - "common_data_class": "TSG", - "maximum": 86400000, - "ln_class": "FSCH", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "StrTm", - "event_class": 3, - "name": "FSCH.StrTm.AI575" - }, - { - "index": 576, - "description": "Selected Schedule Repeat Interval. Interval between actions after the initial occurrence. A zero value means the schedule is not repeated.", - "data_type": "AI", - "common_data_class": "TCS", - "ln_class": "FSCH", - "units": "Varies", - "minimum": 0, - "data_object": "NxtStrTm", - "event_class": 3, - "name": "FSCH.NxtStrTm.AI576" - }, - { - "index": 577, - "description": "Selected Schedule Repeat Interval Units", - "data_type": "AI", - "common_data_class": "SPG", - "maximum": 8, - "ln_class": "FSCH", - "minimum": 0, - "data_object": "SchdReuse", - "event_class": 3, - "allowed_values": { - "0": "No Repeat", - "1": "sec", - "2": "Minutes", - "3": "Hours", - "4": "Days", - "5": "Weeks", - "6": "Months", - "7": "Months on Same Day of Week", - "8": "Months on Same Day of Week from End" - }, - "type": "enumerated", - "name": "FSCH.SchdReuse.AI577" - }, - { - "index": 578, - "description": "Selected Schedule Validation Status", - "data_type": "AI", - "common_data_class": "ENSScheduleState", - "maximum": 4, - "ln_class": "FSCH", - "minimum": 0, - "data_object": "SchdSt", - "event_class": 3, - "name": "FSCH1.SchdSt.AI578" - }, - { - "index": 579, - "description": "Selected Schedule Status", - "data_type": "AI", - "common_data_class": "ENSScheduleState", - "maximum": 4, - "ln_class": "FSCH", - "units": "None (list)", - "minimum": 0, - "data_object": "SchdSt", - "allowed_values": { - "0": "unknown", - "1": "Not available", - "2": "Inactive", - "3": "Ready-to-Run", - "4": "Running" - }, - "type": "enumerated", - "name": "FSCH1.SchdSt.AI579" - }, - { - "index": 580, - "description": "Selected Schedule Number of Points", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "FSCH", - "minimum": 0, - "data_object": "NumEntr", - "event_class": 3, - "name": "FSCH.NumEntr.AI580" - }, - { - "index": 581, - "description": "Select schedule time offset and value pairs for points 1 - 100", - "data_type": "AI", - "minimum": 0, - "name": "FSCHn.SchdEntr.AI581", - "type": "array", - "array_times_repeated": 100, - "array_points": [ - { - "name": "FSCHn.SchdEntr.AI581.time", - "units": "Seconds" - }, - { - "name": "FSCHn.SchdEntr.AI581.val", - "ln_class": "FSCH", - "data_object": "SchdEntr" - } - ] - }, - { - "index": 781, - "description": "Schedule 1 Status", - "data_type": "AI", - "common_data_class": "ENSScheduleState", - "maximum": 4, - "ln_class": "FSCH", - "units": "None (list)", - "minimum": 0, - "data_object": "SchdSt", - "allowed_values": { - "0": "unknown", - "1": "Not available", - "2": "Inactive", - "3": "Ready-to-Run", - "4": "Running" - }, - "type": "enumerated", - "name": "FSCH1.SchdSt.AI781" - }, - { - "index": 782, - "description": "Schedule 1 Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "FSCH", - "minimum": 1, - "data_object": "SchdPrio", - "name": "FSCH.SchdPrio.AI782" - }, - { - "index": 783, - "description": "Schedule 1 Active Time Value. This is the index of the time value entry the schedule is currently running. First entry is 1. Zero if the schedule is not running.", - "data_type": "AI", - "common_data_class": "INS", - "maximum": 10, - "ln_class": "FSCH", - "minimum": 0, - "data_object": "ActStrTm", - "name": "FSCH1.ActStrTm.AI783" - }, - { - "category": "alarm", - "index": 0, - "description": "System Communication Error", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "LCCH", - "data_object": "ChLiv", - "allowed_values": { - "0": "Normal", - "1": "Alarm: Communications error exists in the ESS" - }, - "event_class": 1, - "name": "LCCH.ChLiv.BI0" - }, - { - "category": "alarm", - "index": 1, - "description": "System Has Priority 1 Alarms", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "CALH", - "data_object": "GrAlm", - "allowed_values": { - "0": "No P1 Alarms Active", - "1": "Alarm: One or More P1 Alarms Active" - }, - "event_class": 1, - "name": "CALH.GrAlm.BI1" - }, - { - "category": "alarm", - "index": 2, - "description": "System Has Priority 2 Alarms", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "CALH", - "data_object": "GrWrn", - "allowed_values": { - "0": "No P2 Alarms Active", - "1": "Alarm: One or More P2 Alarms Active" - }, - "event_class": 1, - "name": "CALH.GrWrn.BI2" - }, - { - "category": "alarm", - "index": 3, - "description": "System Has Priority 3 Alarms", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "CALH", - "data_object": "GrInd", - "allowed_values": { - "0": "No P3 Alarms Active", - "1": "Alarm: One or More P3 Alarms Active" - }, - "event_class": 1, - "name": "CALH.GrInd.BI3" - }, - { - "category": "alarm", - "index": 4, - "description": "Storage State of Charge at Maximum. Maximum Usable State of Charge Reached.", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "DSTO", - "data_object": "SocHiWrn", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "DSTO.SocHiWrn.BI4" - }, - { - "category": "alarm", - "index": 5, - "description": "Storage State of Charge is Too High. Maximum Reserve Percentage (of usable capacity) reached.", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "DSTO", - "data_object": "SocHiAlm", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "DSTO.SocHiAlm.BI5" - }, - { - "category": "alarm", - "index": 6, - "description": "Storage State of Charge is Too Low. Minimum Reserve Percentage (of usable capacity) reached.", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "DSTO", - "data_object": "SocLoAlm", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "DSTO.SocLoAlm.BI6" - }, - { - "category": "alarm", - "index": 7, - "description": "Storage State of Charge is Depleted. Minimum Usable State of Charge Reached.", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "DSTO", - "data_object": "SohLoAlm", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "DSTO.SohLoAlm.BI7" - }, - { - "category": "alarm", - "index": 8, - "description": "Storage Internal Temperature is Too High", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "DBAT", - "data_object": "IntnTmpHiAlm", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "DBAT.IntnTmpHiAlm.BI8" - }, - { - "category": "alarm", - "index": 9, - "description": "Storage External (Ambient) Temperature is Too High", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "DBAT", - "data_object": "ExtTmpHiAlm", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "DBAT.ExtTmpHiAlm.BI9" - }, - { - "index": 10, - "description": "System Is In Local State. System has been locked by a local operator which prevents other operators from executing commands. Note: Local State is also sometimes referred to as Maintenance State. Local State overrides Lockout State.", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.disconnected and in maintenance", - "allowed_values": { - "0": "System not in local state", - "1": "System in local state" - }, - "name": "DSTO.DEROpSt.disconnectedandinmaintenance.BI10" - }, - { - "index": 11, - "description": "System Is In Lockout State. System has been locked by an operator such that other operators may not execute commands. Lockout State is also sometimes referred to as Blocked State.", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.disconnected and blocked", - "allowed_values": { - "0": "System not locked out", - "1": "System locked out" - }, - "name": "DSTO.DEROpSt.disconnectedandblocked.BI11" - }, - { - "index": 12, - "description": "System Is Starting Up. Set to 1 when a BO \"System Initiate Start-up Sequence\" command has been received.", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.starting and synchronizing", - "allowed_values": { - "0": "Not Starting Up", - "1": "Start command has been received." - }, - "name": "DSTO.DEROpSt.startingandsynchronizing.BI12" - }, - { - "index": 13, - "description": "System Is Stopping. Set to 1 when an B0 \"System Execute Stop\" command has been received.", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.stopping", - "allowed_values": { - "0": "Not Stopping", - "1": "Emergency stop command has been received." - }, - "name": "DSTO.DEROpSt.stopping.BI13" - }, - { - "index": 14, - "description": "System is Started (Return to Service). If any of the DER Units are started,then true. DER Units in the maintenance operational state are excluded.", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.connected and idle", - "allowed_values": { - "0": "Null", - "1": "Started" - }, - "name": "DSTO.DEROpSt.connectedandidle.BI14" - }, - { - "index": 15, - "description": "System is Stopped (Cease to Energize). If all of the DER Units are stopped, then true. DER Units in the maintenance operational state are excluded.", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.ceased to energize", - "allowed_values": { - "0": "Null", - "1": "Stopped" - }, - "name": "DSTO.DEROpSt.ceasedtoenergize.BI15" - }, - { - "index": 16, - "description": "System Permission to Start Status", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DSTO", - "data_object": "PrmConn", - "allowed_values": { - "0": "Start Permission Not Granted", - "1": "Start Permission Granted" - }, - "name": "DSTO.PrmConn.BI16" - }, - { - "index": 17, - "description": "System Permission to Stop Status", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DSTO", - "data_object": "PrmDscon", - "allowed_values": { - "0": "Stop Permission Not Granted", - "1": "Stop Permission Granted" - }, - "name": "DSTO.PrmDscon.BI17" - }, - { - "index": 18, - "description": "DER is Connected and Idle", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.connected and idle", - "allowed_values": { - "0": "Null", - "1": "Idle-Connected" - }, - "name": "DSTO.DEROpSt.connectedandidle.BI18" - }, - { - "index": 19, - "description": "DER is Connected and Generating", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.connected and generating", - "allowed_values": { - "0": "Null", - "1": "On-Connected" - }, - "name": "DSTO.DEROpSt.connectedandgenerating.BI19" - }, - { - "index": 20, - "description": "DER is Connected and Charging", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.connected and consuming", - "allowed_values": { - "0": "Null", - "1": "On-Charging-Connected" - }, - "name": "DSTO.DEROpSt.connectedandconsuming.BI20" - }, - { - "index": 21, - "description": "DER is Off but Available to Start", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.disconnected and available", - "allowed_values": { - "0": "Null", - "1": "Off-Available" - }, - "name": "DSTO.DEROpSt.disconnectedandavailable.BI21" - }, - { - "index": 22, - "description": "DER is Off and Not Available to Start", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.disconnected and stand-by", - "allowed_values": { - "0": "Null", - "1": "Off-Not-Available" - }, - "name": "DSTO.DEROpSt.disconnectedandstand-by.BI22" - }, - { - "index": 23, - "description": "DER Connect/Disconnect Switch Closed Status", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.off", - "allowed_values": { - "0": "Open", - "1": "Closed" - }, - "name": "DSTO.DEROpSt.off.BI23" - }, - { - "index": 24, - "description": "DER Connect/Disconnect Switch Movement Status", - "data_type": "BI", - "common_data_class": "DPC", - "ln_class": "CSWI", - "data_object": "Pos", - "allowed_values": { - "0": "Not Moving", - "1": "Moving" - }, - "name": "CSWI.Pos.BI24" - }, - { - "index": 25, - "description": "Islanded Mode. Determines how the DER behaves when in an Islanded configuration.", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DSTO", - "data_object": "IsldCtlFol", - "allowed_values": { - "0": "Isochronous Mode. DER attempts to control voltage and frequency independent of configured curves and settings up to the limits of the machine's capabilities in order to achieve the AO Reference Voltage and AO nominal frequency.", - "1": "Droop Mode. DER acts as a follower using Volt/VAR and Freq/Watt curves." - }, - "event_class": 3, - "name": "DSTO.IsldCtlFol.BI25" - }, - { - "index": 26, - "description": "Sensed Grid Config Detection Enabled. If Enabled, the DER may independently change its Active Settings Group based on locally observed grid conditions.", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DECP", - "data_object": "ECPIsldAuto", - "allowed_values": { - "0": "No Autonomous Detection.", - "1": "Autonomous Detection. Inverter's Active Settings Group may differ from the Requested Settings Group" - }, - "event_class": 3, - "name": "DECP.ECPIsldAuto.BI26" - }, - { - "index": 27, - "description": "Storage Capacity Units. Determines whether energy storage values are expressed in units of Amp-hrs or Watt-hrs.", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DSTO", - "data_object": "AGra", - "allowed_values": { - "0": "Amp-hrs (default)", - "1": "Watt-hrs" - }, - "event_class": 3, - "name": "DSTO.AGra.BI27" - }, - { - "index": 28, - "description": "Time Constant Mode. Indicates whether Time Constant Ramp parameters are interpreted as Open Loop Response times or 3Tau values.", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DSTO", - "data_object": "OpnLoopTau", - "allowed_values": { - "0": "Open Loop Response Time", - "1": "3Tau Value" - }, - "event_class": 3, - "name": "DSTO.OpnLoopTau.BI28" - }, - { - "index": 29, - "description": "Power Factor Excitation When Discharging/Generating", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DFPF", - "data_object": "PFGnExtSet", - "allowed_values": { - "0": "Injecting VARs - Q1", - "1": "Absorbing VARs - Q4" - }, - "name": "DFPF.PFGnExtSet.BI29" - }, - { - "index": 30, - "description": "Power Factor Excitation When Charging", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DFPF", - "data_object": "PFLodExtSet", - "allowed_values": { - "0": "Injecting VARs - Q2", - "1": "Absorbing VARs - Q3" - }, - "name": "DFPF.PFLodExtSet.BI30" - }, - { - "index": 31, - "description": "Supports Low/High Voltage Ride-Through Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DHVT", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DHVT.BI31" - }, - { - "index": 32, - "description": "Supports Low/High Frequency Ride-Through Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DHFT", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DHFT.BI32" - }, - { - "index": 33, - "description": "Supports Dynamic Reactive Current Support Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DRGS", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DRGS.BI33" - }, - { - "index": 34, - "description": "Supports Dynamic Volt-Watt Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DVWD", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DVWD.BI34" - }, - { - "index": 35, - "description": "Supports Frequency-Watt Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DHFW", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DHFW.BI35" - }, - { - "index": 36, - "description": "Supports Active Power Limit Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DWLM", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DWLM.BI36" - }, - { - "index": 37, - "description": "Supports Charge/Discharge Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DWGC", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DWGC.BI37" - }, - { - "index": 38, - "description": "Supports Coordinated Charge/Discharge Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DTCD", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DTCD.BI38" - }, - { - "index": 39, - "description": "Supports Active Power Response Mode #1", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DLFL", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DLFL.BI39" - }, - { - "index": 40, - "description": "Supports Active Power Response Mode #2", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DGFL", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DGFL.BI40" - }, - { - "index": 41, - "description": "Supports Active Power Response Mode #3", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DGFL", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DGFL.BI41" - }, - { - "index": 42, - "description": "Supports Automatic Generation Control Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DAGC", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DAGC.BI42" - }, - { - "index": 43, - "description": "Supports Active Power Smoothing Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DWSM", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DWSM.BI43" - }, - { - "index": 44, - "description": "Supports Volt-Watt Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DVWC", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DVWC.BI44" - }, - { - "index": 45, - "description": "Supports Frequency-Watt Curve Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DFWC", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DFWC.BI45" - }, - { - "index": 46, - "description": "Supports Constant VArs Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DVAR", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DVAR.BI46" - }, - { - "index": 47, - "description": "Supports Fixed Power Factor Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DFPF", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DFPF.BI47" - }, - { - "index": 48, - "description": "Supports Volt-VAr Control Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DVVC", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DVVC.BI48" - }, - { - "index": 49, - "description": "Supports Watt-VAr Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DWVR", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DWVR.BI49" - }, - { - "index": 50, - "description": "Supports Power Factor Correction Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DPFC", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DPFC.BI50" - }, - { - "index": 51, - "description": "Supports Pricing Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DPRG", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DPRG.BI51" - }, - { - "index": 52, - "description": "Overvoltage Disconnect Protection Blocked", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "PTOV", - "data_object": "Blk", - "allowed_values": { - "0": "Not Blocked", - "1": "Blocked (Disabled)" - }, - "event_class": 1, - "name": "PTOV.Blk.BI52" - }, - { - "index": 53, - "description": "Overvoltage Disconnect Protection Started", - "data_type": "BI", - "common_data_class": "ACD", - "ln_class": "PTOV", - "data_object": "Str.general", - "allowed_values": { - "0": "Not Started", - "1": "Started (Evaluating)" - }, - "event_class": 1, - "name": "PTOV.Str.general.BI53" - }, - { - "index": 54, - "description": "Overvoltage Disconnect Protection Operated", - "data_type": "BI", - "common_data_class": "ACT", - "ln_class": "PTOV", - "data_object": "Op.general", - "allowed_values": { - "0": "Not Operated", - "1": "Operated (Disconnected)" - }, - "event_class": 1, - "name": "PTOV.Op.general.BI54" - }, - { - "index": 55, - "description": "Undervoltage Disconnect Protection Blocked", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "PTUV", - "data_object": "Blk", - "allowed_values": { - "0": "Not Blocked", - "1": "Blocked (Disabled)" - }, - "event_class": 1, - "name": "PTUV.Blk.BI55" - }, - { - "index": 56, - "description": "Undervoltage Disconnect Protection Started", - "data_type": "BI", - "common_data_class": "ACD", - "ln_class": "PTUV", - "data_object": "Str.general", - "allowed_values": { - "0": "Not Started", - "1": "Started (Evaluating)" - }, - "event_class": 1, - "name": "PTUV.Str.general.BI56" - }, - { - "index": 57, - "description": "Undervoltage Disconnect Protection Operated", - "data_type": "BI", - "common_data_class": "ACT", - "ln_class": "PTUV", - "data_object": "Op.general", - "allowed_values": { - "0": "Not Operated", - "1": "Operated (Disconnected)" - }, - "event_class": 1, - "name": "PTUV.Op.general.BI57" - }, - { - "index": 58, - "description": "Over Frequency Disconnect Protection Blocked", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "PTOV", - "data_object": "Blk", - "allowed_values": { - "0": "Not Blocked", - "1": "Blocked (Disabled)" - }, - "event_class": 1, - "name": "PTOV.Blk.BI58" - }, - { - "index": 59, - "description": "Over Frequency Disconnect Protection Started", - "data_type": "BI", - "common_data_class": "ACD", - "ln_class": "PTOV", - "data_object": "Str.general", - "allowed_values": { - "0": "Not Started", - "1": "Started (Evaluating)" - }, - "event_class": 1, - "name": "PTOV.Str.general.BI59" - }, - { - "index": 60, - "description": "Over Frequency Disconnect Protection Operated", - "data_type": "BI", - "common_data_class": "ACT", - "ln_class": "PTOV", - "data_object": "Op.general", - "allowed_values": { - "0": "Not Operated", - "1": "Operated (Disconnected)" - }, - "event_class": 1, - "name": "PTOV.Op.general.BI60" - }, - { - "index": 61, - "description": "Under Frequency Disconnect Protection Blocked", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "PTUV", - "data_object": "Blk", - "allowed_values": { - "0": "Not Blocked", - "1": "Blocked (Disabled)" - }, - "event_class": 1, - "name": "PTUV.Blk.BI61" - }, - { - "index": 62, - "description": "Under Frequency Disconnect Protection Started", - "data_type": "BI", - "common_data_class": "ACD", - "ln_class": "PTUV", - "data_object": "Str.general", - "allowed_values": { - "0": "Not Started", - "1": "Started (Evaluating)" - }, - "event_class": 1, - "name": "PTUV.Str.general.BI62" - }, - { - "index": 63, - "description": "Under Frequency Disconnect Protection Operated", - "data_type": "BI", - "common_data_class": "ACT", - "ln_class": "PTUV", - "data_object": "Op.general", - "allowed_values": { - "0": "Not Operated", - "1": "Operated (Disconnected)" - }, - "event_class": 1, - "name": "PTUV.Op.general.BI63" - }, - { - "category": "mode_enable", - "index": 64, - "description": "Operating Mode - Low/High Voltage Ride-Through", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DHVT", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DHVT.ModEna.BI64" - }, - { - "category": "mode_enable", - "index": 65, - "description": "Operating Mode - Low/High Frequency Ride-Through", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DHFT", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DHFT.ModEna.BI65" - }, - { - "category": "mode_enable", - "index": 66, - "description": "Operating Mode - Dynamic Reactive Current Support Enabled", - "data_type": "BI", - "common_data_class": "ENC", - "ln_class": "DRGS", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DRGS.ModEna.BI66" - }, - { - "category": "mode_enable", - "index": 67, - "description": "Operating Mode - Dynamic Volt-Watt Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DVWD", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DVWD.ModEna.BI67" - }, - { - "category": "mode_enable", - "index": 68, - "description": "Operating Mode - Frequency-Watt Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DHFW", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DHFW.ModEna.BI68" - }, - { - "category": "mode_enable", - "index": 69, - "description": "Operating Mode - Active Power Limit Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DWLM", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DWLM.ModEna.BI69" - }, - { - "category": "mode_enable", - "index": 70, - "description": "Operating Mode - Charge/Discharge Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DWGC", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DWGC.ModEna.BI70" - }, - { - "category": "mode_enable", - "index": 71, - "description": "Operating Mode - Coordinated Charge/Discharge Management Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DTCD", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DTCD.ModEna.BI71" - }, - { - "category": "mode_enable", - "index": 72, - "description": "Operating Mode - Active Power Response Mode #1 Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DPKP", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DPKP.ModEna.BI72" - }, - { - "category": "mode_enable", - "index": 73, - "description": "Operating Mode - Active Power Response Mode #2 Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DGFL", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DGFL.ModEna.BI73" - }, - { - "category": "mode_enable", - "index": 74, - "description": "Operating Mode - Active Power Response Mode #3 Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DLFL", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DLFL.ModEna.BI74" - }, - { - "category": "mode_enable", - "index": 75, - "description": "Operating Mode - Automatic Generation Control Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DAGC", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DAGC.ModEna.BI75" - }, - { - "category": "mode_enable", - "index": 76, - "description": "Operating Mode - Active Power Smoothing Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DWSM", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DWSM.ModEna.BI76" - }, - { - "category": "mode_enable", - "index": 77, - "description": "Operating Mode - Volt-Watt Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DVWC", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DVWC.ModEna.BI77" - }, - { - "category": "mode_enable", - "index": 78, - "description": "Operating Mode - Frequency-Watt Curve Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DHFW", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DHFW.ModEna.BI78" - }, - { - "category": "mode_enable", - "index": 79, - "description": "Operating Mode - Constant VArs Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DVAR", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DVAR.ModEna.BI79" - }, - { - "category": "mode_enable", - "index": 80, - "description": "Operating Mode - Fixed Power Factor Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DFPF", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DFPF.ModEna.BI80" - }, - { - "category": "mode_enable", - "index": 81, - "description": "Operating Mode - Volt-VAR Control Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DVVR", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DVVR.ModEna.BI81" - }, - { - "category": "mode_enable", - "index": 82, - "description": "Operating Mode - Watt-VAr Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DWVR", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DWVR.ModEna.BI82" - }, - { - "category": "mode_enable", - "index": 83, - "description": "Operating Mode - Power Factor Correction Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DPFC", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DPFC.ModEna.BI83" - }, - { - "category": "mode_enable", - "index": 84, - "description": "Operating Mode - Pricing Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DPRG", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DPRG.ModEna.BI84" - }, - { - "category": "mode_enable", - "index": 85, - "description": "Operating Mode - Event-Based Reactive Current Support Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DRGS", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DRGS.ModEna.BI85" - }, - { - "index": 86, - "description": "Frequency-Watt Mode - Use Hysteresis", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DHFW", - "data_object": "HysEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DHFW.HysEna.BI86" - }, - { - "index": 87, - "description": "Frequency-Watt Mode - Snapshot of Power", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DHFW", - "data_object": "SnptEna", - "allowed_values": { - "0": "Not Active", - "1": "Active" - }, - "name": "DHFW.SnptEna.BI87" - }, - { - "index": 88, - "description": "Frequency-Watt Curve Mode - Use Hysteresis", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DLFW", - "data_object": "HysEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DLFW.HysEna.BI88" - }, - { - "index": 89, - "description": "Frequency-Watt Curve Mode - Snapshot of Power", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DLFW", - "data_object": "SnptEna", - "allowed_values": { - "0": "Not Active", - "1": "Active" - }, - "name": "DLFW.SnptEna.BI89" - }, - { - "index": 90, - "description": "Charge/Discharge Mode - Use Ramp Rates. Indicates whether or not Charge/Discharge should use specified ramp rates or ramp times.", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DWGC", - "data_object": "UseRmpRte", - "allowed_values": { - "0": "Use Time Constants", - "1": "Use Ramp Rates" - }, - "name": "DWGC.UseRmpRte.BI90" - }, - { - "index": 91, - "description": "AGC Mode - Use Ramp Rates. Indicates whether or not charge/discharge should use specified ramp rates or ramp times.", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DAGC", - "data_object": "UseRmpRte", - "allowed_values": { - "0": "Use Time Constants", - "1": "Use Ramp Rates" - }, - "name": "DAGC.UseRmpRte.BI91" - }, - { - "index": 92, - "description": "Volt-Watt - Use Ramp Rates and Time Constants", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DVWC", - "data_object": "UseRmpRte", - "allowed_values": { - "0": "Use Time Constants", - "1": "Use Ramp Rates AND Time Constants" - }, - "name": "DVWC.UseRmpRte.BI92" - }, - { - "index": 93, - "description": "Volt-VAr Enable Autonomous Voltage Reference Adjustment", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DVVR", - "data_object": "VRefAdjEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DVVR.VRefAdjEna.BI93" - }, - { - "category": "alarm", - "index": 94, - "description": "System Meter Active Power is Too High", - "data_type": "BI", - "common_data_class": "MV", - "ln_class": "MMXU", - "data_object": "TotW.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.TotW.range.BI94" - }, - { - "category": "alarm", - "index": 95, - "description": "System Meter Active Power is Too Low", - "data_type": "BI", - "common_data_class": "MV", - "ln_class": "MMXU", - "data_object": "TotW.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.TotW.range.BI95" - }, - { - "category": "alarm", - "index": 96, - "description": "System Meter Reactive Power is Too High", - "data_type": "BI", - "common_data_class": "MV", - "ln_class": "MMXU", - "data_object": "TotVAr.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.TotVAr.range.BI96" - }, - { - "category": "alarm", - "index": 97, - "description": "System Meter Reactive Power is Too Low", - "data_type": "BI", - "common_data_class": "MV", - "ln_class": "MMXU", - "data_object": "TotVAr.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.TotVAr.range.BI97" - }, - { - "category": "alarm", - "index": 98, - "description": "System Meter Power Factor is Too High", - "data_type": "BI", - "common_data_class": "MV", - "ln_class": "MMXU", - "data_object": "TotPF.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.TotPF.range.BI98" - }, - { - "category": "alarm", - "index": 99, - "description": "System Meter Power Factor is Too Low", - "data_type": "BI", - "common_data_class": "MV", - "ln_class": "MMXU", - "data_object": "TotPF.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.TotPF.range.BI99" - }, - { - "category": "alarm", - "index": 100, - "description": "System Meter Phase A Voltage is Too High", - "data_type": "BI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "data_object": "PhV.phsA.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.PhV.phsA.range.BI100" - }, - { - "category": "alarm", - "index": 101, - "description": "System Meter Phase A Voltage is Too Low", - "data_type": "BI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "data_object": "PhV.phsA.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.PhV.phsA.range.BI101" - }, - { - "category": "alarm", - "index": 102, - "description": "System Meter Phase B Voltage is Too High", - "data_type": "BI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "data_object": "PhV.phsB.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.PhV.phsB.range.BI102" - }, - { - "category": "alarm", - "index": 103, - "description": "System Meter Phase B Voltage is Too Low", - "data_type": "BI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "data_object": "PhV.phsB.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.PhV.phsB.range.BI103" - }, - { - "category": "alarm", - "index": 104, - "description": "System Meter Phase C Voltage is Too High", - "data_type": "BI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "data_object": "PhV.phsC.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.PhV.phsC.range.BI104" - }, - { - "category": "alarm", - "index": 105, - "description": "System Meter Phase C Voltage is Too Low", - "data_type": "BI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "data_object": "PhV.phsC.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.PhV.phsC.range.BI105" - }, - { - "category": "alarm", - "index": 106, - "description": "System Meter Communication Error", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "LCCH", - "data_object": "ChLiv", - "allowed_values": { - "0": "Normal: No Active Communications Error", - "1": "Alarm: Active Communications Error" - }, - "event_class": 1, - "name": "LCCH.ChLiv.BI106" - }, - { - "index": 107, - "description": "Selected Curve is Referenced by a Mode", - "data_type": "BI", - "allowed_values": { - "0": "Curve is not Referenced", - "1": "Curve is Referenced" - }, - "event_class": 1, - "name": "BI107" - }, - { - "index": 108, - "description": "Selected Schedule Is Ready", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "FSCH", - "data_object": "SchdSt.3", - "allowed_values": { - "0": "Not Ready", - "1": "Ready" - }, - "name": "FSCH.SchdSt.3.BI108" - }, - { - "index": 109, - "description": "Selected Schedule is Validated", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "FSCH", - "data_object": "SchdSt.2", - "allowed_values": { - "0": "Not validated", - "1": "Validated" - }, - "name": "FSCH.SchdSt.2.BI109" - }, - { - "index": 110, - "description": "Selected Schedule Repeat Weekly Sunday", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BI110" - }, - { - "index": 111, - "description": "Selected Schedule Repeat Weekly Monday", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BI111" - }, - { - "index": 112, - "description": "Selected Schedule Repeat Weekly Tuesday", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BI112" - }, - { - "index": 113, - "description": "Selected Schedule Repeat Weekly Wednesday", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BI113" - }, - { - "index": 114, - "description": "Selected Schedule Repeat Weekly Thursday", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BI114" - }, - { - "index": 115, - "description": "Selected Schedule Repeat Weekly Friday", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BI115" - }, - { - "index": 116, - "description": "Selected Schedule Repeat Weekly Saturday", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BI116" - }, - { - "index": 0, - "description": "System Set Lockout State", - "data_type": "BO", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.disconnected and blocked", - "allowed_values": { - "0": "Not Locked Out", - "1": "Lock Out" - }, - "name": "DSTO.DEROpSt.disconnectedandblocked.BO0" - }, - { - "index": 1, - "description": "System Initiate Start-up Sequence (Return to Service). Setting this to 1 does the following: - Sets BI \"System Is Starting Up\" to 1 indicating that the system is starting up. Additional start-up status can be found in AI \"System Start-up Status\". - Instructs all batteries to connect. - Once each battery has reported that it has connect successfully, instructs corresponding DER Unit to start. System can be shut down by executing B0 \"Emergency Stop\" command. This operation is the same as California Rule 21 \"Soft Start\".", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DCTE", - "data_object": "RtnSrvReq", - "allowed_values": { - "0": "No Change", - "1": "Initiate Start-up" - }, - "name": "DCTE.RtnSrvReq.BO1" - }, - { - "index": 2, - "description": "System Execute Stop (Cease to Energize). Setting this to 1 does the following: - Sets BI \"System Is Emergency Stopping\" to 1 indicating that an emergency stop is in progress. - Ensures that any executing operating modes are shut down (disabled). - Ensures that any executing schedules are shut down (disabled). - Instructs all inverters to shut down. - Instructs all batteries to disconnect. System can be started again by executing BO \"Initiate Start-up Sequence\" command.", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DCTE", - "data_object": "CeaEngzReq", - "allowed_values": { - "0": "No Change", - "1": "Stop (Emergency)" - }, - "name": "DCTE.CeaEngzReq.BO2" - }, - { - "index": 3, - "description": "System Permission to Start", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DSTO", - "data_object": "PrmConn", - "allowed_values": { - "0": "DCTE", - "1": "Start Permission Granted" - }, - "name": "DSTO.PrmConn.BO3" - }, - { - "index": 4, - "description": "System Permission to Stop", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DSTO", - "data_object": "PrmDscon", - "allowed_values": { - "0": "DCTE", - "1": "Stop Permission Granted" - }, - "name": "DSTO.PrmDscon.BO4" - }, - { - "index": 5, - "description": "DER Connect/Disconnect Switch", - "data_type": "BO", - "common_data_class": "DPC", - "ln_class": "CSWI", - "data_object": "Pos", - "allowed_values": { - "0": "Open Switch", - "1": "Close Switch" - }, - "name": "CSWI.Pos.BO5" - }, - { - "index": 6, - "description": "Islanded Mode. Determines how the DER behaves when in an Islanded configuration.", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DGEN", - "data_object": "IsldCtlFol", - "allowed_values": { - "0": "Isochronous Mode. DER attempts to control voltage and frequency independent of configured curves and settings up to the limits of the machine's capabilities in order to achieve AO reference voltage and AO nominal frequency.", - "1": "Droop Mode. DER acts as a follower using Volt/VAR and Freq/Watt curves." - }, - "name": "DGEN.IsldCtlFol.BO6" - }, - { - "index": 7, - "description": "Enable Sensed Grid Config Detection. If Enabled, the DER may independently change its Active Settings Group based on locally observed grid conditions.", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DECP", - "data_object": "ECPIsldAuto", - "allowed_values": { - "0": "No Autonomous Detection.", - "1": "Autonomous Detection. Inverter's Active Settings Group may differ from the Requested Settings Group" - }, - "name": "DECP.ECPIsldAuto.BO7" - }, - { - "index": 8, - "description": "Storage Capacity Units. Determines whether the energy storage values are expressed in Amp-hrs or Watt-hrs.", - "data_type": "BO", - "common_data_class": "ASG", - "ln_class": "DSTO", - "data_object": "AGra", - "allowed_values": { - "0": "Amp-hrs (default)", - "1": "Watt-hrs" - }, - "name": "DSTO.AGra.BO8" - }, - { - "index": 9, - "description": "Time Constant Mode. Indicates whether Time Constant Ramp parameters are interpreted as Open Loop Response times or 3Tau values.", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DSTO", - "data_object": "OpnLoopTau", - "allowed_values": { - "0": "Open Loop Response Time", - "1": "3Tau Value" - }, - "name": "DSTO.OpnLoopTau.BO9" - }, - { - "index": 10, - "description": "Power Factor Excitation When Discharging/Generating", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DFPF", - "data_object": "PFGnExtSet", - "allowed_values": { - "0": "Producing VARs - Q1", - "1": "Absorbing VARs - Q4" - }, - "name": "DFPF.PFGnExtSet.BO10" - }, - { - "index": 11, - "description": "Power Factor Excitation When Charging", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DFPF", - "data_object": "PFLodExtSet", - "allowed_values": { - "0": "Producing VARs - Q2", - "1": "Absorbing VARs - Q3" - }, - "name": "DFPF.PFLodExtSet.BO11" - }, - { - "category": "mode_enable", - "index": 12, - "description": "Enable Low/High Voltage Ride-Through Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DHVT", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DHVT.ModEna.BI64", - "name": "DHVT.ModEna.BO12" - }, - { - "category": "mode_enable", - "index": 13, - "description": "Enable Low/High Frequency Ride-Through Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DHFT", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DHFT.ModEna.BI65", - "name": "DHFT.ModEna.BO13" - }, - { - "category": "mode_enable", - "index": 14, - "description": "Enable Dynamic Reactive Current Support Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DRGS", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DRGS.ModEna.BI66", - "name": "DRGS.ModEna.BO14" - }, - { - "category": "mode_enable", - "index": 15, - "description": "Enable Dynamic Volt-Watt Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DVWD", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DVWD.ModEna.BI67", - "name": "DVWD.ModEna.BO15" - }, - { - "category": "mode_enable", - "index": 16, - "description": "Enable Frequency-Watt Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DHFW", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DHFW.ModEna.BI68", - "name": "DHFW.ModEna.BO16" - }, - { - "category": "mode_enable", - "index": 17, - "description": "Enable Active Power Limit Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DWLM", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DWLM.ModEna.BI69", - "name": "DWLM.ModEna.BO17" - }, - { - "category": "mode_enable", - "index": 18, - "description": "Enable Charge/Discharge Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DWGC", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DTCD.ModEna.BI71", - "name": "DWGC.ModEna.BO18" - }, - { - "category": "mode_enable", - "index": 19, - "description": "Enable Coordinated Charge/Discharge Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DTCD", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "name": "DTCD.ModEna.BO19" - }, - { - "category": "mode_enable", - "index": 20, - "description": "Enable Active Power Response Mode #1", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DPKP", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DPKP.ModEna.BI72", - "name": "DPKP.ModEna.BO20" - }, - { - "category": "mode_enable", - "index": 21, - "description": "Enable Active Power Response Mode #2", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DGFL", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DGFL.ModEna.BI73", - "name": "DGFL.ModEna.BO21" - }, - { - "category": "mode_enable", - "index": 22, - "description": "Enable Active Power Response Mode #3", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DLFL", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DLFL.ModEna.BI74", - "name": "DLFL.ModEna.BO22" - }, - { - "category": "mode_enable", - "index": 23, - "description": "Enable Automatic Generation Control Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DAGC", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DAGC.ModEna.BI75", - "name": "DAGC.ModEna.BO23" - }, - { - "category": "mode_enable", - "index": 24, - "description": "Enable Active Power Smoothing Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DWSM", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DWSM.ModEna.BI76", - "name": "DWSM.ModEna.BO24" - }, - { - "category": "mode_enable", - "index": 25, - "description": "Enable Volt-Watt Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DVWC", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DVWC.ModEna.BI77", - "name": "DVWC.ModEna.BO25" - }, - { - "category": "mode_enable", - "index": 26, - "description": "Enable Frequency-Watt Curve Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DHFW", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DHFW.ModEna.BI78", - "name": "DHFW.ModEna.BO26" - }, - { - "category": "mode_enable", - "index": 27, - "description": "Enable Constant VArs Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DVAR", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DVAR.ModEna.BI79", - "name": "DVAR.ModEna.BO27" - }, - { - "category": "mode_enable", - "index": 28, - "description": "Enable Fixed Power Factor Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DFPF", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DFPF.BI47", - "name": "DFPF.ModEna.BO28" - }, - { - "category": "mode_enable", - "index": 29, - "description": "Enable Volt-VAR Control Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DVVC", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DVVC.BI48", - "name": "DVVC.ModEna.BO29" - }, - { - "category": "mode_enable", - "index": 30, - "description": "Enable Watt-VAr Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DWVR", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DWVR.BI49", - "name": "DWVR.ModEna.BO30" - }, - { - "category": "mode_enable", - "index": 31, - "description": "Enable Power Factor Correction Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DPFC", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DPFC.ModEna.BI83", - "name": "DPFC.ModEna.BO31" - }, - { - "category": "mode_enable", - "index": 32, - "description": "Enable Pricing Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DPRG", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DPRG.ModEna.BI84", - "name": "DPRG.ModEna.BO32" - }, - { - "index": 33, - "description": "Enable Event-Based Reactive Current Support Mode, in which the moving average voltage and the base reactive current are frozen until after the voltage has returned to within the deadband for a specified hold time. Dynamic Reactive Current Support mode must be Enable for this setting to apply.", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DRGS", - "data_object": "ArGraMod", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "name": "DRGS.ArGraMod.BO33" - }, - { - "index": 34, - "description": "Frequency-Watt Mode - Use Hysteresis", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DHFW", - "data_object": "HysEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "name": "DHFW.HysEna.BO34" - }, - { - "index": 35, - "description": "Frequency-Watt Mode - Snapshot of Power", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DHFW", - "data_object": "SnptEna", - "allowed_values": { - "0": "Not Active", - "1": "Active" - }, - "name": "DHFW.SnptEna.BO35" - }, - { - "index": 36, - "description": "Frequency-Watt Curve Mode - Use Hysteresis", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DLFW", - "data_object": "HysEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "name": "DLFW.HysEna.BO36" - }, - { - "index": 37, - "description": "Frequency-Watt Curve Mode - Snapshot of Power", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DLFW", - "data_object": "SnptEna", - "allowed_values": { - "0": "Not Active", - "1": "Active" - }, - "name": "DLFW.SnptEna.BO37" - }, - { - "index": 38, - "description": "Charge/Discharge Mode - Use Ramp Rates. Indicates whether or not Charge/Discharge should use specified ramp rates or time constants.", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DWGC", - "data_object": "UseRmpRte", - "allowed_values": { - "0": "Use Time Constatnts", - "1": "Use Ramp Rates" - }, - "name": "DWGC.UseRmpRte.BO38" - }, - { - "index": 39, - "description": "AGC Mode - Use Ramp Rates. Indicates whether or not AGC mode should use specified ramp rates or time constants.", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DAGC", - "data_object": "UseRmpRte", - "allowed_values": { - "0": "Use Time Constatnts", - "1": "Use Ramp Rates" - }, - "name": "DAGC.UseRmpRte.BO39" - }, - { - "index": 40, - "description": "Volt-Watt - Use Ramp Rates and Time Constants. Indicates whether Volt-Watt mode should use only Time Constants,or both Time Constants and Ramp Rates", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DVWC", - "data_object": "UseRmpRte", - "allowed_values": { - "0": "Use Time Constatnts", - "1": "Use Ramp Rates AND Time Constants" - }, - "name": "DVWC.UseRmpRte.BO40" - }, - { - "index": 41, - "description": "Volt-VAr Enable Autonomous Voltage Reference Adjustment", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DVVR", - "data_object": "VRefAdjEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "name": "DVVR.VRefAdjEna.BO41" - }, - { - "index": 42, - "description": "Set Selected Scheduled Ready", - "data_type": "BO", - "common_data_class": "ENC", - "ln_class": "FSCH", - "data_object": "Mod", - "allowed_values": { - "0": "Not Ready", - "1": "Ready" - }, - "name": "FSCHxx.Mod.BO42" - }, - { - "index": 43, - "description": "Set Selected Schedule Repeat Weekly Sunday", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BO43" - }, - { - "index": 44, - "description": "Set Selected Schedule Repeat Weekly Monday", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BO44" - }, - { - "index": 45, - "description": "Set Selected Schedule Repeat Weekly Tuesday", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BO45" - }, - { - "index": 46, - "description": "Set Selected Schedule Repeat Weekly Wednesday", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BO46" - }, - { - "index": 47, - "description": "Set Selected Schedule Repeat Weekly Thursday", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BO47" - }, - { - "index": 48, - "description": "Set Selected Schedule Repeat Weekly Friday", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BO48" - }, - { - "index": 49, - "description": "Set Selected Schedule Repeat Weekly Saturday", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BO49" - } -] \ No newline at end of file diff --git a/services/core/DNP3Agent/dnp3/outstation.py b/services/core/DNP3Agent/dnp3/outstation.py deleted file mode 100644 index 142eec388d..0000000000 --- a/services/core/DNP3Agent/dnp3/outstation.py +++ /dev/null @@ -1,420 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, SLAC / 8minutenergy / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# This material was prepared in part as an account of work sponsored by an agency of -# the United States Government. Neither the United States Government nor the -# United States Department of Energy, nor SLAC, nor 8minutenergy, nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by the United States Government or any agency thereof, or -# SLAC, 8minutenergy, or Kisensum. The views and opinions of authors expressed -# herein do not necessarily state or reflect those of the -# United States Government or any agency thereof. -# }}} - -import os -import logging - -from pydnp3 import opendnp3, openpal, asiopal, asiodnp3 - -# from volttron.platform.agent import utils - -# utils.setup_logging() -_log = logging.getLogger(__name__) - - -class DNP3Outstation(opendnp3.IOutstationApplication): - """ - Model the Application Layer of a DNP3 outstation. - - This class models the interface for all outstation callback info except for control requests. - - DNP3 spec section 5.1.6.2: - The Application Layer provides the following services for the DNP3 User Layer in an outstation: - - Notifies the DNP3 User Layer when action requests, such as control output, - analog output, freeze and file operations, arrive from a master. - - Requests data and information from the outstation that is wanted by a master - and formats the responses returned to a master. - - Assures that event data is successfully conveyed to a master (using - Application Layer confirmation). - - Sends notifications to the master when the outstation restarts, has queued events, - and requires time synchronization. - - DNP spec section 5.1.6.3: - The Application Layer requires specific services from the layers beneath it. - - Partitioning of fragments into smaller portions for transport reliability. - - Knowledge of which device(s) were the source of received messages. - - Transmission of messages to specific devices or to all devices. - - Message integrity (i.e., error-free reception and transmission of messages). - - Knowledge of the time when messages arrive. - - Either precise times of transmission or the ability to set time values - into outgoing messages. - """ - - outstation = None - outstation_config = {} - agent = None - - def __init__(self, local_ip, port, outstation_config): - """ - Initialize the outstation's Application Layer. - - @param local_ip: Host name (DNS resolved) or IP address of remote endpoint. Default: 0.0.0.0. - @param port: Port remote endpoint is listening on. Default: 20000. - @param outstation_config: A dictionary of configuration parameters. All are optional. Parameters include: - database_sizes: (integer) Size of the Outstation's point database, by point type. Default: 10000. - event_buffers: (integer) Size of the database event buffers. Default: 10. - allow_unsolicited: (boolean) Whether to allow unsolicited requests. Default: True. - link_local_addr: (integer) Link layer local address. Default: 10. - link_remote_addr: (integer) Link layer remote address. Default: 1. - log_levels: List of bit field names (OR'd together) that filter what gets logged by DNP3. Default: [NORMAL]. - Possible values: ALL, ALL_APP_COMMS, ALL_COMMS, NORMAL, NOTHING - threads_to_allocate: (integer) Threads to allocate in the manager's thread pool. Default: 1. - """ - super(DNP3Outstation, self).__init__() - self.local_ip = local_ip - self.port = port - self.set_outstation_config(outstation_config) - # The following variables are initialized after start() is called. - self.stack_config = None - self.log_handler = None - self.manager = None - self.retry_parameters = None - self.listener = None - self.channel = None - self.command_handler = None - - def start(self): - _log.debug('Configuring the DNP3 stack.') - self.stack_config = asiodnp3.OutstationStackConfig(opendnp3.DatabaseSizes.AllTypes(self.outstation_config.get('database_sizes', 10000))) - self.stack_config.outstation.eventBufferConfig = opendnp3.EventBufferConfig.AllTypes(self.outstation_config.get('event_buffers', 10)) - self.stack_config.outstation.params.allowUnsolicited = self.outstation_config.get('allow_unsolicited', True) - self.stack_config.link.LocalAddr = self.outstation_config.get('link_local_addr', 10) - self.stack_config.link.RemoteAddr = self.outstation_config.get('link_remote_addr', 1) - self.stack_config.link.KeepAliveTimeout = openpal.TimeDuration().Max() - - # Configure the outstation database of points based on the contents of the data dictionary. - _log.debug('Configuring the DNP3 Outstation database.') - db_config = self.stack_config.dbConfig - for point in self.get_agent().point_definitions.all_points(): - if point.data_type == 'Analog Input': - cfg = db_config.analog[int(point.index)] - elif point.data_type == 'Binary Input': - cfg = db_config.binary[int(point.index)] - else: - # This database's point configuration is limited to Binary and Analog data types. - cfg = None - if cfg: - # cfg.vIndex = virtual index of the point - cfg.clazz = point.eclass - # cfg.svariation and cfg.evariation are static const: cannot modify - - _log.debug('Creating a DNP3Manager.') - threads_to_allocate = self.outstation_config.get('threads_to_allocate', 1) - # self.log_handler = asiodnp3.ConsoleLogger().Create() # (or use this during regression testing) - # self.log_handler = MyLogger().Create() - self.log_handler = MyLogger() - self.manager = asiodnp3.DNP3Manager(threads_to_allocate, self.log_handler) - - _log.debug('Creating the DNP3 channel, a TCP server.') - self.retry_parameters = asiopal.ChannelRetry().Default() - # self.listener = asiodnp3.PrintingChannelListener().Create() # (or use this during regression testing) - self.listener = AppChannelListener() - self.channel = self.manager.AddTCPServer("server", - self.dnp3_log_level(), - self.retry_parameters, - self.local_ip, - self.port, - self.listener) - - _log.debug('Adding the DNP3 Outstation to the channel.') - # self.command_handler = opendnp3.SuccessCommandHandler().Create() # (or use this during regression testing) - self.command_handler = OutstationCommandHandler() - self.outstation = self.channel.AddOutstation("outstation", self.command_handler, self, self.stack_config) - - # Set the singleton instance that communicates with the Master. - self.set_outstation(self.outstation) - - _log.info('Enabling the DNP3 Outstation. Traffic can now start to flow.') - self.outstation.Enable() - - def reload_parameters(self, local_ip, port, outstation_config): - _log.debug('In reload_parameters') - self.local_ip = local_ip - self.port = port - self.outstation_config = outstation_config - - @classmethod - def get_agent(cls): - """Return the singleton DNP3Agent or MesaAgent instance.""" - agt = cls.agent - if agt is None: - raise ValueError('Outstation has no configured agent') - return agt - - @classmethod - def set_agent(cls, agent): - """Set the singleton DNP3Agent or MesaAgent instance.""" - cls.agent = agent - - @classmethod - def get_outstation(cls): - """Get the singleton instance of IOutstation.""" - outst = cls.outstation - if outst is None: - raise AttributeError('IOutstation is not yet enabled') - return outst - - @classmethod - def set_outstation(cls, outstn): - """ - Set the singleton instance of IOutstation, as returned from the channel's AddOutstation call. - - Making IOutstation available as a singleton allows other classes - to send commands to it -- see apply_update(). - """ - cls.outstation = outstn - - @classmethod - def get_outstation_config(cls): - """Get the outstation_config, a dictionary of configuration parameters.""" - return cls.outstation_config - - @classmethod - def set_outstation_config(cls, outstn_cfg): - """ - Set the outstation_config. - - It's managed as a class variable so that it can be examined by the class method apply_update(). - - :param outstn_cfg: A dictionary of configuration parameters. - """ - cls.outstation_config = outstn_cfg - - def dnp3_log_level(self): - """ - Return a bit-encoded integer that indicates the level of DNP3 logging. - - If a list of level names is specified in the Outstation config, - use a union of those names to construct the integer. Otherwise return the default log level. - """ - log_level_list = self.outstation_config.get('log_levels', ['NORMAL']) - # log_level_list should be a list of strings. If it's not (e.g., if it's a simple string), fail. - if not isinstance(log_level_list, list): - raise TypeError('log_levels should be configured as a list of strings, not as {}'.format(log_level_list)) - log_level_list = [s.upper() for s in log_level_list] - - name_to_bitmasks = { - 'ALL': opendnp3.levels.ALL, - 'ALL_APP_COMMS': opendnp3.levels.ALL_APP_COMMS, - 'ALL_COMMS': opendnp3.levels.ALL_COMMS, - 'NORMAL': opendnp3.levels.NORMAL, - 'NOTHING': opendnp3.levels.NOTHING - } - log_level = 0 - for name in log_level_list: - log_level = log_level | name_to_bitmasks.get(name, 0) - - _log.debug('Setting DNP3 log level={} ({})'.format(log_level, log_level_list)) - return log_level - - # Overridden method - def ColdRestartSupport(self): - """Return a RestartMode enumerated type value indicating whether cold restart is supported.""" - _log.debug('In DNP3 ColdRestartSupport') - return opendnp3.RestartMode.UNSUPPORTED - - # Overridden method - def GetApplicationIIN(self): - """Return the application-controlled IIN field.""" - application_iin = opendnp3.ApplicationIIN() - application_iin.configCorrupt = False - application_iin.deviceTrouble = False - application_iin.localControl = False - application_iin.needTime = False - iin_field = application_iin.ToIIN() - # Experiment with setting iin_field to an error value, e.g. configCorrupt to indicate that - # a point couldn't be found in the Outstation's database. - # Other interesting IIN values might be PARAM_ERROR, ALREADY_EXECUTING, FUNC_NOT_SUPPORTED. - if iin_field.LSB != 0 or iin_field.MSB != 0: - status_string = 'IINField LSB={}, MSB={}'.format(iin_field.LSB, iin_field.MSB) - DNP3Outstation.get_agent().publish_outstation_status(status_string) - return application_iin - - # Overridden method - def SupportsAssignClass(self): - _log.debug('In DNP3 SupportsAssignClass') - return False - - # Overridden method - def SupportsWriteAbsoluteTime(self): - _log.debug('In DNP3 SupportsWriteAbsoluteTime') - return False - - # Overridden method - def SupportsWriteTimeAndInterval(self): - _log.debug('In DNP3 SupportsWriteTimeAndInterval') - return False - - # Overridden method - def WarmRestartSupport(self): - """Return a RestartMode enumerated value indicating whether a warm restart is supported.""" - _log.debug('In DNP3 WarmRestartSupport') - return opendnp3.RestartMode.UNSUPPORTED - - @classmethod - def apply_update(cls, value, index): - """ - Record an opendnp3 data value (Analog, Binary, etc.) in the outstation's database. - - The data value gets sent to the Master as a side-effect. - - :param value: An instance of Analog, Binary, or another opendnp3 data value. - :param index: (integer) Index of the data definition in the opendnp3 database. - """ - _log.debug('Recording DNP3 {} measurement, index={}, value={}'.format(type(value).__name__, index, value.value)) - max_index = cls.get_outstation_config().get('database_sizes', 10000) - if index > max_index: - raise ValueError('Attempt to set a value for index {} which exceeds database size {}'.format(index, - max_index)) - builder = asiodnp3.UpdateBuilder() - builder.Update(value, index) - update = builder.Build() - try: - cls.get_outstation().Apply(update) - except AttributeError as err: - if not os.environ.get('UNITTEST', False): - raise err - - def shutdown(self): - """ - Execute an orderly shutdown of the Outstation. - - The debug messages may be helpful if errors occur during shutdown. - """ - _log.debug('Exiting DNP3 Outstation module...') - self.manager.Shutdown() - _log.debug('Garbage collecting DNP3 Outstation...') - self.set_outstation(None) - _log.debug('Garbage collecting DNP3 stack config...') - self.stack_config = None - _log.debug('Garbage collecting DNP3 channel...') - self.channel = None - _log.debug('Garbage collecting DNP3Manager...') - self.manager = None - - -class OutstationCommandHandler(opendnp3.ICommandHandler): - """ - ICommandHandler implements the Outstation's handling of Select and Operate, - which relay commands and data from the Master to the Outstation. - """ - - def Start(self): - # This debug line is too chatty... - # _log.debug('In DNP3 OutstationCommandHandler.Start') - pass - - def End(self): - # This debug line is too chatty... - # _log.debug('In DNP3 OutstationCommandHandler.End') - pass - - def Select(self, command, index): - """ - The Master sent a Select command to the Outstation. Handle it. - - :param command: ControlRelayOutputBlock, - AnalogOutputInt16, AnalogOutputInt32, AnalogOutputFloat32, or AnalogOutputDouble64. - :param index: int - :return: CommandStatus - """ - return DNP3Outstation.get_agent().process_point_value('Select', command, index, None) - - def Operate(self, command, index, op_type): - """ - The Master sent an Operate command to the Outstation. Handle it. - - :param command: ControlRelayOutputBlock, - AnalogOutputInt16, AnalogOutputInt32, AnalogOutputFloat32, or AnalogOutputDouble64. - :param index: int - :param op_type: OperateType - :return: CommandStatus - """ - return DNP3Outstation.get_agent().process_point_value('Operate', command, index, op_type) - - -class AppChannelListener(asiodnp3.IChannelListener): - """ - IChannelListener has been overridden to implement application-specific channel behavior. - """ - - def __init__(self): - super(AppChannelListener, self).__init__() - - def OnStateChange(self, state): - """ - There has been an outstation state change. Publish the new state to the message bus. - - :param state: A ChannelState. - """ - DNP3Outstation.get_agent().publish_outstation_status(str(state)) - - -# class MyLogger(asiodnp3.ConsoleLogger): -class MyLogger(openpal.ILogHandler): - """ - ILogHandler has been overridden to implement application-specific logging behavior. - """ - - def __init__(self): - super(MyLogger, self).__init__() - self.tcp_client = None - - def Log(self, entry): - """Write a DNP3 log entry to the logger (debug level).""" - location = entry.location.rsplit('/')[-1] if entry.location else '' - filters = entry.filters.GetBitfield() - message = entry.message - - if "Accepted connection from" in message: - self.tcp_client = message.split(": ")[1] - - _log.debug('DNP3Log {0}\t(filters={1}) {2}'.format(location, filters, message)) - # This is here as an example of how to send a specific log entry to the message bus as outstation status. - # if 'Accepted connection' in message or 'Listening on' in message: - # DNP3Outstation.get_agent().publish_outstation_status(str(message)) - - -def main(): - """The Outstation has been started from the command line. Execute ad-hoc tests if desired.""" - dnp3_outstation = DNP3Outstation('0.0.0.0', 20000, {}) - dnp3_outstation.start() - _log.debug('DNP3 initialization complete. In command loop.') - # Ad-hoc tests can be performed at this point if desired. - dnp3_outstation.shutdown() - _log.debug('DNP3 Outstation exiting.') - exit() - -if __name__ == '__main__': - main() diff --git a/services/core/DNP3Agent/dnp3/points.py b/services/core/DNP3Agent/dnp3/points.py deleted file mode 100644 index eec4f3431a..0000000000 --- a/services/core/DNP3Agent/dnp3/points.py +++ /dev/null @@ -1,615 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, SLAC / 8minutenergy / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# This material was prepared in part as an account of work sponsored by an agency of -# the United States Government. Neither the United States Government nor the -# United States Department of Energy, nor SLAC, nor 8minutenergy, nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by the United States Government or any agency thereof, or -# SLAC, 8minutenergy, or Kisensum. The views and opinions of authors expressed -# herein do not necessarily state or reflect those of the -# United States Government or any agency thereof. -# }}} - -from datetime import datetime -import logging -import os -import pytz -import re - -from volttron.platform import jsonapi -from pydnp3 import opendnp3 -from dnp3 import POINT_TYPES, POINT_TYPE_SELECTOR_BLOCK, POINT_TYPE_ENUMERATED, POINT_TYPE_ARRAY -from dnp3 import DATA_TYPE_ANALOG_INPUT, DATA_TYPE_ANALOG_OUTPUT, DATA_TYPE_BINARY_INPUT, DATA_TYPE_BINARY_OUTPUT -from dnp3 import EVENT_CLASSES, DATA_TYPES_BY_GROUP -from dnp3 import DEFAULT_GROUP_BY_DATA_TYPE, DEFAULT_EVENT_CLASS -from dnp3 import PUBLISH_AND_RESPOND - -_log = logging.getLogger(__name__) - - -class DNP3Exception(Exception): - """Raise exceptions that are specific to the DNP3 agent. No special exception behavior is needed at this time.""" - pass - - -class PointDefinitions: - """In-memory repository of PointDefinitions.""" - - def __init__(self, point_definitions_path=None): - self._points = {} # {data_type: {point_index: PointDefinition}} - self._point_name_dict = {} # {point_name: [PointDefinition]} - if point_definitions_path: - file_path = os.path.expandvars(os.path.expanduser(point_definitions_path)) - self.load_points_from_json_file(file_path) - - def __getitem__(self, name): - """Return the PointDefinition associated with this name. Must be unique.""" - if name in [None, 'n/a']: - return None - return self.get_point_named(name) - - def load_points_from_json_file(self, point_definitions_path): - """Load and cache a dictionary of PointDefinitions, indexed by point_type and point index.""" - if point_definitions_path: - try: - file_path = os.path.expandvars(os.path.expanduser(point_definitions_path)) - _log.debug('Loading DNP3 point definitions from {}.'.format(file_path)) - with open(file_path, 'r') as f: - # Filter comments out of the file's contents before loading it as jsonapi. - self.load_points(jsonapi.loads(self.strip_comments(f.read()))) - except Exception as err: - raise ValueError('Problem parsing {}. Error={}'.format(point_definitions_path, err)) - else: - _log.debug('No point_definitions_path specified, loading no points') - - def strip_comments(self, raw_string): - """ - Return a string with comments stripped. - - Both JavaScript-style comments (//... and /*...*/) and hash (#...) comments are removed. - Thanks to VOLTTRON volttron/platform/agent/utils.py/strip_comments() for this logic. - """ - def _repl(match): - return match.group(1) or '' - - _comment_re = re.compile(r'((["\'])(?:\\?.)*?\2)|(/\*.*?\*/)|((?:#|//).*?(?=\n|$))', re.MULTILINE | re.DOTALL) - return _comment_re.sub(_repl, raw_string) - - def load_points(self, point_definitions_json): - """Load and cache a dictionary of PointDefinitions, indexed by point_type and point index.""" - try: - self._points = {} # If they're already loaded, force a reload. - for element in point_definitions_json: - # Load a PointDefinition (or subclass) from JSON, and add it to the dictionary of points. - # If the point defines an array, load additional definitions for each interior point in the array. - try: - if element.get('type', None) != POINT_TYPE_ARRAY: - point_def = PointDefinition(element) - self.update_point(point_def) - else: - point_def = ArrayHeadPointDefinition(element) - self.update_point(point_def) - # Load a separate ArrayPointDefinition for each interior point in the array. - for pt in point_def.create_array_point_definitions(element): - self.update_point(pt) - except ValueError as err: - raise DNP3Exception('Validation error for point with json: {}: {}'.format(element, err)) - except Exception as err: - raise ValueError('Problem parsing PointDefinitions. Error={}'.format(err)) - _log.debug('Loaded {} PointDefinitions'.format(len(self.all_points()))) - - def update_point(self, point_def): - """Add a PointDefinition to self._points and self._point_name_dict.""" - data_type, name, index = point_def.data_type, point_def.name, point_def.index - data_type_dict = self._points.setdefault(data_type, {}) - name_lst = self._point_name_dict.setdefault(name, []) - if index in data_type_dict: - raise ValueError('Duplicate index {} for data type {}'.format(index, data_type)) - if name_lst and point_def.type != 'array': - raise ValueError('Duplicated point name {}'.format(name)) - data_type_dict[index] = point_def - name_lst.append(point_def) - - def for_group_and_index(self, group, index): - """Return a PointDefinition for given group and index""" - data_type = DATA_TYPES_BY_GROUP.get(group, None) - if not data_type: - _log.error('No DNP3 point type found for group {}'.format(group)) - return self._points.get(data_type, {}).get(index, None) - - def point_value_for_command(self, command_type, command, index, op_type): - """ - A DNP3 Select or Operate was received from the master. Create and return a PointValue for its data. - - :param command_type: Either 'Select' or 'Operate'. - :param command: A ControlRelayOutputBlock or else a wrapped data value (AnalogOutputInt16, etc.). - :param index: DNP3 index of the payload's data definition. - :param op_type: An OperateType, or None if command_type == 'Select'. - :return: An instance of PointValue - """ - function_code = command.functionCode if type(command) == opendnp3.ControlRelayOutputBlock else None - data_type = DATA_TYPE_BINARY_OUTPUT if function_code else DATA_TYPE_ANALOG_OUTPUT - point_def = self.for_data_type_and_index(data_type, index) - if not point_def: - raise DNP3Exception('No DNP3 PointDefinition found for point type {} and index {}'.format(data_type, index)) - point_value = PointValue(command_type, - function_code, - command.value if not function_code else None, - point_def, - index, - op_type) - _log.debug('Received DNP3 {}'.format(point_value)) - return point_value - - def for_data_type_and_index(self, data_type, index): - """ - Return a PointDefinition for given data type and index. - - @param data_type: A data type (string). - @param index: Unique integer index of the PointDefinition to be looked up. - """ - return self._points.get(data_type, {}).get(index, None) - - def point_named(self, name, index=None): - """ - Return the PointDefinition with the indicated name and (optionally) index, or None if no match. - - :param name: (string) The point's name. - :param index: (integer) An optional index value. If supplied, search for an array point at this DNP3 index. - """ - point_def_list = self._point_name_dict.get(name, None) - if point_def_list is None: - return None # No points with that name - - if index is not None: - # Return the PointDefinition with a matching index. - for pt in point_def_list: - if pt.index == index: - return pt - return None - - # In multi-element lists, give preference to the ArrayHeadPointDefinition. - for pt in point_def_list: - if pt.is_array_head_point: - return pt - return point_def_list[0] - - def get_point_named(self, name, index=None): - """ - Return the PointDefinition with the indicated name and (optionally) index. - Raise an exception if none found. - - :param name: (string) The point's name. - :param index: (integer) An optional index value. If supplied, search for an array point at this DNP3 index. - :return A PointDefinition. - """ - point_def = self.point_named(name, index=index) - if point_def is None: - if index is not None: - raise DNP3Exception('No point named {} with index {}'.format(name, index)) - else: - raise DNP3Exception('No point named {}'.format(name)) - return point_def - - def all_points(self): - """Return a flat list of all PointDefinitions.""" - point_list = [] - for inner_dict in self._points.values(): - point_list.extend(inner_dict.values()) - return point_list - - -class BasePointDefinition: - """Abstract superclass for PointDefinition data holders.""" - - def __init__(self, element_def): - """Initialize an instance of the PointDefinition from a dictionary of point attributes.""" - self.name = str(element_def.get('name', '')) - self.data_type = element_def.get('data_type', None) - self.index = element_def.get('index', None) - self.type = element_def.get('type', None) - self.description = element_def.get('description', '') - self.scaling_multiplier = element_def.get('scaling_multiplier', 1) # Only used for Analog data_type - self.units = element_def.get('units', '') - self.event_class = element_def.get('event_class', DEFAULT_EVENT_CLASS) - self.selector_block_start = element_def.get('selector_block_start', None) - self.selector_block_end = element_def.get('selector_block_end', None) - self.action = element_def.get('action', None) - self.response = element_def.get('response', None) - self.category = element_def.get('category', None) - self.ln_class = element_def.get('ln_class', None) - self.data_object = element_def.get('data_object', None) - self.common_data_class = element_def.get('common_data_class', None) - self.minimum = element_def.get('minimum', -2147483648) # Only used for Analog data_type - self.maximum = element_def.get('maximum', 2147483647) # Only used for Analog data_type - self.scaling_offset = element_def.get('scaling_offset', 0) # Only used for Analog data_type - self.allowed_values = self.convert_allowed_values(element_def.get('allowed_values', None)) - - @property - def is_enumerated(self): - return self.type == POINT_TYPE_ENUMERATED - - @property - def is_array_point(self): - return False - - @property - def is_array_head_point(self): - return False - - @property - def is_array(self): - return self.is_array_point or self.is_array_head_point - - def convert_allowed_values(self, allowed_values): - if allowed_values: - return {int(str_val): description for str_val, description in allowed_values.items()} - return None - - def validate_point(self): - """A PointDefinition has been created. Perform a variety of validations on it.""" - if not self.name: - raise ValueError('Missing point name') - if self.index is None: - raise ValueError('Missing index for point {}'.format(self.name)) - if not self.data_type: - raise ValueError('Missing data type for point {}'.format(self.name)) - if self.data_type not in DEFAULT_GROUP_BY_DATA_TYPE: - raise ValueError('Invalid data type {} for point {}'.format(self.data_type, self.name)) - if not self.eclass: - raise ValueError('Invalid event class {} for point {}'.format(self.event_class, self.name)) - if self.type and self.type not in POINT_TYPES: - raise ValueError('Invalid type {} for point {}'.format(self.type, self.name)) - if self.action == PUBLISH_AND_RESPOND and not self.response: - raise ValueError('Missing response point name for point {}'.format(self.name)) - if self.is_enumerated and not self.allowed_values: - raise ValueError('Missing allowed values mapping for point {}'.format(self.name)) - if self.is_selector_block: - if self.selector_block_start is None: - raise ValueError('Missing selector_block_start for block named {}'.format(self.name)) - if self.selector_block_end is None: - raise ValueError('Missing selector_block_end for block named {}'.format(self.name)) - if self.selector_block_start > self.selector_block_end: - raise ValueError('Selector block end index < start index for block named {}'.format(self.name)) - else: - if self.selector_block_start is not None: - raise ValueError('selector_block_start defined for non-selector-block point {}'.format(self.name)) - if self.selector_block_end is not None: - raise ValueError('selector_block_end defined for non-selector-block point {}'.format(self.name)) - - def as_json(self): - """Return a json description of the PointDefinition.""" - point_json = { - "name": self.name, - "data_type": self.data_type, - "index": self.index, - "group": self.group, - "event_class": self.event_class - } - if self.type: - point_json["type"] = self.type - if self.description: - point_json["description"] = self.description - if self.units: - point_json["units"] = self.units - if self.selector_block_start is not None: - point_json["selector_block_start"] = self.selector_block_start - if self.selector_block_end is not None: - point_json["selector_block_end"] = self.selector_block_end - if self.allowed_values: - point_json["allowed_values"] = self.allowed_values - if self.action: - point_json["action"] = self.action - if self.response: - point_json["response"] = self.response - if self.category: - point_json["category"] = self.category - if self.ln_class: - point_json["ln_class"] = self.ln_class - if self.data_object: - point_json["data_object"] = self.data_object - if self.common_data_class: - point_json["common_data_class"] = self.common_data_class - if self.data_type in [DATA_TYPE_ANALOG_INPUT, DATA_TYPE_ANALOG_OUTPUT]: - point_json.update({ - "scaling_multiplier": self.scaling_multiplier, - "scaling_offset": self.scaling_offset, - "minimum": self.minimum, - "maximum": self.maximum - }) - - return point_json - - def __str__(self): - """Return a string description of the PointDefinition.""" - try: - return '{0} {1} (event_class={2}, index={3}, type={4})'.format( - self.__class__.__name__, - self.name, - self.event_class, - self.index, - self.data_type - ) - except UnicodeEncodeError as err: - _log.error('Unable to convert point definition to string, err = {}'.format(err)) - return '' - - @property - def group(self): - return DEFAULT_GROUP_BY_DATA_TYPE.get(self.data_type, None) - - @property - def is_input(self): - """Return True if the PointDefinition is a Binary or Analog input point (i.e., sent by the Outstation).""" - return self.data_type in [DATA_TYPE_ANALOG_INPUT, DATA_TYPE_BINARY_INPUT] - - @property - def is_output(self): - """Return True if the PointDefinition is a Binary or Analog output point (i.e., sent by the Master).""" - return self.data_type in [DATA_TYPE_ANALOG_OUTPUT, DATA_TYPE_BINARY_OUTPUT] - - @property - def is_selector_block(self): - return self.type == POINT_TYPE_SELECTOR_BLOCK - - @property - def eclass(self): - """Return the PointDefinition's event class, or the default (2) if no event class was defined for the point.""" - return EVENT_CLASSES.get(self.event_class, None) - - -class PointDefinition(BasePointDefinition): - """Data holder for an OpenDNP3 data element.""" - - def __init__(self, element_def): - """Initialize an instance of the PointDefinition from a dictionary of point attributes.""" - super(PointDefinition, self).__init__(element_def) - self.validate_point() - - def validate_point(self): - """A PointDefinition has been created. Perform a variety of validations on it.""" - super(PointDefinition, self).validate_point() - if self.type and self.type not in ['selector_block', 'enumerated']: - raise ValueError('Invalid type for {}: {}'.format(self.name, self.type)) - - -class ArrayHeadPointDefinition(BasePointDefinition): - """Data holder for an OpenDNP3 data element that is the head point in an array.""" - - def __init__(self, json_element): - """ - Initialize an ArrayPointDefinition instance. - An ArrayPointDefinition defines an interior point (not the head point) in an array. - - :param json_element: A JSON dictionary of point attributes. - """ - super(ArrayHeadPointDefinition, self).__init__(json_element) - self.array_points = json_element.get('array_points', None) - self.array_times_repeated = json_element.get('array_times_repeated', None) - self.array_point_definitions = [] # Holds all ArrayPointDefinitions belonging to this array. - self.validate_point() - - def validate_point(self): - """An ArrayHeadPointDefinition has been created. Perform a variety of validations on it.""" - super(ArrayHeadPointDefinition, self).validate_point() - if self.type != 'array': - raise ValueError('Invalid type {} for array named {}'.format(self.type, self.name)) - if self.array_points is None: - raise ValueError('Missing array_points for array named {}'.format(self.name)) - if self.array_times_repeated is None: - raise ValueError('Missing array_times_repeated for array named {}'.format(self.name)) - - @property - def is_array_point(self): - return True - - @property - def is_array_head_point(self): - return True - - def as_json(self): - """Return a json description of the ArrayHeadPointDefinition.""" - point_json = super(ArrayHeadPointDefinition, self).as_json() - # array_points has been excluded because it's not a simple data type. Is it needed in the json? - # if self.array_points is not None: - # point_json["array_points"] = self.array_points - if self.array_times_repeated is not None: - point_json["array_times_repeated"] = self.array_times_repeated - return point_json - - @property - def array_last_index(self): - """Calculate and return the array's last index value.""" - if self.is_array_head_point: - return self.index + self.array_times_repeated * len(self.array_points) - 1 - else: - return None - - def create_array_point_definitions(self, element): - """Create a separate ArrayPointDefinition for each interior point in the array.""" - for row_number in range(self.array_times_repeated): - for column_number, pt in enumerate(self.array_points): - # The ArrayHeadPointDefinition is already defined -- don't create a redundant definition. - if row_number > 0 or column_number > 0: - array_pt_def = ArrayPointDefinition(element, self, row_number, column_number, pt['name']) - self.array_point_definitions.append(array_pt_def) - return self.array_point_definitions - - -class ArrayPointDefinition(BasePointDefinition): - """Data holder for an OpenDNP3 data element that is interior to an array.""" - - def __init__(self, json_element, base_point_def, row, column, array_element_name): - """ - Initialize an ArrayPointDefinition instance. - An ArrayPointDefinition defines an interior point (not the head point) in an array. - - :param json_element: A JSON dictionary of point attributes. - :param base_point_def: The PointDefinition of the head point in the array. - :param row: The point's row number in the array. - :param column: The point's column number in the array. - :param array_element_name: The point's column name in the array. - """ - super(ArrayPointDefinition, self).__init__(json_element) - self.base_point_def = base_point_def - self.row = row - self.column = column - self.index = self.base_point_def.index + row * len(self.base_point_def.array_points) + column - self.array_element_name = array_element_name - self.validate_point() - - def validate_point(self): - """An ArrayPointDefinition has been created. Perform a variety of validations on it.""" - super(ArrayPointDefinition, self).validate_point() - if self.type != 'array': - raise ValueError('Invalid type {} for array named {}'.format(self.type, self.name)) - if self.base_point_def is None: - raise ValueError('Missing base point definition for array point named {}'.format(self.name)) - if self.row is None: - raise ValueError('Missing row number for array point named {}'.format(self.name)) - if self.column is None: - raise ValueError('Missing column number for array point named {}'.format(self.name)) - if self.index is None: - raise ValueError('Missing index value for array point named {}'.format(self.name)) - if self.array_element_name is None: - raise ValueError('Missing array element name for array point named {}'.format(self.name)) - - @property - def is_array_point(self): - return True - - @property - def is_array_head_point(self): - return False - - def as_json(self): - """Return a json description of the ArrayPointDefinition.""" - point_json = super(ArrayPointDefinition, self).as_json() - if self.row is not None: - point_json["row"] = self.row - if self.row is not None: - point_json["column"] = self.column - if self.row is not None: - point_json["array_element_name"] = self.array_element_name - return point_json - - -class PointValue: - """Data holder for a point value (DNP3 measurement or command) received by an outstation.""" - - def __init__(self, command_type, function_code, value, point_def, index, op_type): - """Initialize an instance of the PointValue.""" - # Don't rely on VOLTTRON utils in this code, which may run outside of VOLTTRON - # self.when_received = utils.get_aware_utc_now() - self.when_received = pytz.UTC.localize(datetime.utcnow()) - self.command_type = command_type - self.function_code = function_code - self.value = value - self.point_def = point_def - self.index = index # MESA Array point indexes can differ from the indexes of their PointDefinitions. - self.op_type = op_type - - def __str__(self): - """Return a string description of the PointValue.""" - str_desc = 'Point value {0} ({1}, {2}.{3}, {4})' - return str_desc.format(self.value or self.function_code, - self.name, - self.point_def.event_class, - self.index, - self.command_type) - - @property - def name(self): - """Return the name of the PointDefinition.""" - return self.point_def.name - - def unwrapped_value(self): - """Unwrap the point's value, returning the sample data type (e.g. an integer, binary, etc. instance).""" - if self.value is None: - # For binary commands, send True if function_code is LATCH_ON, False otherwise - return self.function_code == opendnp3.ControlCode.LATCH_ON - else: - return self.value - - -class PointArray: - """Data holder for a MESA-ESS Array.""" - - def __init__(self, point_def): - """ - The "points" variable is a dictionary of dictionaries: - 0: { - 0: PointValue, - 1: PointValue - }, - 1: { - 0: PointValue, - 1: PointValue - } - (etc.) - It's stored as dictionaries indexed by index numbers, not as lists, - because there's no guarantee that array elements will arrive in order. - - :param point_def: The PointDefinition of the array's head point. - """ - _log.debug('New Array {} starting at {} with bounds ({}, {})'.format(point_def.name, - point_def.index, - point_def.index, - point_def.array_last_index)) - self.point_def = point_def - self.points = {} - - def __str__(self): - return 'Array, points = {}'.format(self.points) - - def as_json(self): - """ - Return a JSON representation of the PointArray: - - [ - {name1: val1a, name2: val2a, ...}, - {name1: val1b, name2: val2b, ...}, - ... - ] - """ - names = [d['name'] for d in self.point_def.array_points] - json_array = [] - for pt_dict_key in sorted(self.points): - pt_dict = self.points[pt_dict_key] - json_array.append({name: (pt_dict[i].value if i in pt_dict else None) for i, name in enumerate(names)}) - return json_array - - def contains_index(self, index): - """Answer whether this Array contains the point index.""" - return self.point_def.index <= index <= self.point_def.array_last_index - - def add_point_value(self, point_value): - """Add point_value to the Array's "points" dictionary.""" - point_def = point_value.point_def - row = 0 if point_def.is_array_head_point else point_def.row - col = 0 if point_def.is_array_head_point else point_def.column - if row not in self.points: - self.points[row] = {} - self.points[row][col] = point_value diff --git a/services/core/DNP3Agent/dnp3_master.py b/services/core/DNP3Agent/dnp3_master.py deleted file mode 100644 index 166026959f..0000000000 --- a/services/core/DNP3Agent/dnp3_master.py +++ /dev/null @@ -1,517 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, 8minutenergy / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Neither 8minutenergy nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by 8minutenergy or Kisensum. -# }}} - -import logging - -from pydnp3 import opendnp3, openpal, asiopal, asiodnp3 - -_log = logging.getLogger(__name__) - - -class DNP3Master: - """ - Interface for all master application callback info except for measurement values. - """ - - def __init__(self, - log_levels=opendnp3.levels.NORMAL | opendnp3.levels.ALL_APP_COMMS, - host_ip="127.0.0.1", # presumably outstation - local_ip="0.0.0.0", - port=20000, - log_handler=asiodnp3.ConsoleLogger().Create(), - channel_listener=asiodnp3.PrintingChannelListener().Create(), - soe_handler=asiodnp3.PrintingSOEHandler().Create(), - platform_application=asiodnp3.DefaultMasterApplication().Create(), - stack_config=None): - - self.log_levels = log_levels - self.host_ip = host_ip - self.local_ip = local_ip - self.port = port - self.log_handler = log_handler - self.channel_listener = channel_listener - self.soe_handler = soe_handler - self.platform_application = platform_application - - self.stackConfig = stack_config - if not self.stackConfig: - # The master config object for a master. - self.stackConfig = asiodnp3.MasterStackConfig() - self.stackConfig.master.responseTimeout = openpal.TimeDuration().Seconds(2) - self.stackConfig.link.RemoteAddr = 10 - - self.manager = None - self.channel = None - self.master = None - - def connect(self): - """Connect to an outstation, add an master to the channel, and start the communications.""" - - # Root DNP3 object used to create channels and sessions - if not self.manager: - self.manager = asiodnp3.DNP3Manager(1, self.log_handler) - - # Connect via a TCPClient socket to a outstation - self.channel = self.manager.AddTCPClient("tcpclient", - self.log_levels, - asiopal.ChannelRetry(), - self.host_ip, - self.local_ip, - self.port, - self.channel_listener) - - # Create a new master on a previously declared port, with a name, log level, command acceptor, and config info. - # This returns a thread-safe interface used for sending commands. - self.master = self.channel.AddMaster("master", - self.soe_handler, - self.platform_application, - self.stackConfig) - - # Enable the master. This will start communications. - self.master.Enable() - - def reconnect(self, host_ip, port): - """Reconnect master to a different host and port and start the communications.""" - if self.master: - self.master.Disable() - - if self.channel: - self.channel.Shutdown() - - self.host_ip = host_ip - self.port = port - self.connect() - - def send_read_command(self, group, variation, index): - """ - Request to read a point data from outstation - - :param group: group of the point data - :param variation: variation of the point data - :param index: index of the point data - :param data_type: 'analog' or 'binary' - """ - self.master.PerformFunction('READ', - opendnp3.FunctionCode.READ, - [opendnp3.Header().Range16(group, variation, index, index + 1)]) - - def send_unsolicited_response_command(self, group, variation, index): - """ - Unsolicited response that was not prompted by an explicit request - - :param group: group of the point data - :param variation: variation of the point data - :param index: index of the point data - :param data_type: 'analog' or 'binary' - """ - self.master.PerformFunction('UNSOLICITED_RESPONSE', - opendnp3.FunctionCode.UNSOLICITED_RESPONSE, - [opendnp3.Header().Range16(group, variation, index, index + 1)]) - - def send_direct_operate_command(self, command, index, callback=asiodnp3.PrintingCommandCallback.Get(), - config=opendnp3.TaskConfig().Default()): - """ - Direct operate a single command - - :param command: command to operate - :param index: index of the command - :param callback: callback that will be invoked upon completion or failure - :param config: optional configuration that controls normal callbacks and allows the user to be specified for SA - """ - self.master.DirectOperate(command, index, callback, config) - - def send_direct_operate_command_set(self, command_set, callback=asiodnp3.PrintingCommandCallback.Get(), - config=opendnp3.TaskConfig().Default()): - """ - Direct operate a set of commands - - :param command_set: set of command headers - :param callback: callback that will be invoked upon completion or failure - :param config: optional configuration that controls normal callbacks and allows the user to be specified for SA - """ - self.master.DirectOperate(command_set, callback, config) - - def send_select_and_operate_command(self, command, index, callback=asiodnp3.PrintingCommandCallback.Get(), - config=opendnp3.TaskConfig().Default()): - """ - Select and operate a single command - - :param command: command to operate - :param index: index of the command - :param callback: callback that will be invoked upon completion or failure - :param config: optional configuration that controls normal callbacks and allows the user to be specified for SA - """ - self.master.SelectAndOperate(command, index, callback, config) - - def send_select_and_operate_command_set(self, command_set, callback=asiodnp3.PrintingCommandCallback.Get(), - config=opendnp3.TaskConfig().Default()): - """ - Select and operate a set of commands - - :param command_set: set of command headers - :param callback: callback that will be invoked upon completion or failure - :param config: optional configuration that controls normal callbacks and allows the user to be specified for SA - """ - self.master.SelectAndOperate(command_set, callback, config) - - def shutdown(self): - """ - Shutdown manager and terminate the threadpool - """ - del self.master - del self.channel - if self.manager: - self.manager.Shutdown() - - -class VisitorIndexedBinary(opendnp3.IVisitorIndexedBinary): - """ - Override IVisitorIndexedBinary in this manner to implement visiting elements of IndexedBinary collection. - - This is used in SOEHandler callback. - """ - - def __init__(self): - super(VisitorIndexedBinary, self).__init__() - self.index_and_value = [] - - def OnValue(self, indexed_instance): - """ - Process current value visiting. - - :param indexed_instance: current value visiting, an instance of IndexedBinary - """ - self.index_and_value.append((indexed_instance.index, indexed_instance.value.value)) - - -class VisitorIndexedDoubleBitBinary(opendnp3.IVisitorIndexedDoubleBitBinary): - """ - Override IVisitorIndexedDoubleBitBinary in this manner to implement visiting elements of IndexedDoubleBitBinary - collection. - - This is used in SOEHandler callback. - """ - - def __init__(self): - super(VisitorIndexedDoubleBitBinary, self).__init__() - self.index_and_value = [] - - def OnValue(self, indexed_instance): - """ - Process current value visiting. - - :param indexed_instance: current value visiting, an instance of IndexedDoubleBitBinary - """ - self.index_and_value.append((indexed_instance.index, indexed_instance.value.value)) - - -class VisitorIndexedCounter(opendnp3.IVisitorIndexedCounter): - """ - Override IVisitorIndexedCounter in this manner to implement visiting elements of IndexedCounter collection. - - This is used in SOEHandler callback. - """ - - def __init__(self): - super(VisitorIndexedCounter, self).__init__() - self.index_and_value = [] - - def OnValue(self, indexed_instance): - """ - Process current value visiting. - - :param indexed_instance: current value visiting, an instance of IndexedCounter - """ - self.index_and_value.append((indexed_instance.index, indexed_instance.value.value)) - - -class VisitorIndexedFrozenCounter(opendnp3.IVisitorIndexedFrozenCounter): - """ - Override IVisitorIndexedFrozenCounter in this manner to implement visiting elements of IndexedFrozenCounter - collection. - - This is used in SOEHandler callback. - """ - - def __init__(self): - super(VisitorIndexedFrozenCounter, self).__init__() - self.index_and_value = [] - - def OnValue(self, indexed_instance): - """ - Process current value visiting. - - :param indexed_instance: current value visiting, an instance of IndexedFrozenCounter - """ - self.index_and_value.append((indexed_instance.index, indexed_instance.value.value)) - - -class VisitorIndexedAnalog(opendnp3.IVisitorIndexedAnalog): - """ - Override IVisitorIndexedAnalog in this manner to implement visiting elements of IndexedAnalog collection. - - This is used in SOEHandler callback. - """ - - def __init__(self): - super(VisitorIndexedAnalog, self).__init__() - self.index_and_value = [] - - def OnValue(self, indexed_instance): - """ - Process current value visiting. - - :param indexed_instance: current value visiting, an instance of IndexedAnalog - """ - self.index_and_value.append((indexed_instance.index, indexed_instance.value.value)) - - -class VisitorIndexedBinaryOutputStatus(opendnp3.IVisitorIndexedBinaryOutputStatus): - """ - Override IVisitorIndexedBinaryOutputStatus in this manner to implement visiting elements of - IndexedBinaryOutputStatus collection. - - This is used in SOEHandler callback. - """ - - def __init__(self): - super(VisitorIndexedBinaryOutputStatus, self).__init__() - self.index_and_value = [] - - def OnValue(self, indexed_instance): - """ - Process current value visiting. - - :param indexed_instance: current value visiting, an instance of IndexedBinaryOutputStatus - """ - self.index_and_value.append((indexed_instance.index, indexed_instance.value.value)) - - -class VisitorIndexedAnalogOutputStatus(opendnp3.IVisitorIndexedAnalogOutputStatus): - """ - Override IVisitorIndexedAnalogOutputStatus in this manner to implement visiting elements of - IndexedAnalogOutputStatus collection. - - This is used in SOEHandler callback. - """ - - def __init__(self): - super(VisitorIndexedAnalogOutputStatus, self).__init__() - self.index_and_value = [] - - def OnValue(self, indexed_instance): - """ - Process current value visiting. - - :param indexed_instance: current value visiting, an instance of IndexedAnalogOutputStatus - """ - self.index_and_value.append((indexed_instance.index, indexed_instance.value.value)) - - -class VisitorIndexedTimeAndInterval(opendnp3.IVisitorIndexedTimeAndInterval): - """ - Override IVisitorIndexedTimeAndInterval in this manner to implement visiting elements of - IndexedTimeAndInterval collection. - - This is used in SOEHandler callback. - """ - - def __init__(self): - super(VisitorIndexedTimeAndInterval, self).__init__() - self.index_and_value = [] - - def OnValue(self, indexed_instance): - """ - Process current value visiting. - - :param indexed_instance: current value visiting, an instance of IndexedTimeAndInterval - """ - # The TimeAndInterval class is a special case, because it doesn't have a "value" per se. - ti_instance = indexed_instance.value - ti_dnptime = ti_instance.time - ti_interval = ti_instance.interval - self.index_and_value.append((indexed_instance.index, (ti_dnptime.value, ti_interval))) - - -class LogHandler(openpal.ILogHandler): - """ - Override ILogHandler in this manner to implement application-specific logging behavior. - """ - - def __init__(self): - super(LogHandler, self).__init__() - - def Log(self, entry): - flag = opendnp3.LogFlagToString(entry.filters.GetBitfield()) - filters = entry.filters.GetBitfield() - location = entry.location.rsplit('/')[-1] if entry.location else '' - message = entry.message - _log.debug('LOG\t\t{:<10}\tfilters={:<5}\tlocation={:<25}\tentry={}'.format(flag, filters, location, message)) - - -class ChannelListener(asiodnp3.IChannelListener): - """ - Override IChannelListener in this manner to implement application-specific channel behavior. - """ - - def __init__(self): - super(ChannelListener, self).__init__() - - def OnStateChange(self, state): - _log.debug('In AppChannelListener.OnStateChange: state={}'.format(opendnp3.ChannelStateToString(state))) - - -class SOEHandler(opendnp3.ISOEHandler): - """ - Override ISOEHandler in this manner to implement application-specific sequence-of-events behavior. - - This is an interface for SequenceOfEvents (SOE) callbacks from the Master stack to the application layer. - """ - - def __init__(self): - super(SOEHandler, self).__init__() - self.result = { - "Binary": {}, - "DoubleBitBinary": {}, - "Counter": {}, - "FrozenCounter": {}, - "Analog": {}, - "BinaryOutputStatus": {}, - "AnalogOutputStatus": {}, - "TimeAndInterval": {} - } - - def Process(self, info, values): - """ - Process measurement data. - - :param info: HeaderInfo - :param values: A collection of values received from the Outstation (various data types are possible). - """ - visitor_class_types = { - opendnp3.ICollectionIndexedBinary: VisitorIndexedBinary, - opendnp3.ICollectionIndexedDoubleBitBinary: VisitorIndexedDoubleBitBinary, - opendnp3.ICollectionIndexedCounter: VisitorIndexedCounter, - opendnp3.ICollectionIndexedFrozenCounter: VisitorIndexedFrozenCounter, - opendnp3.ICollectionIndexedAnalog: VisitorIndexedAnalog, - opendnp3.ICollectionIndexedBinaryOutputStatus: VisitorIndexedBinaryOutputStatus, - opendnp3.ICollectionIndexedAnalogOutputStatus: VisitorIndexedAnalogOutputStatus, - opendnp3.ICollectionIndexedTimeAndInterval: VisitorIndexedTimeAndInterval - } - - visitor_class = visitor_class_types[type(values)] - visitor = visitor_class() - - # Visit all the elements of a collection - values.Foreach(visitor) - - for index, value in visitor.index_and_value: - self.result[type(values).__name__.split("ICollectionIndexed")[1]][index] = value - - def Start(self): - pass - - def End(self): - pass - - -class MasterApplication(opendnp3.IMasterApplication): - def __init__(self): - super(MasterApplication, self).__init__() - - # Overridden method - def AssignClassDuringStartup(self): - _log.debug('In MasterApplication.AssignClassDuringStartup') - return False - - # Overridden method - def OnClose(self): - _log.debug('In MasterApplication.OnClose') - - # Overridden method - def OnOpen(self): - _log.debug('In MasterApplication.OnOpen') - - # Overridden method - def OnReceiveIIN(self, iin): - _log.debug('In MasterApplication.OnReceiveIIN') - - # Overridden method - def OnTaskComplete(self, info): - _log.debug('In MasterApplication.OnTaskComplete') - - # Overridden method - def OnTaskStart(self, type, id): - _log.debug('In MasterApplication.OnTaskStart') - - -def collection_callback(result=None): - """ - :type result: opendnp3.CommandPointResult - """ - print("Header: {0} | Index: {1} | State: {2} | Status: {3}".format( - result.headerIndex, - result.index, - opendnp3.CommandPointStateToString(result.state), - opendnp3.CommandStatusToString(result.status) - )) - - -def command_callback(result=None): - """ - :type result: opendnp3.ICommandTaskResult - """ - print("Received command result with summary: {}".format(opendnp3.TaskCompletionToString(result.summary))) - result.ForeachItem(collection_callback) - - -def restart_callback(result=opendnp3.RestartOperationResult()): - if result: - if result.summary == opendnp3.TaskCompletion.SUCCESS: - status_message = "Success, Time: {0}".format(result.restartTime.GetMilliseconds()) - else: - status_message = "Failure: {0}".format(opendnp3.TaskCompletionToString(result.summary)) - else: - status_message = "Failure: No result returned" - - _log.debug(status_message) - - -def main(): - dnp3_master = DNP3Master(log_handler=LogHandler(), - channel_listener=ChannelListener(), - soe_handler=SOEHandler(), - platform_application=MasterApplication()) - dnp3_master.connect() - # Ad-hoc tests can be inserted here if desired. - dnp3_master.shutdown() - - -if __name__ == '__main__': - main() diff --git a/services/core/DNP3Agent/function_test.py b/services/core/DNP3Agent/function_test.py deleted file mode 100644 index 150c41a89f..0000000000 --- a/services/core/DNP3Agent/function_test.py +++ /dev/null @@ -1,150 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, 8minutenergy / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Neither 8minutenergy nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by 8minutenergy or Kisensum. -# }}} - - -import os - -from dnp3.mesa.functions import FunctionDefinitions -from dnp3.points import PointDefinitions -from dnp3 import DATA_TYPES_BY_GROUP -from dnp3 import DATA_TYPE_ANALOG_OUTPUT, DATA_TYPE_BINARY_OUTPUT - -from volttron.platform import jsonapi -POINT_DEF_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), 'tests', 'data', 'mesa_points.config')) -FUNCTION_DEF_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), 'tests', 'data', 'mesa_functions.yaml')) - -DATA_TYPE_TO_PYTHON_TYPE = { - DATA_TYPE_BINARY_OUTPUT: {bool}, - DATA_TYPE_ANALOG_OUTPUT: {int, float}, -} - - -class FunctionTestException(Exception): - pass - - -class FunctionTest: - - def __init__(self, func_test_path='', func_test_json=None, func_def_path='', point_def_path=''): - self.func_def_path = func_def_path or FUNCTION_DEF_PATH - self.point_definitions = PointDefinitions(point_definitions_path=point_def_path or POINT_DEF_PATH) - self.ftest = func_test_json or jsonapi.load(open(func_test_path)) - self.function_id = self.ftest.get('function_id', self.ftest.get('id', None)) - self.function_name = self.ftest.get('function_name', self.ftest.get('name', None)) - self.name = self.ftest.get('name', None) - self.points = {k: v for k, v in self.ftest.items() if k not in ["name", "function_id", "function_name", "id"]} - - def get_function_def(self): - """ - Gets the function definition for the function test. Returns None if no definition is found. - """ - fdefs = FunctionDefinitions(point_definitions=self.point_definitions, - function_definitions_path=self.func_def_path) - return fdefs.function_for_id(self.function_id) - - @staticmethod - def get_mandatory_steps(func_def): - """ - Returns list of mandatory steps for the given function definition. - - :param func_def: function definition - """ - return [step.name for step in func_def.steps if step.optional in ['M', 'I']] - - def has_mandatory_steps(self, fdef=None): - """ - Returns True if the instance has all required steps, and raises an exception if not. - - :param fdef: function definition - """ - fdef = fdef or self.get_function_def() - if not fdef: - raise FunctionTestException("Function definition not found: {}".format(self.function_id)) - - missing_steps = list(set(self.get_mandatory_steps(fdef)) - set(self.ftest.keys())) - if missing_steps: - raise FunctionTestException("Function Test missing mandatory steps: {}".format(missing_steps)) - - return True - - def points_resolve(self, func_def): - """ - Returns true if all the points in the instance resolve to point names in the function definition, - and raises an exception if not. - - :param func_def: function definition of the given instance - """ - # It would have been more informative to identify the mismatched step/point name, - # but that would break a pytest assertion that matches on this specific exception description. - if not all(step_name in [step.point_def.name for step in func_def.steps if step.point_def] for step_name in - self.points.keys()): - raise FunctionTestException("Not all points resolve") - return True - - def correct_point_types(self): - """ - Check valid point value. - """ - for point_name, point_value in self.points.items(): - point_def = self.point_definitions.point_named(point_name) - point_values = sum([list(v.values()) for v in point_value], []) if point_def.is_array else [point_value] - for value in point_values: - if type(value) not in DATA_TYPE_TO_PYTHON_TYPE[DATA_TYPES_BY_GROUP[point_def.group]]: - # It would have been more informative to display the value and/or type in the error message, - # but that would break a pytest assertion that matches on this specific exception description. - raise FunctionTestException("Invalid point value: {}".format(point_name)) - return True - - def is_valid(self): - """ - Returns True if the function test passes two validation steps: - 1. it has all the mandatory steps - 2. its point names resolve to point names in the function definition - 3. its point value is valid - If the function test is invalid, an exception is raised. - """ - f_def = self.get_function_def() - - try: - self.has_mandatory_steps(f_def) - self.points_resolve(f_def) - self.correct_point_types() - return True - except Exception as err: - raise FunctionTestException("Validation Error: {}".format(str(err))) - - -def main(): - function_test = FunctionTest(func_test_path=os.path.abspath(os.path.join(os.path.dirname(__file__), - 'tests', 'data', 'watt_var_curve.json'))) - function_test.is_valid() - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/services/core/DNP3Agent/install_dnp3_agent.sh b/services/core/DNP3Agent/install_dnp3_agent.sh deleted file mode 100644 index 420ae1dd4d..0000000000 --- a/services/core/DNP3Agent/install_dnp3_agent.sh +++ /dev/null @@ -1,19 +0,0 @@ -# This script assumes that $VOLTTRON_ROOT is the directory where VOLTTRON source code is loaded from github. - -export DNP3_ROOT=$VOLTTRON_ROOT/services/core/DNP3Agent - -# Install the agent that resides in the dnp3 subdirectory -export AGENT_MODULE=dnp3.agent - -cd $VOLTTRON_ROOT - -python scripts/install-agent.py -s $DNP3_ROOT -i dnp3agent -c $DNP3_ROOT/config -t dnp3agent -f - -# Put the agent's point definitions in the config store. -cd $VOLTTRON_ROOT -vctl config store dnp3agent mesa_points.config $DNP3_ROOT/dnp3/mesa_points.config - -echo -echo Stored point configurations in config store... -vctl config list dnp3agent -echo diff --git a/services/core/DNP3Agent/install_mesa_agent.sh b/services/core/DNP3Agent/install_mesa_agent.sh deleted file mode 100644 index b84ed53189..0000000000 --- a/services/core/DNP3Agent/install_mesa_agent.sh +++ /dev/null @@ -1,25 +0,0 @@ -#/bin/sh -# This script assumes that $VOLTTRON_ROOT is the directory where VOLTTRON source code is loaded from github. - -export DNP3_ROOT=$VOLTTRON_ROOT/services/core/DNP3Agent - -# Install the agent that resides in the dnp3.mesa subdirectory -export AGENT_MODULE=dnp3.mesa.agent - -cd $VOLTTRON_ROOT - -python scripts/install-agent.py -s $DNP3_ROOT -i mesaagent -c $DNP3_ROOT/mesaagent.config -t mesaagent -f - -# Convert function YAML to JSON -cd $DNP3_ROOT -python dnp3/mesa/conversion.py < dnp3/mesa/mesa_functions.yaml > dnp3/mesa/mesa_functions.config - -# Put the agent's point definitions and function definitions in the config store. -cd $VOLTTRON_ROOT -vctl config store mesaagent mesa_points.config $DNP3_ROOT/dnp3/mesa_points.config -vctl config store mesaagent mesa_functions.config $DNP3_ROOT/dnp3/mesa/mesa_functions.config - -echo -echo Stored point and function configurations in config store... -vctl config list mesaagent -echo diff --git a/services/core/DNP3Agent/mesa_master.py b/services/core/DNP3Agent/mesa_master.py deleted file mode 100644 index 803caa01a7..0000000000 --- a/services/core/DNP3Agent/mesa_master.py +++ /dev/null @@ -1,142 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, 8minutenergy / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Neither 8minutenergy nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by 8minutenergy or Kisensum. -# }}} - -import os -import time - -from pydnp3 import opendnp3 - -from dnp3 import DIRECT_OPERATE, SELECT, OPERATE, DATA_TYPE_BINARY_OUTPUT -from dnp3.points import PointDefinitions -from dnp3_master import DNP3Master -from function_test import FunctionTest - -POINT_DEF_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), 'tests', 'data', 'mesa_points.config')) - -OUTPUT_TYPES = { - float: opendnp3.AnalogOutputFloat32, - int: opendnp3.AnalogOutputInt32, - bool: opendnp3.ControlRelayOutputBlock -} - - -class MesaMasterException(Exception): - pass - - -class MesaMaster(DNP3Master): - - def __init__(self, **kwargs): - DNP3Master.__init__(self, **kwargs) - - self.SEND_FUNCTIONS = { - DIRECT_OPERATE: self.send_direct_operate_command, - SELECT: self.send_select_and_operate_command, - OPERATE: self.send_select_and_operate_command, - } - - def send_command(self, send_func, pdef, point_value, index=None): - """ - Send a command to outstation. Check for valid value to send. - - :param send_func: send_direct_operate_command or send_select_and_operate_command - :param pdef: point definition - :param point_value: value of the point that will be sent to outstation - :param index: index of the point from point definition - """ - value_type = type(point_value) - - if pdef.data_type == DATA_TYPE_BINARY_OUTPUT: - point_value = opendnp3.ControlCode.LATCH_ON if point_value else opendnp3.ControlCode.LATCH_OFF - - try: - send_func(OUTPUT_TYPES[value_type](point_value), index or pdef.index) - time.sleep(0.2) - except KeyError: - raise MesaMasterException("Unrecognized output type: {}".format(value_type)) - - def send_array(self, json_array, pdef): - """ - Send point array to outstation. - - :param json_array: json array of points and values that will be sent to outstation - :param pdef: point definition - """ - for offset, value in enumerate( - record[field['name']] - for record in json_array for field in pdef.array_points): - if value not in ['', None]: - self.send_command(self.send_direct_operate_command, pdef, value, index=pdef.index+offset) - - def send_function_test(self, point_def_path='', func_def_path='', func_test_path='', func_test_json=None): - """ - Send a function test after validating the function test (as JSON). - - :param point_def_path: path to point definition config - :param func_def_path: path to function definition config - :param func_test_path: path to function test json - :param func_test_json: function test json - """ - ftest = FunctionTest(func_test_path, func_test_json, point_def_path=point_def_path, func_def_path=func_def_path) - - ftest.is_valid() - - pdefs = PointDefinitions(point_definitions_path=point_def_path or POINT_DEF_PATH) - - func_def = ftest.get_function_def() - for func_step_def in func_def.steps: - try: - point_value = ftest.points[func_step_def.name] - except KeyError: - continue - - pdef = pdefs.point_named(func_step_def.name) # No need to test for valid point name, as that was done above - if not pdef: - raise MesaMasterException("Point definition not found: {}".format(func_step_def.name)) - - if type(point_value) == list: - self.send_array(point_value, pdef) - else: - try: - send_func = self.SEND_FUNCTIONS[func_step_def.fcodes[0] if func_step_def.fcodes else DIRECT_OPERATE] - except (KeyError, IndexError): - raise MesaMasterException("Unrecognized sent command function") - - self.send_command(send_func, pdef, point_value) - - -def main(): - mesa_master = MesaMaster() - mesa_master.connect() - # Ad-hoc tests can be inserted here if desired. - mesa_master.shutdown() - - -if __name__ == '__main__': - main() diff --git a/services/core/DNP3Agent/mesaagent.config b/services/core/DNP3Agent/mesaagent.config deleted file mode 100644 index ce39874edd..0000000000 --- a/services/core/DNP3Agent/mesaagent.config +++ /dev/null @@ -1,15 +0,0 @@ -{ - "points": "config://mesa_points.config", - "functions": "config://mesa_functions.config", - "point_topic": "mesa/point", - "function_topic": "mesa/function", - "outstation_status_topic": "mesa/outstation_status", - "outstation_config": { - "database_sizes": 10000, - "log_levels": ["NORMAL"] - }, - "local_ip": "0.0.0.0", - "port": 20000, - "all_functions_supported_by_default": true, - "function_validation": false -} diff --git a/services/core/DNP3Agent/requirements.txt b/services/core/DNP3Agent/requirements.txt deleted file mode 100644 index 9647f44d1e..0000000000 --- a/services/core/DNP3Agent/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -pydnp3==0.1.0 diff --git a/services/core/DNP3Agent/setup.py b/services/core/DNP3Agent/setup.py deleted file mode 100644 index f581207dc4..0000000000 --- a/services/core/DNP3Agent/setup.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, SLAC / 8minutenergy / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# This material was prepared in part as an account of work sponsored by an agency of -# the United States Government. Neither the United States Government nor the -# United States Department of Energy, nor SLAC, nor 8minutenergy, nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by the United States Government or any agency thereof, or -# SLAC, 8minutenergy, or Kisensum. The views and opinions of authors expressed -# herein do not necessarily state or reflect those of the -# United States Government or any agency thereof. -# }}} - -from os import path, environ -from setuptools import setup, find_packages - -MAIN_MODULE = 'agent' - -# Find the agent package that contains the main module -packages = find_packages('.') -agent_package = '' -for package in find_packages(): - # Because there could be other packages such as tests - if path.isfile(package + '/' + MAIN_MODULE + '.py') is True: - agent_package = package -if not agent_package: - raise RuntimeError('None of the packages under {dir} contain the file ' - '{main_module}'.format(main_module=MAIN_MODULE + '.py', - dir=path.abspath('.'))) - -# Find the version number from the main module -agent_module = environ.get('AGENT_MODULE', agent_package + '.' + MAIN_MODULE) -_temp = __import__(agent_module, globals(), locals(), ['__version__'], 0) -__version__ = _temp.__version__ - -# Setup -setup( - name=agent_module.replace('.', ''), - version=__version__, - install_requires=['volttron'], - packages=packages, - entry_points={ - 'setuptools.installation': [ - 'eggsecutable = ' + agent_module + ':main', - ] - } -) diff --git a/services/core/DNP3Agent/tests/MesaTestAgent/setup.py b/services/core/DNP3Agent/tests/MesaTestAgent/setup.py deleted file mode 100644 index 9c01575e32..0000000000 --- a/services/core/DNP3Agent/tests/MesaTestAgent/setup.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, 8minutenergy / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Neither 8minutenergy nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by 8minutenergy or Kisensum. -# }}} - -from os import path -from setuptools import setup, find_packages - -MAIN_MODULE = 'agent' - -# Find the agent package that contains the main module -packages = find_packages('.') -agent_package = '' -for package in find_packages(): - # Because there could be other packages such as tests - if path.isfile(package + '/' + MAIN_MODULE + '.py') is True: - agent_package = package -if not agent_package: - raise RuntimeError('None of the packages under {dir} contain the file ' - '{main_module}'.format(main_module=MAIN_MODULE + '.py', - dir=path.abspath('.'))) - -# Find the version number from the main module -agent_module = agent_package + '.' + MAIN_MODULE -_temp = __import__(agent_module, globals(), locals(), ['__version__'], 0) -__version__ = _temp.__version__ - -# Setup -setup( - name=agent_package + 'agent', - version=__version__, - install_requires=['volttron'], - packages=packages, - entry_points={ - 'setuptools.installation': [ - 'eggsecutable = ' + agent_module + ':main', - ] - } -) diff --git a/services/core/DNP3Agent/tests/MesaTestAgent/testagent.config b/services/core/DNP3Agent/tests/MesaTestAgent/testagent.config deleted file mode 100644 index 1f73dc61fa..0000000000 --- a/services/core/DNP3Agent/tests/MesaTestAgent/testagent.config +++ /dev/null @@ -1,23 +0,0 @@ -{ - "mesaagent_id": "mesaagent", - # The point configuration can be defined here by the test agent, or the test agent - # can rely on the DNP3 driver's config (e.g. dnp3.csv). If a point configuration is - # defined here, and the test agent is started after the PlatformDriverAgent, the test - # agent's point config will override the DNP3 driver config. - # "point_config": { - # "DCHD.WTgt": {"group": 41, "index": 65}, - # "DCHD.WinTms": {"group": 41, "index": 66}, - # "DCHD.RmpTms": {"group": 41, "index": 67}, - # "DCHD.RevtTms": {"group": 41, "index": 68}, - # "DCHD.RmpUpRte": {"group": 41, "index": 69}, - # "DCHD.RmpDnRte": {"group": 41, "index": 70}, - # "DCHD.ChaRmpUpRte": {"group": 41, "index": 71}, - # "DCHD.ChaRmpDnRte": {"group": 41, "index": 72}, - # "DCHD.ModPrty": {"group": 41, "index": 9}, - # "DCHD.VArAct": {"group": 41, "index": 10}, - # "DCHD.ModEna": {"group": 12, "index": 5} - # }, - "point_topic": "mesa/point", - "function_topic": "mesa/function", - "outstation_status_topic": "mesa/outstation_status" -} diff --git a/services/core/DNP3Agent/tests/MesaTestAgent/testagent/__init__.py b/services/core/DNP3Agent/tests/MesaTestAgent/testagent/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/services/core/DNP3Agent/tests/MesaTestAgent/testagent/agent.py b/services/core/DNP3Agent/tests/MesaTestAgent/testagent/agent.py deleted file mode 100644 index 80ae3731ec..0000000000 --- a/services/core/DNP3Agent/tests/MesaTestAgent/testagent/agent.py +++ /dev/null @@ -1,217 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, 8minutenergy / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Neither 8minutenergy nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by 8minutenergy or Kisensum. -# }}} - - - -import logging -import sys - -from volttron.platform.agent import utils -from volttron.platform.vip.agent import Agent, Core, RPC -from volttron.platform.scheduling import periodic - -utils.setup_logging() -_log = logging.getLogger(__name__) - -__version__ = '1.0' - -RPC_INTERVAL_SECS = 10 - -TEST_GET_POINT_NAME1 = 'DCHD.ModEna' -TEST_GET_POINT_NAME2 = 'DCHD.VArAct.out' -TEST_GET_SELECTOR_BLOCK_POINT_NAME = 'Curve Edit Selector' -TEST_SET_POINT_NAME = 'DCHD.WTgt.1' -TEST_SET_SUPPORT_POINT_NAME = 'DCHA.Beh' -# Test has been disabled: these points are not part of the current production data definitions. -# TEST_DEVICE_ARRAY = { -# "Inverter power readings": [ -# { -# "Inverter active power output - Present Active Power output level": 302, -# "Inverter reactive output - Present reactive power output level": 100 -# } -# ] -# } -TEST_INPUT_CURVE = { - "FMAR.in.PairArray.CsvPts": [ - {"FMAR.in.PairArray.CsvPts.xVal": 30, "FMAR.in.PairArray.CsvPts.yVal": 130}, - {"FMAR.in.PairArray.CsvPts.xVal": 31, "FMAR.in.PairArray.CsvPts.yVal": 131}, - {"FMAR.in.PairArray.CsvPts.xVal": 32, "FMAR.in.PairArray.CsvPts.yVal": 132} - ] -} - - -def mesa_test_agent(config_path, **kwargs): - """ - Parse the TestAgent configuration file and return an instance of - the agent that has been created using that configuration. - - See initialize_config() method documentation for a description of each configurable parameter. - - This agent can be installed from a command-line shell as follows: - export VOLTTRON_ROOT= - export MESA_TEST=$VOLTTRON_ROOT/services/core/MesaAgent/tests/TestAgent - cd $VOLTTRON_ROOT - python scripts/install-agent.py -s $$MESA_TEST -i mesatest -c $MESA_TEST/testagent.config -t mesatest -f - - :param config_path: (str) Path to a configuration file. - :returns: TestAgent instance - """ - try: - config = utils.load_config(config_path) - except (Exception, err): - _log.error("Error loading MesaTestAgent configuration: {}".format(err)) - config = {} - mesaagent_id = config.get('mesaagent_id', 'mesaagent') - point_topic = config.get('point_topic', 'mesa/point') - function_topic = config.get('function_topic', 'mesa/function') - outstation_status_topic = config.get('outstation_status_topic', 'mesa/outstation_status') - point_config = config.get('point_config', None) - return MesaTestAgent(mesaagent_id, point_topic, function_topic, outstation_status_topic, point_config, **kwargs) - - -class MesaTestAgent(Agent): - """ - This is a sample test agent that demonstrates and tests MesaAgent. - It exercises MesaAgent's exposed RPC calls and consumes VOLTTRON messages published by MesaAgent. - """ - - def __init__(self, mesaagent_id, point_topic, function_topic, outstation_status_topic, point_config, **kwargs): - super(MesaTestAgent, self).__init__(**kwargs) - self.mesaagent_id = None - self.point_topic = None - self.function_topic = None - self.outstation_status_topic = None - self.point_config = None - self.default_config = {'mesaagent_id': mesaagent_id, - 'point_topic': point_topic, - 'function_topic': function_topic, - 'outstation_status_topic': outstation_status_topic, - 'point_config': point_config} - self.vip.config.set_default("config", self.default_config) - self.vip.config.subscribe(self._configure, actions=["NEW", "UPDATE"], pattern="config") - self.initialize_config(self.default_config) - - def _configure(self, config_name, action, contents): - """The agent's config may have changed. Re-initialize it.""" - config = self.default_config.copy() - config.update(contents) - self.initialize_config(config) - - def initialize_config(self, config): - self.mesaagent_id = config['mesaagent_id'] - self.point_topic = config['point_topic'] - self.function_topic = config['function_topic'] - self.outstation_status_topic = config['outstation_status_topic'] - self.point_config = config['point_config'] - _log.debug('MesaTestAgent configuration parameters:') - _log.debug('\tmesaagent_id={}'.format(self.mesaagent_id)) - _log.debug('\tpoint_topic={}'.format(self.point_topic)) - _log.debug('\tfunction_topic={}'.format(self.function_topic)) - _log.debug('\toutstation_status_topic={}'.format(self.outstation_status_topic)) - _log.debug('\tpoint_config={}'.format(self.point_config)) - - @Core.receiver('onstart') - def onstart_method(self, sender): - """The test agent has started. Perform initialization and spawn the main process loop.""" - _log.debug('Starting MesaTestAgent') - if self.point_config: - # This test agent can configure the points itself, or (by default) it can rely on the DNP3 driver to do it. - _log.debug('Sending DNP3 point map: {}'.format(self.point_config)) - self.send_rpc('config_points', self.point_config) - # Subscribe to the MesaAgent's point, function, and outstation_status publications. - self.vip.pubsub.subscribe(peer='pubsub', prefix=self.point_topic, callback=self.receive_point_value) - self.vip.pubsub.subscribe(peer='pubsub', prefix=self.function_topic, callback=self.receive_function) - self.vip.pubsub.subscribe(peer='pubsub', prefix=self.outstation_status_topic, callback=self.receive_status) - self.core.schedule(periodic(RPC_INTERVAL_SECS), self.issue_rpcs) - - @staticmethod - def receive_point_value(peer, sender, bus, topic, headers, point_value): - """(Subscription callback) Receive a point value.""" - _log.debug('MesaTestAgent received point_value={}'.format(point_value)) - - def receive_function(self, peer, sender, bus, topic, headers, point_value): - """(Subscription callback) Receive a function.""" - _log.debug('MesaTestAgent received function={}'.format(point_value)) - if 'expected_response' in point_value: - # The function step expects a response. Send one. - self.set_point(point_value['expected_response'], 1) - - @staticmethod - def receive_status(peer, sender, bus, topic, headers, point_value): - """(Subscription callback) Receive outstation status.""" - _log.debug('MesaTestAgent received outstation status={}'.format(point_value)) - - def issue_rpcs(self): - """Periodically issue RPCs to the DNP3 agent.""" - self.get_point(TEST_GET_POINT_NAME1) - self.get_point(TEST_GET_POINT_NAME2) - self.get_selector_block(TEST_GET_SELECTOR_BLOCK_POINT_NAME, 3) - self.set_point(TEST_SET_POINT_NAME, 10) - self.set_point(TEST_SET_SUPPORT_POINT_NAME, True) - # self.set_points(TEST_DEVICE_ARRAY) - self.set_points(TEST_INPUT_CURVE) - - def get_point(self, point_name): - """Get a single metric from the MesaAgent via an RPC call.""" - point_value = self.send_rpc('get_point', point_name) - _log.debug('MesaTestAgent get_point received {}={}'.format(point_name, point_value)) - - def get_selector_block(self, point_name, edit_selector): - """Get a selector block from the MesaAgent via an RPC call.""" - selector_block = self.send_rpc('get_selector_block', point_name, edit_selector) - _log.debug('MesaTestAgent get_selector_block {} selector {} received {}'.format(point_name, - edit_selector, - selector_block)) - - def set_point(self, point_name, value): - """Send a single point value to the MesaAgent via an RPC call.""" - _log.debug('MesaTestAgent set_point sent {}={}'.format(point_name, value)) - self.send_rpc('set_point', point_name, value) - - def set_points(self, json_payload): - """Send a single point value to the MesaAgent via an RPC call.""" - _log.debug('MesaTestAgent sending points: {}'.format(json_payload)) - self.send_rpc('set_points', json_payload) - - def send_rpc(self, rpc_name, *args, **kwargs): - """Send an RPC request to the MesaAgent, and return its response (if any).""" - response = self.vip.rpc.call(self.mesaagent_id, rpc_name, *args, **kwargs) - return response.get(30) - - -def main(): - """Start the agent.""" - utils.vip_main(mesa_test_agent, identity='mesa_test_agent', version=__version__) - - -if __name__ == '__main__': - try: - sys.exit(main()) - except KeyboardInterrupt: - pass diff --git a/services/core/DNP3Agent/tests/README.md b/services/core/DNP3Agent/tests/README.md deleted file mode 100644 index 8238796c8f..0000000000 --- a/services/core/DNP3Agent/tests/README.md +++ /dev/null @@ -1,43 +0,0 @@ -The tests/ subdirectory of DNP3Agent contains several types of tests: - -# MesaAgent Regression Tests - -**test_mesa_agent.py** contains MesaAgent pytest regression tests as follows: - -1. Load point and function definitions. -2. Start a MesaAgent process. -3. Test routing of DNP3 output: - - Send point values from a simulated Master. - - Verify that MesaAgent has published them correctly on the VOLTTRON message bus. -4. Test routing of DNP3 input: - - Use RPC calls to set point values. - - Verify that the simulated Master has received them correctly. - -# DNP3Agent Regression Tests - -**test_dnp3_agent.py** contains DNP3Agent pytest regression tests as follows: - -1. Load point definitions. -2. Start a DNP3Agent process. -3. Test routing of DNP3 output, similar to the MesaAgent tests above. -4. Test routing of DNP3 input, similar to the MesaAgent tests above. - -# Data Regression Tests - -**test_mesa_data.py** contains pytest regression tests of MesaAgent data. - -The test strategy in test_mesa_data.py is similar to the other pytest strategies, -but rather than working with a small, controlled set of data, this module's tests -use "production" point and function definitions. - -# Ad-Hoc Unit Test Support - -**unit_test_point_definitions.py** contains a mix of standalone non-pytest -methods that test and validate various types of data and behavior. - -**MesaTestAgent** is a VOLTTRON agent that interacts with MesaAgent, sending -RPC calls and subscribing to MesaAgent publication of data. - -**mesa_platform_cmd.py** is a standalone command-line utility (built on the Python -Cmd library) that sends point and function values from the master to -the (MesaAgent) DNP3 outstation. diff --git a/services/core/DNP3Agent/tests/data/connect_and_disconnect.json b/services/core/DNP3Agent/tests/data/connect_and_disconnect.json deleted file mode 100644 index 26ba349c17..0000000000 --- a/services/core/DNP3Agent/tests/data/connect_and_disconnect.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "function_id": "connect_and_disconnect", - "function_name": "Connect and Disconnect", - "DCTE.WinTms.AO16": 10, - "DCTE.RvrtTms.AO17": 12, - "CSWI.Pos.BO5": true -} \ No newline at end of file diff --git a/services/core/DNP3Agent/tests/data/enable_watt_var_power_mode.json b/services/core/DNP3Agent/tests/data/enable_watt_var_power_mode.json deleted file mode 100644 index 756b598ebe..0000000000 --- a/services/core/DNP3Agent/tests/data/enable_watt_var_power_mode.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "function_id": "enable_watt-var_power_mode", - "function_name": "Enable Watt-Var Power Mode", - "DWVR.ModPrio.AO221": 1, - "DWVR.WinTms.AO222": 2, - "DWVR.RmpTms.AO223": 3, - "DWVR.RvrtTms.AO224": 4, - "DWVR.EcpRef.AO225": 5, - "DWVR.OpnLoopMax.AO227": 10, - "DWVR.OpnLoopMax.AO228": 1, - "DWVR.WVArCrv.AO226": 2, - "DWVR.ModEna.BO30": true -} \ No newline at end of file diff --git a/services/core/DNP3Agent/tests/data/enable_watt_var_schedule.json b/services/core/DNP3Agent/tests/data/enable_watt_var_schedule.json deleted file mode 100644 index a30f249fa4..0000000000 --- a/services/core/DNP3Agent/tests/data/enable_watt_var_schedule.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "function_id": "enable_schedules", - "function_name": "Enable Watt-Var Schedule", - "FSCHxx.Mod.BO42": true, - "FSCC.Schd.AO461": 2, - "FSCHxx.SchdReuse.BO43": true, - "FSCHxx.SchdReuse.BO44": false, - "FSCHxx.SchdReuse.BO45": false, - "FSCHxx.SchdReuse.BO46": false, - "FSCHxx.SchdReuse.BO47": false, - "FSCHxx.SchdReuse.BO48": false, - "FSCHxx.SchdReuse.BO49": false -} \ No newline at end of file diff --git a/services/core/DNP3Agent/tests/data/mesa_functions.yaml b/services/core/DNP3Agent/tests/data/mesa_functions.yaml deleted file mode 100644 index fdad5de95b..0000000000 --- a/services/core/DNP3Agent/tests/data/mesa_functions.yaml +++ /dev/null @@ -1,2581 +0,0 @@ -functions: -- id: connect_and_disconnect - name: Connect and Disconnect - ref: AN2018 Spec section 2.4.4 Table 29 - steps: - - description: Set time window - fcodes: - - direct_operate - optional: I - point_name: DCTE.WinTms.AO16 - response: DCTE.WinTms.AI60 - step_number: 1 - - description: Set reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DCTE.RvrtTms.AO17 - response: DCTE.RvrtTms.AI61 - step_number: 2 - - description: Retrieve status of switch - fcodes: - - read - - response - optional: O - point_name: n/a - response: DSTO.DEROpSt.off.BI23 - step_number: 3 - - action: publish - description: Issue switch control command and receive response - fcodes: - - select - - operate - optional: M - point_name: CSWI.Pos.BO5 - response: DSTO.DEROpSt.off.BI23 - step_number: 4 - - description: Detect if switch is moving - fcodes: - - read - - response - optional: O - point_name: n/a - response: CSWI.Pos.BI24 - step_number: 5 -- id: cease_to_energize_and_return_to_service - name: Cease to Energize and Return to Service - ref: AN2018 Spec section 2.4.5 Table 30 - steps: - - description: Set Cease to Energize Time Window - fcodes: - - direct_operate - optional: I - point_name: DCTE.WinTms.AO13 - response: DCTE.WinTms.AI57 - step_number: 1 - - description: Set Cease to Energize Ramp DownTime - fcodes: - - direct_operate - optional: I - point_name: DCTE.RmpTms.AO14 - response: DCTE.RmpTms.AI58 - step_number: 2 - - description: Set Cease to Energize Timeout Period - fcodes: - - direct_operate - optional: I - point_name: DCTE.RvrtTms.AO15 - response: DCTE.RvrtTms.AI59 - step_number: 3 - - description: Cause DER to Cease to Energize - fcodes: - - select - - operate - optional: M - point_name: DCTE.CeaEngzReq.BO2 - response: DSTO.DEROpSt.connectedandidle.BI14 - step_number: 4 - - description: Give DER Permission to Stop - fcodes: - - select - - operate - optional: M - point_name: DSTO.PrmDscon.BO4 - response: DSTO.PrmDscon.BI17 - step_number: 5 - - description: Confirm DER is Stopping - fcodes: - - read - - response - optional: O - point_name: n/a - response: DSTO.DEROpSt.stopping.BI13 - step_number: 6 - - description: Confirm DER has Ceased to Energize - fcodes: - - read - - response - optional: O - point_name: n/a - response: DSTO.DEROpSt.ceasedtoenergize.BI15 - step_number: 7 - - description: Set High Voltage Limit - fcodes: - - direct_operate - optional: I - point_name: DCTE.VHiLim.AO6 - response: DCTE.VHiLim.AI50 - step_number: 8 - - description: Set Low Voltage Limit - fcodes: - - direct_operate - optional: I - point_name: DCTE.VLoLim.AO7 - response: DCTE.VLoLim.AI51 - step_number: 9 - - description: Set High Frequency Limit - fcodes: - - direct_operate - optional: I - point_name: DCTE.HzHiLim.AO8 - response: DCTE.HzHiLim.AI52 - step_number: 10 - - description: Set Low Frequency Limit - fcodes: - - direct_operate - optional: I - point_name: DCTE.HzLoLim.AO9 - response: DCTE.HzLoLim.AI53 - step_number: 11 - - description: Set Delay Time - fcodes: - - direct_operate - optional: I - point_name: DCTE.RtnDlyTmms.AO10 - response: DCTE.RtnDlTmms.AI54 - step_number: 12 - - description: Set Return to Service Time Window - fcodes: - - direct_operate - optional: I - point_name: DCTE.WinTms.AO11 - response: DCTE.WinTms.AI55 - step_number: 13 - - description: Set Return to Service Ramp Up Time - fcodes: - - direct_operate - optional: I - point_name: DCTE.RtnRmpTmms.AO12 - response: DCTE.RtnRmpTmms.AI56 - step_number: 14 - - description: Cause DER to Return to Service - fcodes: - - select - - operate - optional: M - point_name: DCTE.RtnSrvReq.BO1 - response: DSTO.DEROpSt.startingandsynchronizing.BI12 - step_number: 15 - - action: publish - description: Give DER Permission to Start - fcodes: - - select - - operate - optional: M - point_name: DSTO.PrmConn.BO3 - response: DSTO.PrmConn.BI16 - step_number: 16 - - description: Confirm DER is Started - fcodes: - - read - - response - optional: O - point_name: n/a - response: DSTO.DEROpSt.connectedandidle.BI14 - step_number: 17 -- id: enable_low_high_voltage_ride-through_mode - mode_types: - curve: - - 9 - - 10 - - 11 - - 12 - schedule: - - 1 - - 2 - - 3 - - 4 - name: Enable Low/High Voltage Ride-Through Mode - ref: AN2018 Spec section 2.5.1 Table 33 - steps: - - description: Set the Reference Voltage if it is not already set - fcodes: - - direct_operate - optional: I - point_name: DECP.VRef.AO0 - response: DECP.VRef.AI29 - step_number: 1 - - description: Set the Reference Voltage Offset if it is not already set - fcodes: - - direct_operate - optional: I - point_name: DECP.VRefOfs.AO1 - response: DECP.VRefOfs.AI30 - step_number: 2 - - description: Identify the meter used to measure the voltage. By default this - is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DHVT.EcpRef.AO22 - response: DHVT.EcpRef.AI71 - step_number: 3 - - description: 'DGSMn.ModTyp.AO245 = <9> HVRT Must Trip: If the curve is a must - trip curve, identify the index of the curve which specifies trip points when - the voltage is high' - fcodes: - - direct_operate - func_ref: curve - optional: O - point_name: PTOV.BlkRef.AO23 - response: PTOV.BlkRef.AI73 - step_number: 4 - - description: 'DGSMn.ModTyp.AO245 = <11> LVRT Must Trip: If the curve is a must - trip curve, identify the index of the curve which specifies trip points when - the voltage is low' - fcodes: - - direct_operate - func_ref: curve - optional: O - point_name: PTUV.BlkRef.AO24 - response: PTUV.BlkRef.AI74 - step_number: 5 - - description: 'DGSMn.ModTyp.AO245 = <10> HVRT Momentary Cessation: If the curve - is a must trip curve, identify the index of the curve which specifies where - generation/discharging must stop when the voltage is high' - fcodes: - - direct_operate - func_ref: curve - optional: O - point_name: PTOV.BlkRef.AO25 - response: PTOV.BlkRef.AI75 - step_number: 6 - - description: 'DGSMn.ModTyp.AO245 = <12> LVRT Momentary Cessation: If the curve - is a must trip curve, identify the index of the curve which specifies where - generation/discharging must stop when the voltage is low' - fcodes: - - direct_operate - func_ref: curve - optional: O - point_name: PTUV.BlkRef.AO26 - response: PTUV.BlkRef.AI76 - step_number: 7 - - action: publish - description: Enable the Low/High Voltage Ride-Through Mode - fcodes: - - select - - operate - optional: M - point_name: DHVT.ModEna.BO12 - response: DHVT.ModEna.BI64 - step_number: 8 -- id: enable_low_high_frequency_ride-through_mode - mode_types: - curve: - - 13 - - 14 - - 15 - - 16 - schedule: - - 5 - - 6 - - 7 - - 8 - name: Enable Low/High Frequency Ride-Through Mode - ref: AN2018 Spec section 2.5.2 Table 35 - steps: - - description: Set the Nominal Grid Frequency if it is not already set - fcodes: - - direct_operate - optional: I - point_name: DECP.EcpNomHz.AO2 - response: DECP.EcpNomHz.AI31 - step_number: 1 - - description: Identify the meter used to measure the frequency. By default this - is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DHFT.EcpRef.AO27 - response: DHFT.EcpRef.AI77 - step_number: 2 - - description: 'DGSMn.ModTyp.AO245 = <13> HFRT Must Trip: Identify the index of - the frequency ride through curve which specifies trip ponts when the frequency - is high' - fcodes: - - direct_operate - func_ref: curve - optional: O - point_name: PTOF.BlkRef.AO28 - response: PTOF.BlkRef.AI79 - step_number: 3 - - description: 'DGSMn.ModTyp.AO245 = <15> LFRT Must Trip: Identify the index of - the frequency ride through curve which specifies trip ponts when the voltage - is low' - fcodes: - - direct_operate - func_ref: curve - optional: O - point_name: PTUF.BlkRef.AO29 - response: PTUF.BlkRef.AI80 - step_number: 4 - - description: 'DGSMn.ModTyp.AO245 = <14> HFRT Mandatory Operation: Identify the - index of the frequency ride through curve which specifies where generation/discharging - must stop when the frequency is high' - fcodes: - - direct_operate - func_ref: curve - optional: O - point_name: PTOF.BlkRef.AO30 - response: PTOF.BlkRef.AI81 - step_number: 5 - - description: 'DGSMn.ModTyp.AO245 = <16> LFRT Mandatory Operation: Identify the - index of the frequency ride through curve which specifies where generation/discharging - must stop when the frequency is high' - fcodes: - - direct_operate - func_ref: curve - optional: O - point_name: PTUF.BlkRef.AO31 - response: PTUF.BlkRef.AI82 - step_number: 6 - - action: publish - description: Enable the Low/High Frequency Ride-Through Mode - fcodes: - - select - - operate - optional: M - point_name: DHFT.ModEna.BO13 - response: DHFT.ModEna.BI65 - step_number: 7 -- id: enable_frequency-watt_mode - name: Enable Frequency-Watt Mode - ref: AN2018 Spec section 2.5.3 Table 36 - mode_types: - schedule: - - 11 - steps: - - description: If not already established, set the Nominal Grid Frequency - fcodes: - - direct_operate - optional: I - point_name: DECP.EcpNomHz.AO2 - response: DECP.EcpNomHz.AI31 - step_number: 1 - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DHFW2.ModPrio.AO57 - response: DHFW2.ModPrio.AI115 - step_number: 2 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DHFW.WinTms.AO58 - response: DHFW.WinTms.AI116 - step_number: 3 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DHFW.RmpTms.AO59 - response: DHFW.RmpTms.AI117 - step_number: 4 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DHFW.RvrtTms.AO60 - response: DHFW.RvrtTms.AI118 - step_number: 5 - - description: Identify the meter used to measure the frequency. By default this - is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DHFW.EcpRef.AO61 - response: DHFW2.EcpRef.AI119 - step_number: 6 - - description: Set the High Starting Frequency - fcodes: - - direct_operate - optional: I - point_name: DHFW.HzStr.AO62 - response: DHFW2.HzStr.AI121 - step_number: 7 - - description: Set the High Stopping Frequency - fcodes: - - direct_operate - optional: I - point_name: DHFW.HzStop.AO63 - response: DHFW2.HzStop.AI122 - step_number: 8 - - description: Set the High Discharging Gradient - fcodes: - - direct_operate - optional: I - point_name: DHFW.WGra.AO64 - response: DHFW.WGra.AI123 - step_number: 9 - - description: Set the High Charging Gradient - fcodes: - - direct_operate - optional: I - point_name: DHFW.WChaGra.AO65 - response: DHFW.WChaGra.AI124 - step_number: 10 - - description: Set the Low Starting Frequency - fcodes: - - direct_operate - optional: I - point_name: DLFW.HzStr.AO66 - response: DLFW2.HzStr.AI125 - step_number: 11 - - description: Set the Low Stopping Frequency - fcodes: - - direct_operate - optional: I - point_name: DLFW.HzStop.AO67 - response: DLFW2.HzStop.AI126 - step_number: 12 - - description: Set the Low Discharging Gradient - fcodes: - - direct_operate - optional: I - point_name: DLFW.WGra.AO68 - response: DLFW.WGra.AI127 - step_number: 13 - - description: Set the Low Charging Gradient - fcodes: - - direct_operate - optional: I - point_name: DLFW.WChaGra.AO69 - response: DLFW.WChaGra.AI128 - step_number: 14 - - description: Set the Start Delay - fcodes: - - direct_operate - optional: I - point_name: DHFW2.ActStrDlTmms.AO70 - response: DHFW2.ActStrDlTmms.AI129 - step_number: 15 - - description: Set the Stop Delay - fcodes: - - direct_operate - optional: I - point_name: DHFW2.ActStopDlTmms.AO71 - response: DHFW2.ActStopDlTmms.AI130 - step_number: 16 - - description: Set the Ramp Up Time Constant - fcodes: - - direct_operate - optional: I - point_name: DHFW.OpnLoop.AO72 - response: DLFW.OpnLoopMax.AI131 - step_number: 17 - - description: Set the Ramp Down Time Constant - fcodes: - - direct_operate - optional: I - point_name: DHFW.OpnLoop.AO73 - response: DHFW.OpnLoopMax.AI132 - step_number: 18 - - description: Set the Discharging Up Ramp Rate - fcodes: - - direct_operate - optional: I - point_name: DHFW.DschRpuRte.AO74 - response: DHFW.RpuRte.AI133 - step_number: 19 - - description: Set the Discharging Down Ramp Rate - fcodes: - - direct_operate - optional: I - point_name: DHFW.DschRpdRte.AO75 - response: DHFW.RpdRteMax.AI134 - step_number: 20 - - description: Set the Charging Up Ramp Rate - fcodes: - - direct_operate - optional: I - point_name: DHFW.ChaRpuRte.AO76 - response: DHFW.RpuChaRte.AI135 - step_number: 21 - - description: Set the Charging Down Ramp Rate - fcodes: - - direct_operate - optional: I - point_name: DHFW.ChaRpdRte.AO77 - response: DHFW.RpdChaRteMax.AI136 - step_number: 22 - - description: Set the Hight Return Gradient - fcodes: - - direct_operate - optional: I - point_name: DHFW.RtnRmpRte.AO78 - response: DHFW2.RtnRmpRte.AI137 - step_number: 23 - - description: Set the Low Return Gradient - fcodes: - - direct_operate - optional: I - point_name: DLFW.RtnRmpRte.AO79 - response: DLFW2.RtnRmpRte.AI138 - step_number: 24 - - description: Set the minium State of Charge to be used by this mode - fcodes: - - direct_operate - optional: I - point_name: DHFW.SocUseMin.AO80 - response: DAGC.SocUseMinPct.AI140 - step_number: 25 - - description: Set the maximum State of Charge to be used by this mode - fcodes: - - direct_operate - optional: I - point_name: DHFW.SocUseMax.AO81 - response: DAGC.SocUseMaxPct.AI141 - step_number: 26 - - description: Enable or Disable Hysteresis - fcodes: - - direct_operate - optional: I - point_name: DHFW.HysEna.BO34 - response: DHFW.HysEna.BI86 - step_number: 27 - - description: Enable or Disable Snapshot of Power - fcodes: - - direct_operate - optional: I - point_name: DHFW.SnptEna.BO35 - response: DHFW.SnptEna.BI87 - step_number: 28 - - action: publish - description: Enable Frequency-Watt Mode - fcodes: - - select - - operate - optional: M - point_name: DHFW.ModEna.BO16 - response: DHFW.ModEna.BI68 - step_number: 29 -- id: enable_dynamic_reactive_current_support_mode - name: Enable Dynamic Reactive Current Support Mode - ref: AN2018 Spec section 2.5.4 Table 37 - mode_types: - schedule: - - 9 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DRGS.ModPrio.AO32 - response: DRGS.ModPrio.AI83 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DRGS.WinTms.AO33 - response: DRGS.WinTms.AI84 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DRGS.RmpTms.AO34 - response: DRGS.RmpTms.AI85 - step_number: 3 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DRGS.RvrtTms.AO35 - response: DRGS.RvrtTms.AI86 - step_number: 4 - - description: Identify the meter used to measure the voltage. By default this - is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DRGS.EcpRef.AO36 - response: DRGS.EcpRef.AI87 - step_number: 5 - - description: Set the Gradient Mode to select the curve shape - fcodes: - - direct_operate - optional: M - point_name: DRGS.ArGraMod.AO37 - response: DRGS.ArGraMod.AI91 - step_number: 6 - - description: Set the Deadband Minimum Voltage - fcodes: - - direct_operate - optional: M - point_name: DRGS.DbVMin.AO38 - response: DRGS.DbVMin.AI92 - step_number: 7 - - description: Set the Deadband Maximum Voltage - fcodes: - - direct_operate - optional: M - point_name: DRGS.DbVMax.AO39 - response: DRGS.DbVMax.AI93 - step_number: 8 - - description: Set the Reactive Current Support Gradient for Sags - fcodes: - - direct_operate - optional: M - point_name: DRGS.ArGraSag.AO40 - response: DRGS.ArGraSag.AI94 - step_number: 9 - - description: Set the Reactive Current Support Gradient for Swells - fcodes: - - direct_operate - optional: M - point_name: DRGS.ArGraSwl.AO41 - response: DRGS.ArGraSwl.AI95 - step_number: 10 - - description: Set the Filter Time for the Moving Average Voltage in seconds - fcodes: - - direct_operate - optional: M - point_name: DRGS.FilTms.AO42 - response: DRGS.FilTms.AI96 - step_number: 11 - - description: Enable Event-Based Reactive Current Support if required. It shall - default to Disabled. - fcodes: - - direct_operate - optional: I - point_name: DRGS.ArGraMod.BO33 - response: DRGS.ModEna.BI85 - step_number: 12 - - description: Set the Hold Time in milliseconds if Event-Based Reactive Current - Support is required. - fcodes: - - direct_operate - optional: I - point_name: DRGS.HoldTmms.AO46 - response: DRGS.HoldTmms.AI100 - step_number: 13 - - description: Set the Block Zone Voltage if required. Otherwise it shall default - to zero. - fcodes: - - direct_operate - optional: I - point_name: DRGS.BlkZnV.AO43 - response: DRGS.BlkZnV.AI97 - step_number: 14 - - description: Set the Hysteresis Block Zone Voltage if required. Otherwise it - shall default to zero. - fcodes: - - direct_operate - optional: I - point_name: DRGS.HysBlkZnV.AO44 - response: DRGS.HysBlkZnV.AI98 - step_number: 15 - - description: Set the Block Zone Time in milliseconds if required. Otherwise it - shall default to zero. - fcodes: - - direct_operate - optional: I - point_name: DRGS.BlkZnTmms.AO45 - response: DRGS.BlkZnTmms.AI99 - step_number: 16 - - action: publish - description: Enable Dynamic Reactive Current Mode - fcodes: - - select - - operate - optional: M - point_name: DRGS.ModEna.BO14 - response: DRGS.ModEna.BI66 - step_number: 17 -- id: enable_volt-watt_mode - name: Enable Volt-Watt Mode - ref: AN2018 Spec section 2.5.5 Table 38 - steps: - - description: If not already established, set the Reference Voltage - fcodes: - - direct_operate - optional: I - point_name: DECP.VRef.AO0 - response: DECP.VRef.AI29 - step_number: 1 - - description: If not already established, set the Reference Voltage Offset - fcodes: - - direct_operate - optional: I - point_name: DECP.VRefOfs.AO1 - response: DECP.VRefOfs.AI30 - step_number: 2 - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DVWD.ModPrio.AO48 - response: DVWD.ModPrio.AI102 - step_number: 3 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DVWD.WinTms.AO49 - response: DVWD.WinTms.AI103 - step_number: 4 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DVWD.RmpTms.AO50 - response: DVWD.RmpTms.AI104 - step_number: 5 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DVWD.RvrtTms.AO51 - response: DVWD.RvrtTms.AI105 - step_number: 6 - - description: Identify the meter used to measure the voltage. By default this - is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DVWD2.EcpRef.AO52 - response: DVWD2.EcpRef.AI106 - step_number: 7 - - description: Set the Dynamic Volt-Watt Gradient - fcodes: - - direct_operate - optional: I - point_name: DVWD.DynVWGra.AO53 - response: DVWD.DynVWGra.AI110 - step_number: 8 - - description: Set the Dynamic Volt-Watt Filter Time - fcodes: - - direct_operate - optional: I - point_name: DVWD.VWFilTms.AO54 - response: DVWD.VWFilTms.AI111 - step_number: 9 - - description: Set the Dynamic Volt-Watt Lower Deadband - fcodes: - - direct_operate - optional: I - point_name: DVWD.DbVWLo.AO55 - response: DVWD.DbVWLo.AI112 - step_number: 10 - - description: Set the Dynamic Volt-Watt Upper Deadband - fcodes: - - direct_operate - optional: I - point_name: DVWD.DbVWHi.AO56 - response: DVWD.DbVWHi.AI113 - step_number: 11 - - action: publish - description: Enable Dynamic Volt-Watt mode - fcodes: - - select - - operate - optional: M - point_name: DVWD.ModEna.BO15 - response: DVWD.ModEna.BI67 - step_number: 12 -- id: enable_active_power_limit_mode - name: Enable Active Power Limit Mode - ref: AN2018 Spec section 2.6.1 Table 39 - mode_types: - schedule: - - 12 - - 13 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DWMX.ModPrio.AO82 - response: DWMX.ModPrio.AI142 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DWMX.WinTms.AO83 - response: DWMX.WinTms.AI143 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DWMX.RmpTms.AO84 - response: DWMX.RmpTms.AI144 - step_number: 3 - - description: Set reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DWMX.RvrtTms.AO85 - response: DWMX.RvrtTms.AI145 - step_number: 4 - - description: Identify the meter used to measure the active power. By default - this is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DWMX.EcpRef.AO86 - response: DWMX.EcpRef.AI146 - step_number: 5 - - description: Retrieve Maximum Active Generation Power Capability - fcodes: - - read - - response - optional: O - point_name: n/a - response: DGEN.WMax.AI32 - step_number: 6 - - description: Retrieve Maximum Active Charging Power Capability - fcodes: - - read - - response - optional: O - point_name: n/a - response: DSTO.ChaWMax.AI33 - step_number: 7 - - description: Set maximum output in percent of nominal Watts (Charging) - fcodes: - - direct_operate - optional: M - point_name: DWMX.WLimPct.AO87 - response: DWMX.WLimPct.AI148 - step_number: 8 - - description: Set maximum output in percent of nominal Watts (Generating) - fcodes: - - direct_operate - optional: M - point_name: DWMN.WLimPct.AO88 - response: DWMN.WLimPct.AI149 - step_number: 9 - - action: publish - description: Enable Active Power Limit mode and receive response - fcodes: - - select - - operate - optional: M - point_name: DWLM.ModEna.BO17 - response: DWLM.ModEna.BI69 - step_number: 10 -- id: enable_charge_discharge_storage_mode - name: Enable Charge/Discharge Storage Mode - ref: AN2018 Spec section 2.6.2 Table 41 - mode_types: - schedule: - - 14 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DWGC.ModPrio.AO89 - response: DWGC.ModPrio.AI150 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DWGC.WinTms.AO90 - response: DWGC.WinTms.AI151 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DWGC.RmpTms.AO91 - response: DWGC.RmpTms.AI152 - step_number: 3 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DWGC.RvrtTms.AO92 - response: DWGC.RvrtTms.AI153 - step_number: 4 - - description: Select whether to use Ramp Rates or Time Constants - fcodes: - - direct_operate - optional: I - point_name: DWGC.UseRmpRte.BO38 - response: DWGC.UseRmpRte.BI90 - step_number: 5 - - description: 'If DWGC.UseRmpRte = 0: Set Charge/Discharge Time Constant Ramp Up - Time' - fcodes: - - direct_operate - optional: O - point_name: DWGC.OpnLoop.AO94 - response: DWGC.OpnLoopMax.AI155 - step_number: 6 - - description: 'If DWGC.UseRmpRte = 0: Set Charge/Discharge Time Constant Ramp Down - Time' - fcodes: - - direct_operate - optional: O - point_name: DWGC.OpnLoop.AO95 - response: DWGC.OpnLoopMax.AI156 - step_number: 7 - - description: 'If DWGC.UseRmpRte = 1: Set Discharge Ramp Up Rate' - fcodes: - - direct_operate - optional: O - point_name: DWGC.DschRpuRte.AO96 - response: DWGC.RpuRte.AI157 - step_number: 8 - - description: 'If DWGC.UseRmpRte = 1: Set Discharge Ramp Down Rate' - fcodes: - - direct_operate - optional: O - point_name: DWGC.DschRpdRte.AO97 - response: DWGC.RpdRteMax.AI158 - step_number: 9 - - description: 'If DWGC.UseRmpRte = 1: Set Charge Ramp Up Rate' - fcodes: - - direct_operate - optional: O - point_name: DWGC.ChaRpuRte.AO98 - response: DWGC.RpuChaRte.AI159 - step_number: 10 - - description: 'If DWGC.UseRmpRte = 1: Set Charge Ramp Down Rate' - fcodes: - - direct_operate - optional: O - point_name: DWGC.ChaRpdRte.AO99 - response: DWGC.RpdChaRteMax.AI160 - step_number: 11 - - description: Set Minimum Reserve for Storage (percent of Battery Capacity Rating) - fcodes: - - direct_operate - optional: I - point_name: DWGC.SocUseMinPct.AO100 - response: DWGC.SocUseMinPct.AI161 - step_number: 12 - - description: Set Maximum Reserve for Storage (percent of Battery Capacity Rating) - fcodes: - - direct_operate - optional: I - point_name: DWGC.SocUseMaxPct.AO101 - response: DWGC.SocUseMaxPct.AI162 - step_number: 13 - - description: Set discharge/charge Active Power Target. Positive is discharging, - negative is charging. - fcodes: - - direct_operate - optional: M - point_name: DWGC.GnWPctSpt.AO93 - response: DWGC.GnWPctSpt.AI154 - step_number: 14 - - action: publish - description: Enable charge/discharge mode and receive response - fcodes: - - select - - operate - optional: M - point_name: DWGC.ModEna.BO18 - response: DWGC.ModEna.BI70 - step_number: 15 -- id: enable_coordinated_charge_discharge_management_mode - name: Enable Coordinated Charge/Discharge Management Mode - ref: AN2018 Spec section 2.6.3 Table 42 - mode_types: - schedule: - - 15 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DTCD.ModPrio.AO102 - response: DTCD.ModPrio.AI163 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DTCD.WinTms.AO103 - response: DTCD.WinTms.AI164 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DTCD.RmpTms.AO104 - response: DTCD.RmpTms.AI165 - step_number: 3 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DTCD.RvrtTms.AO105 - response: DTCD.RvrtTms.AI166 - step_number: 4 - - description: Set the Target State of Charge, as a percentage of Usable Capacity - fcodes: - - direct_operate - optional: M - point_name: DTCD.SocUseTgtPct.AO106 - response: DTCD.SocUseTgtPct.AI167 - step_number: 5 - - description: Set the Target Date Charge Needed - fcodes: - - direct_operate - optional: M - point_name: DTCD.DateTgt.AO107 - response: DTCD.DateTgt.AI168 - step_number: 6 - - description: Set the Target Time Charge Needed (milliseconds since midnight) - fcodes: - - direct_operate - optional: M - point_name: DTCD.DateTgtTms.AO108 - response: DTCD.DateTgtTms.AI169 - step_number: 7 - - action: publish - description: Enable coordinated charge/discharge mode and receive response - fcodes: - - select - - operate - optional: M - point_name: DWGC.ModEna.BO18 - response: DTCD.ModEna.BI71 - step_number: 8 -- id: enable_active_power_response_modes - name: Enable Active Power Response Modes - ref: AN2018 Spec section 2.6.4 Table 43 - mode_types: - schedule: - - 16 - - 17 - - 18 - steps: - - description: 'Set the priority of mode #1' - fcodes: - - direct_operate - optional: I - point_name: DPKP.ModPrio.AO115 - response: DPKP.ModPrio.AI176 - step_number: 1 - - description: 'Set the priority of mode #2' - fcodes: - - direct_operate - optional: I - point_name: DGFL.ModPrio.AO124 - response: DGFL.ModPrio.AI187 - step_number: 2 - - description: 'Set the priority of mode #3' - fcodes: - - direct_operate - optional: I - point_name: DLFL.ModPrio.AO133 - response: DLFL.ModPrio.AI198 - step_number: 3 - - description: 'Set enabling time window of mode #1' - fcodes: - - direct_operate - optional: I - point_name: DPKP.WinTms.AO116 - response: DPKP.WinTms.AI177 - step_number: 4 - - description: 'Set enabling time window of mode #2' - fcodes: - - direct_operate - optional: I - point_name: DGFL.WinTms.AO125 - response: DGFL.WinTms.AI188 - step_number: 5 - - description: 'Set enabling time window of mode #3' - fcodes: - - direct_operate - optional: I - point_name: DLFL.WinTms.AO134 - response: DLFL.WinTms.AI199 - step_number: 6 - - description: 'Set enabling ramp time of mode #1' - fcodes: - - direct_operate - optional: I - point_name: DPKP.RmpTms.AO117 - response: DPKP.RmpTms.AI178 - step_number: 7 - - description: 'Set enabling ramp time of mode #2' - fcodes: - - direct_operate - optional: I - point_name: DGFL.RmpTms.AO126 - response: DGFL.RmpTms.AI189 - step_number: 8 - - description: 'Set enabling ramp time of mode #3' - fcodes: - - direct_operate - optional: I - point_name: DLFL.RmpTms.AO135 - response: DLFL.RmpTms.AI200 - step_number: 9 - - description: 'Set reversion timeout period of mode #1' - fcodes: - - direct_operate - optional: I - point_name: DPKP.RvrtTms.AO118 - response: DPKP.RvrtTms.AI179 - step_number: 10 - - description: 'Set reversion timeout period of mode #2' - fcodes: - - direct_operate - optional: I - point_name: DGFL.RvrtTms.AO127 - response: DGFL.RvrtTms.AI190 - step_number: 11 - - description: 'Set reversion timeout period of mode #3' - fcodes: - - direct_operate - optional: I - point_name: DLFL.RvrtTms.AO136 - response: DLFL.RvrtTms.AI201 - step_number: 12 - - description: 'Identify the meter used to measure the Reference Power Input of - mode #1. By default this is the System Meter (ID = 0)' - fcodes: - - direct_operate - optional: I - point_name: DPKP.EcpRef.AO119 - response: DPKP.EcpRef.AI180 - step_number: 13 - - description: 'Identify the meter used to measure the Reference Power Input of - mode #2. By default this is the System Meter (ID = 0)' - fcodes: - - direct_operate - optional: I - point_name: DGFL.EcpRef.AO128 - response: DGFL.EcpRef.AI191 - step_number: 14 - - description: 'Identify the meter used to measure the Reference Power Input of - mode #3. By default this is the System Meter (ID = 0)' - fcodes: - - direct_operate - optional: I - point_name: DLFL.EcpRef.AO137 - response: DLFL.EcpRef.AI202 - step_number: 15 - - description: 'Set the power threshold for activating mode #1' - fcodes: - - direct_operate - optional: M - point_name: DPKP.PkPwrWLim.AO120 - response: DPKP.PkPwrWLim.AI182 - step_number: 16 - - description: 'Set the power threshold for activating mode #2' - fcodes: - - direct_operate - optional: M - point_name: DGFL.PkPwrWLim.AO129 - response: DGFL.PkPwrWLim.AI193 - step_number: 17 - - description: 'Set the power threshold for activating mode #3' - fcodes: - - direct_operate - optional: M - point_name: DLFL.PkPwrWLim.AO138 - response: DLFL.PkPwrWLim.AI204 - step_number: 18 - - description: 'Set the ratio used to calculate the output power from the measured - power of mode #1' - fcodes: - - direct_operate - optional: M - point_name: DPKP.PkPwrFolPct.AO121 - response: DPKP.PkPwrFolPct.AI183 - step_number: 19 - - description: 'Set the ratio used to calculate the output power from the measured - power of mode #2' - fcodes: - - direct_operate - optional: M - point_name: DGFL.PkPwrFolPct.AO130 - response: DGFL.PkPwrFolPct.AI194 - step_number: 20 - - description: 'Set the ratio used to calculate the output power from the measured - power of mode #3' - fcodes: - - direct_operate - optional: M - point_name: DLFL.PkPwrFolPct.AO139 - response: DLFL.PkPwrFolPct.AI205 - step_number: 21 - - description: 'Set the maximum ramp up rate of mode #1' - fcodes: - - direct_operate - optional: I - point_name: DPKP.RpuRte.AO122 - response: DPKP.RpuRte.AI184 - step_number: 22 - - description: 'Set the maximum ramp up rate of mode #2' - fcodes: - - direct_operate - optional: I - point_name: DGFL.RpuRte.AO131 - response: DGFL.RpuRte.AI195 - step_number: 23 - - description: 'Set the maximum ramp up rate of mode #3' - fcodes: - - direct_operate - optional: I - point_name: DLFL.RpuRte.AO140 - response: DLFL.RpuRte.AI206 - step_number: 24 - - description: 'Set the maximum ramp down rate of mode #1' - fcodes: - - direct_operate - optional: I - point_name: DPKP.RpdRte.AO123 - response: DPKP.RpdRte.AI185 - step_number: 25 - - description: 'Set the maximum ramp down rate of mode #2' - fcodes: - - direct_operate - optional: I - point_name: DGFL.RpdRte.AO132 - response: DGFL.RpdRte.AI196 - step_number: 26 - - description: 'Set the maximum ramp down rate of mode #3' - fcodes: - - direct_operate - optional: I - point_name: DLFL.RpdRte.AO141 - response: DLFL.RpdRte.AI207 - step_number: 27 - - description: 'Enable the active response mode #1' - fcodes: - - select - - operate - optional: M - point_name: DPKP.ModEna.BO20 - response: DPKP.ModEna.BI72 - step_number: 28 - - description: 'Enable the active response mode #2' - fcodes: - - select - - operate - optional: M - point_name: DGFL.ModEna.BO21 - response: DGFL.ModEna.BI73 - step_number: 29 - - action: publish - description: 'Enable the active response mode #3' - fcodes: - - select - - operate - optional: M - point_name: DLFL.ModEna.BO22 - response: DLFL.ModEna.BI74 - step_number: 30 -- id: perform_automatic_generation_control_mode - name: Perform Automatic Generation Control Mode - ref: AN2018 Spec section 2.6.5 Table 45 - mode_types: - schedule: - - 19 - steps: - - description: Set priority of the mode - fcodes: - - direct_operate - optional: I - point_name: DAGC.ModPrio.AO142 - response: DAGC.ModPrio.AI209 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DAGC.WinTms.AO143 - response: DAGC.WinTms.AI210 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DAGC.RmpTms.AO144 - response: DAGC.RmpTms.AI211 - step_number: 3 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DAGC.RvrtTms.AO145 - response: DAGC.RvrtTms.AI212 - step_number: 4 - - description: Select whether to use Ramp Rates or Time Constants - fcodes: - - direct_operate - optional: I - point_name: DAGC.UseRmpRte.BO39 - response: DAGC.UseRmpRte.BI91 - step_number: 5 - - description: 'If DAGC.UseRmpRte = 0: Set AGC Ramp Time Constant Up Time' - fcodes: - - direct_operate - optional: O - point_name: DAGC.OpnLoop.AO147 - response: DAGC.RmpUpTms.AI214 - step_number: 6 - - description: 'If DAGC.UseRmpRte = 0: Set AGC Ramp Time Constant Down Time' - fcodes: - - direct_operate - optional: O - point_name: DAGC.OpnLoop.AO148 - response: DAGC.RmpDnTms.AI215 - step_number: 7 - - description: 'If DAGC.UseRmpRte = 1: Set AGC Discharge Ramp Up Rate' - fcodes: - - direct_operate - optional: O - point_name: DAGC.DschRpuRte.AO149 - response: DAGC.RpuRte.AI216 - step_number: 8 - - description: 'If DAGC.UseRmpRte = 1: Set AGC Discharge Ramp Down Rate' - fcodes: - - direct_operate - optional: O - point_name: DAGC.DschRpdRte.AO150 - response: DAGC.RpdRte.AI217 - step_number: 9 - - description: 'If DAGC.UseRmpRte = 1: Set AGC Charge Ramp Up Rate' - fcodes: - - direct_operate - optional: O - point_name: DAGC.ChaRpuRte.AO151 - response: DAGC.RpuChaRte.AI218 - step_number: 10 - - description: 'If DAGC.UseRmpRte = 1: Set AGC Charge Ramp Down Rate' - fcodes: - - direct_operate - optional: O - point_name: DAGC.ChaRpdRte.AO152 - response: DAGC.RpdChaRte.AI219 - step_number: 11 - - description: Set Minimum Usable State of Charge (percent of Usable Capacity Rating) - fcodes: - - direct_operate - optional: I - point_name: DAGC.SocUseMinPct.AO153 - response: DAGC.SocUseMinPct.AI220 - step_number: 12 - - description: Set Maximum Usable State of Charge (percent of Usable Capacity Rating) - fcodes: - - direct_operate - optional: I - point_name: DAGC.SocUseMaxPct.AO154 - response: DAGC.SocUseMaxPct.AI221 - step_number: 13 - - description: Set the Active Power Target (in Watts) Positive is discharging, negative - is charging. - fcodes: - - direct_operate - optional: M - point_name: DAGC.GnWSpt.AO146 - response: DAGC.GnWSpt.AI213 - step_number: 14 - - description: Enable AGC mode and receive response. - fcodes: - - select - - operate - optional: M - point_name: DAGC.ModEna.BO23 - response: DAGC.ModEna.BI75 - step_number: 15 - - action: publish - description: Once the mode is enabled, periodically update the Active Power Target. - fcodes: - - direct_operate - optional: M - point_name: DAGC.GnWSpt.AO146 - response: DAGC.GnWSpt.AI213 - step_number: 16 - - description: Read the predicted State of Charge - fcodes: - - read - optional: O - point_name: n/a - response: DAGC.SocExpc.AI224 - step_number: 17 -- id: enable_active_power_smoothing - name: Enable Active Power Smoothing - ref: AN2018 Spec section 2.6.6 Table 46 - mode_types: - schedule: - - 20 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DWSM.ModPrio.AO155 - response: DWSM.ModPrio.AI227 - step_number: 1 - - description: Set time window - fcodes: - - direct_operate - optional: I - point_name: DWSM.WinTms.AO156 - response: DWSM.WinTms.AI228 - step_number: 2 - - description: Set ramp time - fcodes: - - direct_operate - optional: I - point_name: DWSM.RmpTms.AO157 - response: DWSM.RmpTms.AI229 - step_number: 3 - - description: Set reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DWSM.RvrtTms.AO158 - response: DWSM.RvrtTms.AI230 - step_number: 4 - - description: Identify the meter used to measure the Reference Power Input. By - default this is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DWSM.EcpRef.AO159 - response: DWSM.EcpRef.AI231 - step_number: 5 - - description: Set the Active Power Smoothing Gradient - fcodes: - - direct_operate - optional: I - point_name: DWSM.WSmthGra.AO160 - response: DWSM.WSmthGra.AI233 - step_number: 6 - - description: Set the Active Power Smoothing Lower Limit - fcodes: - - direct_operate - optional: I - point_name: DWSM.WSmthLoLim.AO161 - response: DWSM.WSmthLoLim.AI234 - step_number: 7 - - description: Set the Active Power Smoothing Upper Limit - fcodes: - - direct_operate - optional: I - point_name: DWSM.WSmthHiLim.AO162 - response: DWSM.WSmthHiLim.AI235 - step_number: 8 - - description: Set the Active Power Smoothing Filter Time - fcodes: - - direct_operate - optional: I - point_name: DWSM.FilTms.AO163 - response: DWSM.FilTms.AI236 - step_number: 9 - - description: Set the Discharge Ramp Up Rate - fcodes: - - direct_operate - optional: I - point_name: DWSM.DschRpuRte.AO164 - response: DWSM.RpuRte.AI237 - step_number: 10 - - description: Set the Discharge Ramp Down Rate - fcodes: - - direct_operate - optional: I - point_name: DWSM.DschRpdRte.AO165 - response: DWSM.RpdRte.AI238 - step_number: 11 - - description: Set the Charge Ramp Up Rate - fcodes: - - direct_operate - optional: I - point_name: DWSM.ChaRpuRte.AO166 - response: DWSM.RpuChaRte.AI239 - step_number: 12 - - description: Set the Charge Ramp Down Rate - fcodes: - - direct_operate - optional: I - point_name: DWSM.ChaRpdRte.AO167 - response: DWSM.RpdChaRte.AI240 - step_number: 13 - - action: publish - description: Enable Active Power Smoothing Mode - fcodes: - - select - - operate - optional: M - point_name: DWSM.ModEna.BO24 - response: DWSM.ModEna.BI76 - step_number: 14 -- id: enable_volt-watt_curve - mode_types: - curve: - - 5 - schedule: - - 21 - name: Enable Volt-Watt Curve - ref: AN2018 Spec section 2.6.7 Table 47 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DVWC.ModPrio.AO168 - response: DVWC.ModPrio.AI242 - step_number: 1 - - description: Set time window - fcodes: - - direct_operate - optional: I - point_name: DVWC.WinTms.AO169 - response: DVWC.WinTms.AI243 - step_number: 2 - - description: Set ramp time - fcodes: - - direct_operate - optional: I - point_name: DVWC.RmpTms.AO170 - response: DVWC.RmpTms.AI244 - step_number: 3 - - description: Set reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DVWC.RvrtTms.AO171 - response: DVWC.RvrtTms.AI245 - step_number: 4 - - description: Identify the meter used to measure the Voltage. By default this - is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DVWC.EcpRef.AO172 - response: DVWC.EcpRef.AI246 - step_number: 5 - - description: Set the reference voltage if it has not already been set - fcodes: - - direct_operate - optional: I - point_name: DECP.VRef.AO0 - response: DECP.VRef.AI29 - step_number: 6 - - description: Set the reference voltage offset if it has not already been set - fcodes: - - direct_operate - optional: I - point_name: DECP.VRefOfs.AO1 - response: DECP.VRefOfs.AI30 - step_number: 7 - - description: Identify the index of the curve being used - fcodes: - - direct_operate - func_ref: curve - optional: M - point_name: DVWC.VWCrv.AO173 - response: DVWC.VWCrv.AI248 - step_number: 8 - - action: publish - description: Enable the Volt-Watt Mode - fcodes: - - select - - operate - optional: M - point_name: DVWC.ModEna.BO25 - response: DVWC.ModEna.BI77 - step_number: 9 - - description: Read the maximum active power the outstation will attempt to generate - or absorb based on the voltage and the curve in use. - fcodes: - - read - optional: O - point_name: n/a - response: DVWC.ReqWLim.AI249 - step_number: 10 - - description: Read the actual active power produced or absorbed - fcodes: - - read - optional: O - point_name: n/a - response: MMXU.TotW.AI537 - step_number: 11 -- id: enable_frequency-watt_curve_mode - mode_types: - curve: - - 3 - schedule: - - 22 - - 23 - - 24 - name: Enable Frequency-Watt Curve Mode - ref: AN2018 Spec section 2.6.8 Table 48 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DHFW.ModPrio.AO181 - response: DHFW.ModPrio.AI257 - step_number: 1 - - description: Set time window - fcodes: - - direct_operate - optional: I - point_name: DHFW.WinTms.AO182 - response: DHFW.WinTms.AI258 - step_number: 2 - - description: Set ramp time - fcodes: - - direct_operate - optional: I - point_name: DHFW.RvrtTms.AO184 - response: DHFW.RmpTms.AI259 - step_number: 3 - - description: Set reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DHFW.RmpTms.AO183 - response: DHFW.RvrtTms.AI260 - step_number: 4 - - description: Identify the meter used to measure the Frequency. By default this - is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DHFW.EcpRef.AO185 - response: DHFW.EcpRef.AI261 - step_number: 5 - - description: Set the Nominal Grid Frequency if it is not already set - fcodes: - - direct_operate - optional: I - point_name: DECP.EcpNomHz.AO2 - response: DECP.EcpNomHz.AI31 - step_number: 6 - - description: Set the starting delays - fcodes: - - direct_operate - optional: I - point_name: DHFW.ActStrDlTmms.AO189 - response: DHFW.ActStrDlTmms.AI266 - step_number: 7 - - description: Set the stopping delays - fcodes: - - direct_operate - optional: I - point_name: DHFW.ActStopDlTmms.AO190 - response: DHFW.ActStopDlTmms.AI267 - step_number: 8 - - description: Set the frequency-watt curve ramp up time constant for the output - fcodes: - - direct_operate - optional: I - point_name: DHFW.OpnLoopMax.AO191 - response: DHFW.OpnLoopMax.AI268 - step_number: 9 - - description: Set the frequency-watt curve ramp down time constant for the output - fcodes: - - direct_operate - optional: I - point_name: DHFW.OpnLoopMax.AO192 - response: DHFW.OpnLoopMax.AI269 - step_number: 10 - - description: Set the frequency-watt curve discharge ramp up rate - fcodes: - - direct_operate - optional: I - point_name: DHFW.RpuRte.AO193 - response: DHFW.RpuRte.AI270 - step_number: 11 - - description: Set the frequency-watt curve discharge ramp down rate - fcodes: - - direct_operate - optional: I - point_name: DHFW.RpdRte.AO194 - response: DHFW.RpdRte.AI271 - step_number: 12 - - description: Set the frequency-watt curve charge ramp up rate - fcodes: - - direct_operate - optional: I - point_name: DHFW.RpuChaRte.AO195 - response: DHFW.RpuChaRte.AI272 - step_number: 13 - - description: Set the frequency-watt curve charge ramp down rate - fcodes: - - direct_operate - optional: I - point_name: DHFW.RpdChaRte.AO196 - response: DHFW.RpdChaRte.AI273 - step_number: 14 - - description: Identify the index of the main curve being used - fcodes: - - direct_operate - func_ref: curve - optional: I - point_name: DHFW.HzWCrv.AO186 - response: DHFW.HzWCrv.AI263 - step_number: 15 - - description: Identify the index of the high frequency hyteresis curve being used - fcodes: - - direct_operate - func_ref: curve - optional: I - point_name: DHFW.HysCrv.AO187 - response: DHFW.HysCrv.AI264 - step_number: 16 - - description: Identify the index of the low frequency hyteresis curve being used - fcodes: - - direct_operate - func_ref: curve - optional: I - point_name: DLFW.HysCrv.AO188 - response: DLFW.HysCrv.AI265 - step_number: 17 - - description: Set the minimum state of charge in which this mode shall operate - fcodes: - - direct_operate - optional: I - point_name: DHFW.SocUseMinPct.AO197 - response: DHFW.SocUseMinPct.AI275 - step_number: 18 - - description: Set the maximum state of charge in which this mode shall operate - fcodes: - - direct_operate - optional: I - point_name: DHFW.SocUseMaxPct.AO198 - response: DHFW.SocUseMaxPct.AI276 - step_number: 19 - - description: Choose whether to use hysteresis - fcodes: - - direct_operate - optional: I - point_name: DLFW.HysEna.BO36 - response: DLFW.HysEna.BI88 - step_number: 20 - - description: Choose whether to snapshot power - fcodes: - - direct_operate - optional: I - point_name: DLFW.SnptEna.BO37 - response: DLFW.SnptEna.BI89 - step_number: 21 - - action: publish - description: Enable the Frequency-Watt Curve Mode - fcodes: - - select - - operate - optional: M - point_name: DHFW.ModEna.BO26 - response: DHFW.ModEna.BI78 - step_number: 22 -- id: set_constant_var_output - name: Set Constant Var Output - ref: AN2018 Spec section 2.7.1 Table 50 - mode_types: - schedule: - - 25 - steps: - - description: Set the priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DVAR.ModPrio.AO199 - response: DVAR.ModPrio.AI277 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DVAR.WinTms.AO200 - response: DVAR.WinTms.AI278 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DVAR.RmpTms.AO201 - response: DVAR.RmpTms.AI279 - step_number: 3 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DVAR.RvrtTms.AO202 - response: DVAR.RvrtTms.AI280 - step_number: 4 - - description: Set the ramp up Time Constants - fcodes: - - direct_operate - optional: I - point_name: DVAR.OpnLoopMax.AO204 - response: DVAR.OpnLoopMax.AI282 - step_number: 5 - - description: Set the ramp down Time Constants - fcodes: - - direct_operate - optional: I - point_name: DVAR.OpnLoopMax.AO205 - response: DVAR.OpnLoopMax.AI283 - step_number: 6 - - description: Choose whether the time constants represent 3-Tau limits or Open - Loop Response Times - fcodes: - - direct_operate - optional: I - point_name: DSTO.OpnLoopTau.BO9 - response: DSTO.OpnLoopTau.BI28 - step_number: 7 - - description: If Open Loop Response Times are selected, choose the percentage of - final output represented by the time constant (e.g. 90% or 95%) - fcodes: - - direct_operate - optional: I - point_name: DSTO.OpnLoopPct.AO3 - response: DGEN.OpnLoopPct.AI40 - step_number: 8 - - description: Select the meaning of the Constant VArs Reactive Power Target - fcodes: - - direct_operate - optional: I - point_name: DSTO.VArRef.AO5 - response: DGEN.VArSetRef.AI42 - step_number: 9 - - description: 'If DSTO.VArRef = 1: Read percent of maximum active generation power' - fcodes: - - read - - response - optional: O - point_name: n/a - response: DGEN.WMax.AI32 - step_number: 10 - - description: 'If DSTO.VArRef = 1: Read percent of maximum active charging power' - fcodes: - - read - - response - optional: O - point_name: n/a - response: DSTO.ChaWMax.AI33 - step_number: 11 - - description: 'If DSTO.VArRef = 2: Read percent of maximum reactive injection power' - fcodes: - - read - - response - optional: O - point_name: n/a - response: DGEN.IvarMax.AI34 - step_number: 12 - - description: 'If DSTO.VArRef = 2: Read percent of maximum reactive absorption - power' - fcodes: - - read - - response - optional: O - point_name: n/a - response: DGEN.AvarMax.AI35 - step_number: 13 - - description: 'If DSTO.VArRef = 3: Read percent of system reactive injection power' - fcodes: - - read - - response - optional: O - point_name: n/a - response: DGEN.AvarAvl.AI45 - step_number: 14 - - description: 'If DSTO.VArRef = 3: Read percent of system reactive absorption power' - fcodes: - - read - - response - optional: O - point_name: n/a - response: DGEN.IvarAvl.AI46 - step_number: 15 - - description: Set the Constant VArs Reactive Power Target in percent - fcodes: - - direct_operate - optional: M - point_name: DVAR.VArTgtPct.AO203 - response: DVAR.VArTgtPct.AI281 - step_number: 16 - - action: publish - description: Enable Constant VArs mode and receive response - fcodes: - - select - - operate - optional: M - point_name: DVAR.ModEna.BO27 - response: DVAR.ModEna.BI79 - step_number: 17 -- id: set_a_fixed_power_factor - name: Set a Fixed Power Factor - ref: AN2018 Spec section 2.7.2 Table 52 - mode_types: - schedule: - - 26 - steps: - - description: Set the priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DFPF.ModPrio.AO206 - response: DFPF.ModPrio.AI284 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DFPF.WinTms.AO207 - response: DFPF.WinTms.AI285 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DFPF.RmpTms.AO208 - response: DFPF.RmpTms.AI286 - step_number: 3 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DFPF.RvrtTms.AO209 - response: DFPF.RvrtTms.AI287 - step_number: 4 - - description: Set the requirement for whether to inject or absorb VARs (PFExt) - when discharging / generating - fcodes: - - direct_operate - optional: I - point_name: DFPF.PFGnExtSet.BO10 - response: DFPF.PFGnExtSet.BI29 - step_number: 5 - - description: Set the requirement for whether to inject or absorb VARs (PFExt) - when charging - fcodes: - - direct_operate - optional: I - point_name: DFPF.PFLodExtSet.BO11 - response: DFPF.PFLodExtSet.BI30 - step_number: 6 - - description: Set Fixed Power Factor Setpoint to use when generating / discharging - fcodes: - - direct_operate - optional: M - point_name: DFPF.PFGnTgt.AO210 - response: DFPF.PFGnTgt.AI288 - step_number: 7 - - description: Set Fixed Power Factor Setpoint to use when charging - fcodes: - - direct_operate - optional: M - point_name: DFPF.PFLodTgt.AO211 - response: DFPF.PFLodTgt.AI289 - step_number: 8 - - action: publish - description: Enable fixed power factor mode and receive response - fcodes: - - select - - operate - optional: M - point_name: DFPF.ModEna.BO28 - response: DFPF.BI47 - step_number: 9 -- id: change_and_select_volt-var_control_mode - mode_types: - curve: - - 2 - schedule: - - 27 - name: Change and Select Volt-Var Control Mode - ref: AN2018 Spec section 2.7.3 Table 54 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DVVR.ModPrio.AO212 - response: DVVR.ModPrio.AI290 - step_number: 1 - - description: Set the enabling time window - fcodes: - - direct_operate - optional: I - point_name: DVVR.WinTms.AO213 - response: DVVR.WinTms.AI291 - step_number: 2 - - description: Set the enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DVVR.RmpTms.AO214 - response: DVVR.RmpTms.AI292 - step_number: 3 - - description: Set the enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DVVR.RvrtTms.AO215 - response: DVVR.RvrtTms.AI293 - step_number: 4 - - description: Identify the meter used to measure the voltage. By default this - is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DVVR.EcpRef.AO216 - response: DVVR.EcpRef.AI294 - step_number: 5 - - description: If using a fixed Voltage reference, set the reference voltage if - it has not already been set - fcodes: - - direct_operate - optional: O - point_name: DECP.VRef.AO0 - response: DECP.VRef.AI29 - step_number: 6 - - description: If using a fixed Voltage reference, set the reference voltage offset - if it has not already been set - fcodes: - - direct_operate - optional: O - point_name: DECP.VRefOfs.AO1 - response: DECP.VRefOfs.AI30 - step_number: 7 - - description: If autonomously adjusting the Voltage reference, set the time constant - for the lowpass filter - fcodes: - - direct_operate - optional: O - point_name: DVVR.VRefTmms.AO220 - response: DVVR.VRefTmms.AI300 - step_number: 8 - - description: If autonomously adjusting the Voltage reference, enable autonomous - adjustment - fcodes: - - direct_operate - optional: O - point_name: DVVR.VRefAdjEna.BO41 - response: DVVR.VRefAdjEna.BI93 - step_number: 9 - - description: Set the ramp up time constant for the output of the curve - fcodes: - - direct_operate - optional: I - point_name: DVVR.OpnLoopMax.AO218 - response: DVVR.OpnLoopMax.AI298 - step_number: 10 - - description: Set the ramp down time constant for the output of the curve - fcodes: - - direct_operate - optional: I - point_name: DVVR.OpnLoopMax.AO219 - response: DVVR.OpnLoopMax.AI299 - step_number: 11 - - description: Identify the index of the curve being used - fcodes: - - direct_operate - func_ref: curve - optional: M - point_name: DVVR.VVArCrv.AO217 - response: DVVR.VVArCrv.AI297 - step_number: 12 - - action: publish - description: Enable the Volt-VAr Control Mode - fcodes: - - select - - operate - optional: M - point_name: DVVC.ModEna.BO29 - response: DVVC.BI48 - step_number: 13 - - description: Read the adjusted reference voltage, if it is not fixed - fcodes: - - read - optional: O - point_name: n/a - response: DVVR.VRefSet.AI296 - step_number: 14 - - description: Read the measured Voltage - fcodes: - - read - optional: O - point_name: n/a - response: MMXN.Vol.AI295 - step_number: 15 - - description: Read the attempted VArs - fcodes: - - read - optional: O - point_name: n/a - response: DVVR.ReqVAr.AI301 - step_number: 16 - - description: Read the actual VArs (if using system meter) - fcodes: - - read - optional: O - point_name: n/a - response: MMXU.TotVAr.AI541 - step_number: 17 -- id: enable_watt-var_power_mode - mode_types: - curve: - - 4 - schedule: - - 28 - name: Enable Watt-Var Power Mode - ref: AN2018 Spec section 2.7.4 Table 55 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DWVR.ModPrio.AO221 - response: DWVR.ModPrio.AI302 - step_number: 1 - - description: Set the enabling time window - fcodes: - - direct_operate - optional: I - point_name: DWVR.WinTms.AO222 - response: DWVR.WinTms.AI303 - step_number: 2 - - description: Set the enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DWVR.RmpTms.AO223 - response: DWVR.RmpTms.AI304 - step_number: 3 - - description: Set the enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DWVR.RvrtTms.AO224 - response: DWVR.RvrtTms.AI305 - step_number: 4 - - description: Identify the meter used to measure the active power. By default - this is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DWVR.EcpRef.AO225 - response: DWVR.EcpRef.AI306 - step_number: 5 - - description: Read the maximum generation power used as the reference for percent - Watts - fcodes: - - read - - response - optional: O - point_name: n/a - response: DGEN.WMax.AI32 - step_number: 6 - - description: Read the maximum charging power used as the reference for percent - Watts - fcodes: - - read - - response - optional: O - point_name: n/a - response: DSTO.ChaWMax.AI33 - step_number: 7 - - description: Set the ramp up time constant for the output of the curve - fcodes: - - direct_operate - optional: I - point_name: DWVR.OpnLoopMax.AO227 - response: DWVR.OpnLoopMax.AI309 - step_number: 8 - - description: Set the ramp down time constant for the output of the curve - fcodes: - - direct_operate - optional: I - point_name: DWVR.OpnLoopMax.AO228 - response: DWVR.OpnLoopMax.AI310 - step_number: 9 - - description: Identify the index of the curve being used - fcodes: - - direct_operate - func_ref: curve - optional: M - point_name: DWVR.WVArCrv.AO226 - response: DWVR.WVArCrv.AI308 - step_number: 10 - - action: publish - description: Enable the Watt-VAr Power Mode - fcodes: - - select - - operate - optional: M - point_name: DWVR.ModEna.BO30 - response: DWVR.BI49 - step_number: 11 -- id: enable_power_factor_correction_mode - name: Enable Power Factor Correction Mode - ref: AN2018 Spec section 2.7.5 Table 56 - mode_types: - schedule: - - 29 - steps: - - description: Set the priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DPFC.ModPrio.AO229 - response: DPFC.ModPrio.AI312 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DPFC.WinTms.AO230 - response: DPFC.WinTms.AI313 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DPFC.RmpRte.AO231 - response: DPFC.RmpTms.AI314 - step_number: 3 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DPFC.RvrtTms.AO232 - response: DPFC.RvrtTms.AI315 - step_number: 4 - - description: Identify the meter used to measure the active power. By default - this is the System Meter (ID = 0) - fcodes: - - direct_operate - optional: I - point_name: DPFC.EcpRef.AO233 - response: DPFC.EcpRef.AI316 - step_number: 5 - - description: Set the requirement for whether to inject or absorb VARs (PFExt) - when discharging - fcodes: - - direct_operate - optional: I - point_name: DFPF.PFGnExtSet.BO10 - response: DFPF.PFGnExtSet.BI29 - step_number: 6 - - description: Set the requirement for whether to inject or absorb VARs (PFExt) - when charging - fcodes: - - direct_operate - optional: I - point_name: DFPF.PFLodExtSet.BO11 - response: DFPF.PFLodExtSet.BI30 - step_number: 7 - - description: Set the Average PF Target - fcodes: - - direct_operate - optional: M - point_name: DPFC.PFTrg.AO234 - response: MMXU.TotPF.AI317 - step_number: 8 - - description: Set the Lower PF Limit - fcodes: - - direct_operate - optional: M - point_name: DPFC.PFCorRef.rangeC.AO235 - response: DPFC.PFTrg.AI318 - step_number: 9 - - description: Set the Upper PF Limit - fcodes: - - direct_operate - optional: M - point_name: DPFC.PFCorRef.rangeC.AO236 - response: DPFC.PFCorRef.rangeC.AI319 - step_number: 10 - - action: publish - description: Enable Power Factor Correction Mode - fcodes: - - select - - operate - optional: M - point_name: DPFC.ModEna.BO31 - response: DPFC.ModEna.BI83 - step_number: 11 -- id: signal_a_price_change - name: Signal a Price Change - ref: AN2018 Spec section 2.8 Table 57 - steps: - - description: Set priority of this mode - fcodes: - - direct_operate - optional: I - point_name: DPRG.ModPrio.AO237 - response: DPRG.ModPrio.AI321 - step_number: 1 - - description: Set enabling time window - fcodes: - - direct_operate - optional: I - point_name: DPRG.WinTms.AO238 - response: DPRG.WinTms.AI322 - step_number: 2 - - description: Set enabling ramp time - fcodes: - - direct_operate - optional: I - point_name: DPRG.RmpTms.AO239 - response: DPRG.RmpTms.AI323 - step_number: 3 - - description: Set enabling reversion timeout - fcodes: - - direct_operate - optional: I - point_name: DPRG.RvrtTms.AO240 - response: DPRG.RvrtTms.AI324 - step_number: 4 - - description: Set pricing mode time constant for ramping up - fcodes: - - direct_operate - optional: I - point_name: DPRG.OpnLoopMax.AO242 - response: DPRG.OpnLoopMax.AI326 - step_number: 5 - - description: Set pricing mode time constant for ramping down - fcodes: - - direct_operate - optional: I - point_name: DPRG.OpnLoopMax.AO243 - response: DPRG.OpnLoopMax.AI327 - step_number: 6 - - description: Set pricing signal and receive response - fcodes: - - select - - operate - optional: M - point_name: DPRG.PrcRef.AO241 - response: DPRG.PrcRef.AI325 - step_number: 7 - - action: publish - description: Enable pricing signal mode and receive response - fcodes: - - select - - operate - optional: M - point_name: DPRG.ModEna.BO32 - response: DPRG.ModEna.BI84 - step_number: 8 -- id: enable_schedules - name: Enable Schedules - ref: AN2018 Spec section 2.9 Table 59 - steps: - - description: Enable the Schedule by changing its state to ready - fcodes: - - select - - operate - optional: M - point_name: FSCHxx.Mod.BO42 - response: FSCH.SchdSt.3.BI108 - step_number: 1 - - description: Select shedule index - fcodes: - - direct_operate - func_ref: schedule - optional: M - point_name: FSCC.Schd.AO461 - response: FSCC.Schd.AI570 - step_number: 2 - - description: Check that outstation validates the selected schedule - fcodes: - - read - optional: O - point_name: n/a - response: FSCH.SchdSt.2.BI109 - step_number: 3 - - description: Set selected schedule repeat weekly Sunday - fcodes: - - select - - operate - optional: I - point_name: FSCHxx.SchdReuse.BO43 - response: FSCHxx.SchdReuse.BI110 - step_number: 4 - - description: Set selected schedule repeat weekly Monday - fcodes: - - select - - operate - optional: I - point_name: FSCHxx.SchdReuse.BO44 - response: FSCHxx.SchdReuse.BI111 - step_number: 5 - - description: Set selected schedule repeat weekly Tuesday - fcodes: - - select - - operate - optional: I - point_name: FSCHxx.SchdReuse.BO45 - response: FSCHxx.SchdReuse.BI112 - step_number: 6 - - description: Set selected schedule repeat weekly Wednesday - fcodes: - - select - - operate - optional: I - point_name: FSCHxx.SchdReuse.BO46 - response: FSCHxx.SchdReuse.BI113 - step_number: 7 - - description: Set selected schedule repeat weekly Thursday - fcodes: - - select - - operate - optional: I - point_name: FSCHxx.SchdReuse.BO47 - response: FSCHxx.SchdReuse.BI114 - step_number: 8 - - description: Set selected schedule repeat weekly Friday - fcodes: - - select - - operate - optional: I - point_name: FSCHxx.SchdReuse.BO48 - response: FSCHxx.SchdReuse.BI115 - step_number: 9 - - action: publish - description: Set selected schedule repeat weekly Saturday - fcodes: - - select - - operate - optional: I - point_name: FSCHxx.SchdReuse.BO49 - response: FSCHxx.SchdReuse.BI116 - step_number: 10 - - description: Be notified when the schedule is running - fcodes: - - read - optional: O - point_name: n/a - response: FSCH1.SchdSt.AI579 - step_number: 11 -- id: curve - name: Curve - ref: AN2018 Spec Curve Definition - steps: - - description: Select which curve to edit - fcodes: - - direct_operate - optional: M - point_name: DGSMn.InCrv.AO244 - response: DGSMn.InCrv.AI328 - step_number: 1 - - description: Specify the Curve Mode Type - fcodes: - - direct_operate - optional: M - point_name: DGSMn.ModTyp.AO245 - response: DGSMn.ModTyp.AI329 - step_number: 2 - - description: Specify that the Independent (X-Value) units for the curve - fcodes: - - direct_operate - optional: M - point_name: FMARn.IndpUnits.AO247 - response: FMARn.IndpUnits.AI331 - step_number: 3 - - description: Specify the Dependent (Y-Value) units for the curve - fcodes: - - direct_operate - optional: M - point_name: FMARn.DepRef.AO248 - response: FMARn.DepRef.AI332 - step_number: 4 - - description: Set X-Value and Y-Values pairs for the curve - fcodes: - - direct_operate - optional: M - point_name: FMARn.PairArr.CrvPts.AO249 - response: FMARn.PairArr.CrvPts.AI333 - step_number: 5 - - action: publish - description: Set number of points used for the curve - fcodes: - - direct_operate - optional: M - point_name: FMARn.PairArr.NumPts.AO246 - response: FMARn.PairArr.NumPts.AI330 - step_number: 6 -- id: schedule - name: Schedule - ref: AN2018 Spec Schedule Definition - steps: - - description: Select which schedule to edit. This is the index of the schedule, - not its identity. The indexes shall be the monotonically increasing integers - 12, 13, 14 .etc. while the curve identities may be any unique number. - fcodes: - - direct_operate - optional: M - point_name: FSCC.Schd.AO461 - response: FSCC.Schd.AI570 - step_number: 1 - - description: Set the identity of the schedule to a unique number - fcodes: - - direct_operate - optional: M - point_name: FSCC.Schd.AO462 - response: FSCC.Schd.AI571 - step_number: 2 - - description: Set the priority for the schedule - fcodes: - - direct_operate - optional: M - point_name: FSCH1.SchdPrio.AO463 - response: FSCH.SchdPrio.AI572 - step_number: 3 - - description: Set the meaning of the Y-values of the schedule, i.e. the schedule - type. Refer to Table 58. - fcodes: - - direct_operate - optional: M - point_name: FSCH.SchdVal.valEq.AO464 - response: AI573 - step_number: 4 - - description: Set the date for the schedule to start - fcodes: - - direct_operate - optional: M - point_name: FSCH.StrTm.AO465 - response: FSCH.StrTm.AI574 - step_number: 5 - - description: Set the time for the schedule to start - fcodes: - - direct_operate - optional: M - point_name: FSCH.StrTm.AO466 - response: FSCH.StrTm.AI575 - step_number: 6 - - description: Set the repetition interval - fcodes: - - direct_operate - optional: M - point_name: FSCH.NxtStrTm.AO467 - response: FSCH.NxtStrTm.AI576 - step_number: 7 - - description: Set the units of the repetition interval - fcodes: - - direct_operate - optional: M - point_name: FSCH.SchdReuse.AO468 - response: FSCH.SchdReuse.AI577 - step_number: 8 - - description: Set the Time Offset (X-Values) and Schedule Value (Y-Values) for - each schedule point - fcodes: - - direct_operate - optional: M - point_name: FSCHn.SchdEntr.AO470 - response: FSCHn.SchdEntr.AI581 - step_number: 9 - - action: publish - description: Set the number of points used for the schedule. - fcodes: - - direct_operate - optional: M - point_name: FSCH.NumEntr.AO469 - response: FSCH.NumEntr.AI580 - step_number: 10 \ No newline at end of file diff --git a/services/core/DNP3Agent/tests/data/mesa_points.config b/services/core/DNP3Agent/tests/data/mesa_points.config deleted file mode 100644 index 1ce6cdace7..0000000000 --- a/services/core/DNP3Agent/tests/data/mesa_points.config +++ /dev/null @@ -1,10276 +0,0 @@ -[ - { - "index": 0, - "description": "Reference Voltage", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DECP", - "units": "Volts", - "minimum": 0, - "data_object": "VRef", - "name": "DECP.VRef.AO0" - }, - { - "index": 1, - "description": "Reference Voltage Offset", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DECP", - "units": "Volts", - "data_object": "VRefOfs", - "name": "DECP.VRefOfs.AO1" - }, - { - "index": 2, - "description": "Nominal Grid Frequency", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DECP", - "units": "Hz", - "minimum": 0, - "data_object": "EcpNomHz", - "name": "DECP.EcpNomHz.AO2" - }, - { - "index": 3, - "description": "Open Loop Response Time Percentage. Percent of target to reach within the open loop response time. Default is 90%.", - "data_type": "AO", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DSTO", - "units": "Percent", - "minimum": 0, - "data_object": "OpnLoopPct", - "name": "DSTO.OpnLoopPct.AO3" - }, - { - "index": 4, - "description": "Power Factor Sign convention", - "data_type": "AO", - "common_data_class": "ENG", - "maximum": 2, - "ln_class": "MMXU", - "units": "None", - "minimum": 1, - "data_object": "PFSign", - "allowed_values": { - "1": "IEC active power", - "2": "IEEE lead/lag" - }, - "type": "enumerated", - "name": "MMXU.PFSign.AO4" - }, - { - "index": 5, - "description": "Reference for Reactive Power Setpoints. Selects which setpoint is active. Default is <3>.", - "data_type": "AO", - "common_data_class": "ENG", - "maximum": 3, - "ln_class": "DSTO", - "units": "None (list)", - "minimum": 0, - "data_object": "VArRef", - "allowed_values": { - "0": "Not applicable / Unknown", - "1": "Percent of Maximum Active Power (WMax)", - "2": "Percent of Maximum Reactive Power (VArMax)", - "3": "Percent of Available Reactive Power (VArAval)" - }, - "type": "enumerated", - "name": "DSTO.VArRef.AO5" - }, - { - "index": 6, - "description": "DER Start (Return to Service) Voltage High Limit. Percent of Reference Voltage.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 20000, - "ln_class": "DCTE", - "units": "Percent", - "minimum": 0, - "data_object": "VHiLim", - "name": "DCTE.VHiLim.AO6" - }, - { - "index": 7, - "description": "DER Start (Return to Service) Voltage Low Limit. Percent of Reference Voltage.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 10000, - "ln_class": "DCTE", - "units": "Percent", - "minimum": 0, - "data_object": "VLoLim", - "name": "DCTE.VLoLim.AO7" - }, - { - "index": 8, - "description": "DER Start (Return to Service) Frequency High Limit", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DCTE", - "units": "Hz", - "minimum": 0, - "data_object": "HzHiLim", - "name": "DCTE.HzHiLim.AO8" - }, - { - "index": 9, - "description": "DER Start (Return to Service) Frequency Low Limit", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DCTE", - "units": "Hz", - "minimum": 0, - "data_object": "HzLoLim", - "name": "DCTE.HzLoLim.AO9" - }, - { - "index": 10, - "description": "DER Start (Return to Service) Delay", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RtnDlyTmms", - "name": "DCTE.RtnDlyTmms.AO10" - }, - { - "index": 11, - "description": "DER Start (Return to Service) Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DCTE.WinTms.AO11" - }, - { - "index": 12, - "description": "DER Start (Return to Service) Ramp Up Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RtnRmpTmms", - "name": "DCTE.RtnRmpTmms.AO12" - }, - { - "index": 13, - "description": "DER Stop (Cease to Energize) Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DCTE.WinTms.AO13" - }, - { - "index": 14, - "description": "DER Stop (Cease to Energize) Ramp Down Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DCTE.RmpTms.AO14" - }, - { - "index": 15, - "description": "DER Stop (Cease to Energize) Reversion Timeout Period. Time to revert from the stopped state and return to service.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DCTE.RvrtTms.AO15" - }, - { - "index": 16, - "description": "Connect/Disconnect Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DCTE.WinTms.AO16" - }, - { - "index": 17, - "description": "Connect/Disconnect Reversion Timeout Period. Timeout (reversion time is for the Disconnect only).", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DCTE.RvrtTms.AO17" - }, - { - "index": 18, - "description": "Requested Settings Group", - "data_type": "AO", - "common_data_class": "ENG", - "maximum": 255, - "ln_class": "DECP", - "units": "None (list)", - "minimum": 0, - "data_object": "EcpIsldSt", - "allowed_values": { - "0": "Not Used", - "1": "Unspecified / Autonomously Determined (see BO Enable Sensed Grid Config Detection)", - "2": "Factory Configuration", - "3": "Default Configuration / Comms Lost", - "4": "Normal Grid-Connected Configuration", - "5": "Islanded Condition 1 (small, local island)", - "6": "Islanded Condition 2 (larger, area island)", - "7": "Islanded Condition 3 (largest, regional island)", - "8": "1st Alternate Grid-Connected Configuration", - "9": "2nd Alternate Grid-Connected Configuration", - "10": "3rd Alternate Grid-Connected Configuration" - }, - "type": "enumerated", - "name": "DECP1.EcpIsldSt.AO18" - }, - { - "index": 19, - "description": "Settings Group Being Edited", - "data_type": "AO", - "common_data_class": "ENG", - "maximum": 255, - "ln_class": "DRCC", - "units": "None (list)", - "minimum": 0, - "data_object": "EcpIsldSt", - "name": "DRCC1.EcpIsldSt.AO19" - }, - { - "index": 20, - "description": "Freeze Counter Interval. Interval between freeze counter operations after the initial occurrence. A zero value means the free counter operation is not repeated.", - "data_type": "AO", - "minimum": 0, - "name": "AO20" - }, - { - "index": 21, - "description": "Freeze Counter Interval Units. Units of the interval between freeze counter operations.", - "data_type": "AO", - "maximum": 9, - "minimum": 0, - "units": "None (list)", - "allowed_values": { - "0": "The outstation does not repeat the action, regardless of the Interval count.", - "1": "Milliseconds - In this case the interval is always counted relative to the Start Time and is constant regardless of the clock time set at the Outstation.", - "2": "Seconds - At the same millisecond within the second that is specified in the Start Time.", - "3": "Minutes - At the same second and millisecond within the minute that is specified in the Start Time.", - "4": "Hours - At the same minute,second and B7millisecond within the hour that is specified in the Start Time.", - "5": "Days - At the same time of day that is specified in the Start Time.", - "6": "Weeks - On the same day of the week at the same time of day that is specified in the Start Time", - "7": "Months - On the same day of each month at the same time of day that is specified in the Start Time. If the Start Time falls on the 29th or greater day of the month, the outstation shall not perform the action in months that do not have such a day.", - "8": "Months on Same Day of Week from Start of Month - At the same time of the day on the same day of the week after the beginning of the month as the day specified in the Start Time. For instance, if the Start Time specifies the second Tuesday of February and the Interval Count is 2, the next action shall occur on the second Tuesday of April. In the same example, if the Interval Count is set to 12, this is the same as specifying, Every year on the second Tuesday in February. If the specified day does not occur in a given month when an action was scheduled to occur, the outstation shall not perform the action that month but shall perform it at the next valid scheduled time.", - "9": "Months on Same Day of Week from End of Month - The outstation shall interpret this setting as in <8>, but the day of the week shall be measured from the end of the month, e.g., the second-last Tuesday in February." - }, - "type": "enumerated", - "name": "AO21" - }, - { - "index": 22, - "description": "Low/High Voltage Ride-Through Signal Meter ID. Referenced ECP. This is the meter from which current is being read to evaluate and provide support.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DHVT", - "minimum": 0, - "data_object": "EcpRef", - "name": "DHVT.EcpRef.AO22" - }, - { - "index": 23, - "description": "Low/High Voltage Ride-Through High Must Trip Curve Index. Index of the Voltage Ride-through curve which specifies trip points when the voltage is high.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "PTOV", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTOV.BlkRef.AO23" - }, - { - "index": 24, - "description": "Low/High Voltage Ride-Through Low Must Trip Curve Index. Index of the Voltage Ride-through curve which specifies trip points when the voltage is low.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "PTUV", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTUV.BlkRef.AO24" - }, - { - "index": 25, - "description": "Low/High Voltage Ride-Through High Momentary Cessation Curve Index. Index of the Voltage Ride-through curve which specifies where generation/discharging must stop when the voltage is high.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "PTOV", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTOV.BlkRef.AO25" - }, - { - "index": 26, - "description": "Low/High Voltage Ride-Through Low Momentary Cessation Curve Index. Index of the Voltage Ride-through curve which specifies where generation/discharging must stop when the voltage is low.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "PTUV", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTUV.BlkRef.AO26" - }, - { - "index": 27, - "description": "Low/High Frequency Ride-Through Signal Meter ID. Referenced ECP. This is the meter from which current is being read to evaluate and provide support.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DHFT", - "minimum": 0, - "data_object": "EcpRef", - "name": "DHFT.EcpRef.AO27" - }, - { - "index": 28, - "description": "Low/High Frequency Ride-Through High Must Trip Curve Index. Index of the Frequency Ride-through curve which specifies trip points when the frequency is high.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "PTOF", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTOF.BlkRef.AO28" - }, - { - "index": 29, - "description": "Low/High Frequency Ride-Through Low Must Trip Curve Index. Index of the Frequency Ride-through curve which specifies trip points when the frequency is low.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "PTUF", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTUF.BlkRef.AO29" - }, - { - "index": 30, - "description": "Low/High Frequency Ride-Through High Momentary Cessation Curve Index. Index of the Frequency Ride-through curve which specifies where generation/discharging must stop when the frequency is high.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "PTOF", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTOF.BlkRef.AO30" - }, - { - "index": 31, - "description": "Low/High Frequency Ride-Through Low Momentary Cessation Curve Index. Index of the Frequency Ride-through curve which specifies where generation/discharging must stop when the frequency is low.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "PTUF", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTUF.BlkRef.AO31" - }, - { - "index": 32, - "description": "Dynamic Reactive Current Support Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DRGS", - "minimum": 0, - "data_object": "ModPrio", - "name": "DRGS.ModPrio.AO32" - }, - { - "index": 33, - "description": "Dynamic Reactive Current Support Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DRGS.WinTms.AO33" - }, - { - "index": 34, - "description": "Dynamic Reactive Current Support Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DRGS.RmpTms.AO34" - }, - { - "index": 35, - "description": "Dynamic Reactive Current Support Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DRGS.RvrtTms.AO35" - }, - { - "index": 36, - "description": "Dynamic Reactive Current Support Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DRGS", - "minimum": 0, - "data_object": "EcpRef", - "name": "DRGS.EcpRef.AO36" - }, - { - "index": 37, - "description": "Dynamic Reactive Current Support - Gradient Mode.", - "data_type": "AO", - "common_data_class": "ENG", - "maximum": 2, - "ln_class": "DRGS", - "units": "None (list)", - "minimum": 0, - "data_object": "ArGraMod", - "allowed_values": { - "0": "Undefined", - "1": "Gradients reach 0 at the moving average Voltage", - "2": "Gradients reach 0 at the Voltage deadbands" - }, - "type": "enumerated", - "name": "DRGS.ArGraMod.AO37" - }, - { - "index": 38, - "description": "Dynamic Reactive Current Support Deadband Minimum Voltage. Percentage of the nominal voltage (DRCT.Vref), measured from the moving average voltage (RDGS.VAv). Support is no longer applied when the voltage stays above this value for the length of the Hold Time.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 0, - "ln_class": "DRGS", - "units": "Percent", - "minimum": -10000, - "data_object": "DbVMin", - "name": "DRGS.DbVMin.AO38" - }, - { - "index": 39, - "description": "Dynamic Reactive Current Support Deadband Maximum Voltage. Percentage of the nominal voltage (DRCT.Vref), measured from the moving average voltage (RDGS.VAv). Support is no longer applied when the voltage stays below this value for the length of the Hold Time.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 10000, - "ln_class": "DRGS", - "units": "Percent", - "minimum": 0, - "data_object": "DbVMax", - "name": "DRGS.DbVMax.AO39" - }, - { - "index": 40, - "description": "Dynamic Reactive Current Support Gradient for Sags. Percentage of the rated current (DRAT.ARtg) to apply capacitively per percentage of the negative deviation from the moving average voltage (RDGS.Av). It is a ratio of percent and is therefore unitless.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DRGS", - "units": "Percent current per percent voltage deviation", - "data_object": "ArGraSag", - "name": "DRGS.ArGraSag.AO40" - }, - { - "index": 41, - "description": "Dynamic Reactive Current Support Gradient for Swells. Percentage of the rated current (DRAT.ARtg) to apply inductively per percentage of the positive deviation from the moving average voltage (RDGS.Av). It is a ratio of percent and is therefore unitless.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DRGS", - "units": "Percent current per percent voltage deviation", - "data_object": "ArGraSwl", - "name": "DRGS.ArGraSwl.AO41" - }, - { - "index": 42, - "description": "Dynamic Reactive Current Support Filter Time for Moving Average Voltage (RDGS.VAv). Used to determine amount of dynamic reactive current support.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "Seconds", - "minimum": 0, - "data_object": "FilTms", - "name": "DRGS.FilTms.AO42" - }, - { - "index": 43, - "description": "Dynamic Reactive Current Support Block Zone Voltage. Percentage of the nominal voltage (DRCT.VRef) below which no reactive current support shall be applied.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DRGS", - "units": "Percent", - "minimum": 0, - "data_object": "BlkZnV", - "name": "DRGS.BlkZnV.AO43" - }, - { - "index": 44, - "description": "Dynamic Reactive Current Support Hysteresis Block Zone Voltage. Percentage of the nominal voltage (DRCT.VRef). After being blocked, reactive current support shall not resume until the voltage has been above BlkZnV + HysBlkZnV.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DRGS", - "units": "Percent", - "minimum": 0, - "data_object": "HysBlkZnV", - "name": "DRGS.HysBlkZnV.AO44" - }, - { - "index": 45, - "description": "Dynamic Reactive Current Support Block Zone Time. Time in milliseconds from the beginning of any \"sag\" event,before which dynamic reactive current support will always continue,regardless of how low voltage may sag.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "ms", - "minimum": 0, - "data_object": "BlkZnTmms", - "name": "DRGS.BlkZnTmms.AO45" - }, - { - "index": 46, - "description": "Dynamic Reactive Current Support Start Hold Time. When the voltage exceeds the deadband limits for this length of time (measured in milliseconds),the \"sag\" or \"swell\" event begins and the DER may begin altering active power output.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "ms", - "minimum": 0, - "data_object": "HoldTmms", - "name": "DRGS.HoldTmms.AO46" - }, - { - "index": 47, - "description": "Dynamic Reactive Current Support End Hold Time. When the voltage returns to within the deadband limits for this length of time (measured in milliseconds),the \"sag\" or \"swell\" event is considered to be over. Reactive current support ends,frozen values are unfrozen,and a new event can begin.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "ms", - "minimum": 0, - "data_object": "HoldTmms", - "name": "DRGS.HoldTmms.AO47" - }, - { - "index": 48, - "description": "Dynamic Volt-Watt Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWD", - "minimum": 0, - "data_object": "ModPrio", - "name": "DVWD.ModPrio.AO48" - }, - { - "index": 49, - "description": "Dynamic Volt-Watt Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWD", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DVWD.WinTms.AO49" - }, - { - "index": 50, - "description": "Dynamic Volt-Watt Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWD", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DVWD.RmpTms.AO50" - }, - { - "index": 51, - "description": "Dynamic Volt-Watt Reversion Timeout period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWD", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DVWD.RvrtTms.AO51" - }, - { - "index": 52, - "description": "Dynamic Volt-Watt Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DVWD", - "minimum": 0, - "data_object": "EcpRef", - "name": "DVWD2.EcpRef.AO52" - }, - { - "index": 53, - "description": "Dynamic Volt-Watt Gradient. Signed unit-less quantity that establishes the ratio of additional Watts supplied (expressed in terms of % DRCT.WMax) to the present difference from the moving average voltage (expressed as % DRCT.VRef).", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DVWD", - "units": "Percent watts per percent voltage difference", - "data_object": "DynVWGra", - "name": "DVWD.DynVWGra.AO53" - }, - { - "index": 54, - "description": "Dynamic Volt-Watt Filter Time. The time in seconds used to calculate the moving average voltage for dynamic Volt-Watt support.", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DVWD", - "units": "Seconds", - "minimum": 0, - "data_object": "VWFilTms", - "name": "DVWD.VWFilTms.AO54" - }, - { - "index": 55, - "description": "Dynamic Volt-Watt Lower Deadband. Percentage of the nominal voltage (DRCT.Vref) measured below the moving average voltage. If the present voltage is above this value, no additional Watts shall be supplied.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DVWD", - "units": "Percent", - "minimum": 0, - "data_object": "DbVWLo", - "name": "DVWD.DbVWLo.AO55" - }, - { - "index": 56, - "description": "Dynamic Volt-Watt Upper Deadband. Percentage of the nominal voltage (DRCT.Vref) measured above the moving average voltage. If the present voltage is below this value,no additional Watts shall be supplied.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DVWD", - "units": "Percent", - "minimum": 0, - "data_object": "DbVWHi", - "name": "DVWD.DbVWHi.AO56" - }, - { - "index": 57, - "description": "Frequency-Watt Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "ModPrio", - "name": "DHFW2.ModPrio.AO57" - }, - { - "index": 58, - "description": "Frequency-Watt Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DHFW.WinTms.AO58" - }, - { - "index": 59, - "description": "Frequency-Watt Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DHFW.RmpTms.AO59" - }, - { - "index": 60, - "description": "Frequency-Watt Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DHFW.RvrtTms.AO60" - }, - { - "index": 61, - "description": "Frequency-Watt Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "EcpRef", - "name": "DHFW.EcpRef.AO61" - }, - { - "index": 62, - "description": "Frequency-Watt High Starting Frequency", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DHFW", - "units": "Hz", - "minimum": 0, - "data_object": "HzStr", - "name": "DHFW.HzStr.AO62" - }, - { - "index": 63, - "description": "Frequency-Watt High Stopping Frequency", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DHFW", - "units": "Hz", - "minimum": 0, - "data_object": "HzStop", - "name": "DHFW.HzStop.AO63" - }, - { - "index": 64, - "description": "Frequency-Watt High Discharging/Generating Gradient", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DHFW", - "units": "Percent watts per percent frequency difference", - "data_object": "WGra", - "name": "DHFW.WGra.AO64" - }, - { - "index": 65, - "description": "Frequency-Watt High Charging Gradient", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DHFW", - "units": "Percent watts per percent frequency difference", - "data_object": "WChaGra", - "name": "DHFW.WChaGra.AO65" - }, - { - "index": 66, - "description": "Frequency-Watt Low Starting Frequency", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 0, - "ln_class": "DLFW", - "units": "Hz", - "minimum": -70000, - "data_object": "HzStr", - "name": "DLFW.HzStr.AO66" - }, - { - "index": 67, - "description": "Frequency-Watt Low Stopping Frequency", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 0, - "ln_class": "DLFW", - "units": "Hz", - "minimum": -70000, - "data_object": "HzStop", - "name": "DLFW.HzStop.AO67" - }, - { - "index": 68, - "description": "Frequency-Watt Low Discharging/Generating Gradient", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DLFW", - "units": "Percent watts per percent frequency difference", - "data_object": "WGra", - "name": "DLFW.WGra.AO68" - }, - { - "index": 69, - "description": "Frequency-Watt Low Charging Gradient", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DLFW", - "units": "Percent watts per percent frequency difference", - "data_object": "WChaGra", - "name": "DLFW.WChaGra.AO69" - }, - { - "index": 70, - "description": "Frequency-Watt Start Delay", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "ActStrDlTmms", - "name": "DHFW2.ActStrDlTmms.AO70" - }, - { - "index": 71, - "description": "Frequency-Watt Stop Delay", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "ActStopDlTmms", - "name": "DHFW2.ActStopDlTmms.AO71" - }, - { - "index": 72, - "description": "Frequency-Watt Ramp Up Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoop", - "name": "DHFW.OpnLoop.AO72" - }, - { - "index": 73, - "description": "Frequency-Watt Ramp Down Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoop", - "name": "DHFW.OpnLoop.AO73" - }, - { - "index": 74, - "description": "Frequency-Watt Discharge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpuRte", - "name": "DHFW.DschRpuRte.AO74" - }, - { - "index": 75, - "description": "Frequency-Watt Discharge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpdRte", - "name": "DHFW.DschRpdRte.AO75" - }, - { - "index": 76, - "description": "Frequency-Watt Charge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpuRte", - "name": "DHFW.ChaRpuRte.AO76" - }, - { - "index": 77, - "description": "Frequency-Watt Charge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpdRte", - "name": "DHFW.ChaRpdRte.AO77" - }, - { - "index": 78, - "description": "Frequency-Watt Hi Return Gradient", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DHFW", - "units": "Percent watts per percent frequency difference", - "data_object": "RtnRmpRte", - "name": "DHFW.RtnRmpRte.AO78" - }, - { - "index": 79, - "description": "Frequency-Watt Low Return Gradient", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DLFW", - "units": "Percent watts per percent frequency difference", - "data_object": "RtnRmpRte", - "name": "DLFW.RtnRmpRte.AO79" - }, - { - "index": 80, - "description": "Frequency-Watt Minimum Usable SOC", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DHFW", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMin", - "name": "DHFW.SocUseMin.AO80" - }, - { - "index": 81, - "description": "Frequency-Watt Maximum Usable SOC", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DHFW", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMax", - "name": "DHFW.SocUseMax.AO81" - }, - { - "index": 82, - "description": "Active Power Limit Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWMX", - "minimum": 0, - "data_object": "ModPrio", - "name": "DWMX.ModPrio.AO82" - }, - { - "index": 83, - "description": "Active Power Limit Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWMX", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DWMX.WinTms.AO83" - }, - { - "index": 84, - "description": "Active Power Limit Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWMX", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DWMX.RmpTms.AO84" - }, - { - "index": 85, - "description": "Active Power Limit Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWMX", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DWMX.RvrtTms.AO85" - }, - { - "index": 86, - "description": "Active Power Limit Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DWMX", - "minimum": 0, - "data_object": "EcpRef", - "name": "DWMX.EcpRef.AO86" - }, - { - "index": 87, - "description": "Active Power Limit Charge Setpoint. Maximum allowed Watts as a percentage of Maximum Active Power capability.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWMX", - "units": "Percent", - "minimum": 0, - "data_object": "WLimPct", - "name": "DWMX.WLimPct.AO87" - }, - { - "index": 88, - "description": "Active Power Limit Discharge Setpoint. Maximum allowed Watts as a percentage of Maximum Active Power capability.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWMN", - "units": "Percent", - "minimum": 0, - "data_object": "WLimPct", - "name": "DWMN.WLimPct.AO88" - }, - { - "index": 89, - "description": "Charge/Discharge Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWGC", - "minimum": 0, - "data_object": "ModPrio", - "name": "DWGC.ModPrio.AO89" - }, - { - "index": 90, - "description": "Charge/Discharge Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DWGC.WinTms.AO90" - }, - { - "index": 91, - "description": "Charge/Discharge Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DWGC.RmpTms.AO91" - }, - { - "index": 92, - "description": "Charge/Discharge Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DWGC.RvrtTms.AO92" - }, - { - "index": 93, - "description": "Charge/Discharge Active Power Target. Percentage of maxmum active power.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWGC", - "units": "Percent", - "minimum": -1000, - "data_object": "GnWPctSpt", - "name": "DWGC.GnWPctSpt.AO93" - }, - { - "index": 94, - "description": "Charge/Discharge Ramp Up Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoop", - "name": "DWGC.OpnLoop.AO94" - }, - { - "index": 95, - "description": "Charge/Discharge Ramp Down Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoop", - "name": "DWGC.OpnLoop.AO95" - }, - { - "index": 96, - "description": "Charge/Discharge Discharge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpuRte", - "name": "DWGC.DschRpuRte.AO96" - }, - { - "index": 97, - "description": "Charge/Discharge Discharge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpdRte", - "name": "DWGC.DschRpdRte.AO97" - }, - { - "index": 98, - "description": "Charge/Discharge Charge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpuRte", - "name": "DWGC.ChaRpuRte.AO98" - }, - { - "index": 99, - "description": "Charge/Discharge Charge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpdRte", - "name": "DWGC.ChaRpdRte.AO99" - }, - { - "index": 100, - "description": "Charge/Discharge Minimum Reserve for Storage. The minimum level to which the storage system may be discharged,expressed as a percentage of the total usable storage.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWGC", - "units": "Percent", - "minimum": -1000, - "data_object": "SocUseMinPct", - "name": "DWGC.SocUseMinPct.AO100" - }, - { - "index": 101, - "description": "Charge/Discharge Maximum Reserve for Storage. The maximum level to which the storage system may be discharged,expressed as a percentage of the total usable storage.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWGC", - "units": "Percent", - "minimum": -1000, - "data_object": "SocUseMaxPct", - "name": "DWGC.SocUseMaxPct.AO101" - }, - { - "index": 102, - "description": "Coordinated Charge/Discharge Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "minimum": 0, - "data_object": "ModPrio", - "name": "DTCD.ModPrio.AO102" - }, - { - "index": 103, - "description": "Coordinated Charge/Discharge Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DTCD.WinTms.AO103" - }, - { - "index": 104, - "description": "Coordinated Charge/Discharge Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DTCD.RmpTms.AO104" - }, - { - "index": 105, - "description": "Coordinated Charge/Discharge Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DTCD.RvrtTms.AO105" - }, - { - "index": 106, - "description": "Coordinated Charge/Discharge Target State of Charge. Charge that the system is expected to achieve,as a percentage of the usable capacity.", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DTCD", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseTgtPct", - "name": "DTCD.SocUseTgtPct.AO106" - }, - { - "index": 107, - "description": "Coordinated Charge/Discharge Target Date. Date by which the storage system must reach the target SOC. Expressed as number of days since January 1, 1970, UTC.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Days", - "minimum": 0, - "data_object": "DateTgt", - "name": "DTCD.DateTgt.AO107" - }, - { - "index": 108, - "description": "Coordinated Charge/Discharge Target Time. Time by which storage system must reach the target SOC. Expressed as number of milliseconds since the start of Target Date.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "DateTgtTms", - "name": "DTCD.DateTgtTms.AO108" - }, - { - "index": 109, - "description": "Coordinated Charge/Discharge Energy Request", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Watt-hours", - "minimum": 0, - "data_object": "SocWReq", - "name": "DTCD.SocWReq.AO109" - }, - { - "index": 110, - "description": "Coordinated Charge/Discharge Minimum Charging Duration", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "ChaDurTms", - "name": "DTCD.ChaDurTms.AO110" - }, - { - "index": 111, - "description": "Coordinated Charge/Discharge Date of Reference", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Days", - "minimum": 0, - "data_object": "DateTgt", - "name": "DTCD.DateTgt.AO111" - }, - { - "index": 112, - "description": "Coordinated Charge/Discharge Time of Reference", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "SocDateTms", - "name": "DTCD.SocDateTms.AO112" - }, - { - "index": 113, - "description": "Coordinated Charge/Discharge Duration at Maximum Charge Rate", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "ChaDurMax", - "name": "DTCD.ChaDurMax.AO113" - }, - { - "index": 114, - "description": "Coordinated Charge/Discharge Duration at Maximum Discharge Rate", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "DschDurMax", - "name": "DTCD.DschDurMax.AO114" - }, - { - "index": 115, - "description": "Active Power Response Mode #1 Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPKP", - "minimum": 0, - "data_object": "ModPrio", - "name": "DPKP.ModPrio.AO115" - }, - { - "index": 116, - "description": "Active Power Response Mode #1 Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPKP", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DPKP.WinTms.AO116" - }, - { - "index": 117, - "description": "Active Power Response Mode #1 Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPKP", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DPKP.RmpTms.AO117" - }, - { - "index": 118, - "description": "Active Power Response Mode #1 Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPKP", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DPKP.RvrtTms.AO118" - }, - { - "index": 119, - "description": "Active Power Response Mode #1 Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DPKP", - "minimum": 0, - "data_object": "EcpRef", - "name": "DPKP.EcpRef.AO119" - }, - { - "index": 120, - "description": "Active Power Response Mode #1 Power Threshold", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DPKP", - "units": "Watts", - "data_object": "PkPwrWLim", - "name": "DPKP.PkPwrWLim.AO120" - }, - { - "index": 121, - "description": "Active Power Response Mode #1 Ratio", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DPKP", - "units": "Percent", - "minimum": 0, - "data_object": "PkPwrFolPct", - "name": "DPKP.PkPwrFolPct.AO121" - }, - { - "index": 122, - "description": "Active Power Response Mode #1 Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DPKP", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DPKP.RpuRte.AO122" - }, - { - "index": 123, - "description": "Active Power Response Mode #1 Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DPKP", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DPKP.RpdRte.AO123" - }, - { - "index": 124, - "description": "Active Power Response Mode #2 Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DGFL", - "minimum": 0, - "data_object": "ModPrio", - "name": "DGFL.ModPrio.AO124" - }, - { - "index": 125, - "description": "Active Power Response Mode #2 Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DGFL", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DGFL.WinTms.AO125" - }, - { - "index": 126, - "description": "Active Power Response Mode #2 Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DGFL", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DGFL.RmpTms.AO126" - }, - { - "index": 127, - "description": "Active Power Response Mode #2 Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DGFL", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DGFL.RvrtTms.AO127" - }, - { - "index": 128, - "description": "Active Power Response Mode #2 Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DGFL", - "minimum": 0, - "data_object": "EcpRef", - "name": "DGFL.EcpRef.AO128" - }, - { - "index": 129, - "description": "Active Power Response Mode #2 Power Threshold", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DGFL", - "units": "Watts", - "data_object": "PkPwrWLim", - "name": "DGFL.PkPwrWLim.AO129" - }, - { - "index": 130, - "description": "Active Power Response Mode #2 Ratio", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DGFL", - "units": "Percent", - "minimum": 0, - "data_object": "PkPwrFolPct", - "name": "DGFL.PkPwrFolPct.AO130" - }, - { - "index": 131, - "description": "Active Power Response Mode #2 Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DGFL", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DGFL.RpuRte.AO131" - }, - { - "index": 132, - "description": "Active Power Response Mode #2 Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DGFL", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DGFL.RpdRte.AO132" - }, - { - "index": 133, - "description": "Active Power Response Mode #3 Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DLFL", - "minimum": 0, - "data_object": "ModPrio", - "name": "DLFL.ModPrio.AO133" - }, - { - "index": 134, - "description": "Active Power Response Mode #3 Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DLFL", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DLFL.WinTms.AO134" - }, - { - "index": 135, - "description": "Active Power Response Mode #3 Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DLFL", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DLFL.RmpTms.AO135" - }, - { - "index": 136, - "description": "Active Power Response Mode #3 Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DLFL", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DLFL.RvrtTms.AO136" - }, - { - "index": 137, - "description": "Active Power Response Mode #3 Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DLFL", - "minimum": 0, - "data_object": "EcpRef", - "name": "DLFL.EcpRef.AO137" - }, - { - "index": 138, - "description": "Active Power Response Mode #3 Power Threshold", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DLFL", - "units": "Watts", - "data_object": "PkPwrWLim", - "name": "DLFL.PkPwrWLim.AO138" - }, - { - "index": 139, - "description": "Active Power Response Mode #3 Ratio", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DLFL", - "units": "Percent", - "minimum": 0, - "data_object": "PkPwrFolPct", - "name": "DLFL.PkPwrFolPct.AO139" - }, - { - "index": 140, - "description": "Active Power Response Mode #3 Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DLFL", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DLFL.RpuRte.AO140" - }, - { - "index": 141, - "description": "Active Power Response Mode #3 Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DLFL", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DLFL.RpdRte.AO141" - }, - { - "index": 142, - "description": "AGC Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DAGC", - "minimum": 0, - "data_object": "ModPrio", - "name": "DAGC.ModPrio.AO142" - }, - { - "index": 143, - "description": "AGC Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DAGC.WinTms.AO143" - }, - { - "index": 144, - "description": "AGC Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DAGC.RmpTms.AO144" - }, - { - "index": 145, - "description": "AGC Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DAGC.RvrtTms.AO145" - }, - { - "index": 146, - "description": "AGC Active Power Target", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DAGC", - "units": "Watts", - "data_object": "GnWSpt", - "name": "DAGC.GnWSpt.AO146" - }, - { - "index": 147, - "description": "AGC Ramp Time Constant Up Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoop", - "name": "DAGC.OpnLoop.AO147" - }, - { - "index": 148, - "description": "AGC Ramp Time Constant Down Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoop", - "name": "DAGC.OpnLoop.AO148" - }, - { - "index": 149, - "description": "AGC Discharge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DAGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpuRte", - "name": "DAGC.DschRpuRte.AO149" - }, - { - "index": 150, - "description": "AGC Discharge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DAGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpdRte", - "name": "DAGC.DschRpdRte.AO150" - }, - { - "index": 151, - "description": "AGC Charge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DAGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpuRte", - "name": "DAGC.ChaRpuRte.AO151" - }, - { - "index": 152, - "description": "AGC Charge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DAGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpdRte", - "name": "DAGC.ChaRpdRte.AO152" - }, - { - "index": 153, - "description": "AGC Minimum Usable SOC", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DAGC", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMinPct", - "name": "DAGC.SocUseMinPct.AO153" - }, - { - "index": 154, - "description": "AGC Maximum Usable SOC", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DAGC", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMaxPct", - "name": "DAGC.SocUseMaxPct.AO154" - }, - { - "index": 155, - "description": "Active Power Smoothing Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWSM", - "minimum": 0, - "data_object": "ModPrio", - "name": "DWSM.ModPrio.AO155" - }, - { - "index": 156, - "description": "Active Power Smoothing Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWSM", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DWSM.WinTms.AO156" - }, - { - "index": 157, - "description": "Active Power Smoothing Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWSM", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DWSM.RmpTms.AO157" - }, - { - "index": 158, - "description": "Active Power Smoothing Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWSM", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DWSM.RvrtTms.AO158" - }, - { - "index": 159, - "description": "Active Power Smoothing Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DWSM", - "minimum": 0, - "data_object": "EcpRef", - "name": "DWSM.EcpRef.AO159" - }, - { - "index": 160, - "description": "Active Power Smoothing Gradient", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DWSM", - "units": "Watts per delta-watt", - "data_object": "WSmthGra", - "name": "DWSM.WSmthGra.AO160" - }, - { - "index": 161, - "description": "Active Power Smoothing Lower Limit. Difference in Watts from the moving average of the reference power (MMXN1.Watt) above which no smoothing shall be applied.", - "data_type": "AO", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DWSM", - "units": "Watts", - "data_object": "WSmthLoLim", - "name": "DWSM.WSmthLoLim.AO161" - }, - { - "index": 162, - "description": "Active Power Smoothing Upper Limit. Difference in Watts from the moving average of the reference power (MMXN.Watt) below which no smoothing shall be applied.", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DWSM", - "units": "Watts", - "minimum": 0, - "data_object": "WSmthHiLim", - "name": "DWSM.WSmthHiLim.AO162" - }, - { - "index": 163, - "description": "Active Power Smoothing Filter Time (Seconds). Time in seconds used to calculate the moving average of the reference load or generation (MMXN1.Watt) being smoothed.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWSM", - "units": "Seconds", - "minimum": 0, - "data_object": "FilTms", - "name": "DWSM.FilTms.AO163" - }, - { - "index": 164, - "description": "Active Power Smoothing Discharge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWSM", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpuRte", - "name": "DWSM.DschRpuRte.AO164" - }, - { - "index": 165, - "description": "Active Power Smoothing Discharge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWSM", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpdRte", - "name": "DWSM.DschRpdRte.AO165" - }, - { - "index": 166, - "description": "Active Power Smoothing Charge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWSM", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpuRte", - "name": "DWSM.ChaRpuRte.AO166" - }, - { - "index": 167, - "description": "Active Power Smoothing Charge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWSM", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpdRte", - "name": "DWSM.ChaRpdRte.AO167" - }, - { - "index": 168, - "description": "Volt-Watt Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWC", - "minimum": 0, - "data_object": "ModPrio", - "name": "DVWC.ModPrio.AO168" - }, - { - "index": 169, - "description": "Volt-Watt Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DVWC.WinTms.AO169" - }, - { - "index": 170, - "description": "Volt-Watt Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DVWC.RmpTms.AO170" - }, - { - "index": 171, - "description": "Volt-Watt Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DVWC.RvrtTms.AO171" - }, - { - "index": 172, - "description": "Volt-Watt Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DVWC", - "minimum": 0, - "data_object": "EcpRef", - "name": "DVWC.EcpRef.AO172" - }, - { - "index": 173, - "description": "Volt-Watt Curve Index", - "data_type": "AO", - "common_data_class": "CSG", - "ln_class": "DVWC", - "minimum": 0, - "data_object": "VWCrv", - "name": "DVWC.VWCrv.AO173" - }, - { - "index": 174, - "description": "Volt-Watt Filter Time (Seconds)", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "FilTms", - "name": "DVWC.FilTms.AO174" - }, - { - "index": 175, - "description": "Volt-Watt Ramp Up Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoop", - "name": "DVWC.OpnLoop.AO175" - }, - { - "index": 176, - "description": "Volt-Watt Ramp Down Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoop", - "name": "DVWC.OpnLoop.AO176" - }, - { - "index": 177, - "description": "Volt-Watt Discharging Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DVWC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpuRte", - "name": "DVWC.DschRpuRte.AO177" - }, - { - "index": 178, - "description": "Volt-Watt Discharging Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DVWC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "DschRpdRte", - "name": "DVWC.DschRpdRte.AO178" - }, - { - "index": 179, - "description": "Volt-Watt Charging Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DVWC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpuRte", - "name": "DVWC.ChaRpuRte.AO179" - }, - { - "index": 180, - "description": "Volt-Watt Charging Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DVWC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "ChaRpdRte", - "name": "DVWC.ChaRpdRte.AO180" - }, - { - "index": 181, - "description": "Frequency-Watt Curve Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "ModPrio", - "name": "DHFW.ModPrio.AO181" - }, - { - "index": 182, - "description": "Frequency-Watt Curve Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DHFW.WinTms.AO182" - }, - { - "index": 183, - "description": "Frequency-Watt Curve Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DHFW.RmpTms.AO183" - }, - { - "index": 184, - "description": "Frequency-Watt Curve Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DHFW.RvrtTms.AO184" - }, - { - "index": 185, - "description": "Frequency-Watt Curve Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "EcpRef", - "name": "DHFW.EcpRef.AO185" - }, - { - "index": 186, - "description": "Frequency-Watt Curve - Curve Index", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "HzWCrv", - "name": "DHFW.HzWCrv.AO186" - }, - { - "index": 187, - "description": "Frequency-Watt Curve - High Frequency Hysteresis Curve Index", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "HysCrv", - "name": "DHFW.HysCrv.AO187" - }, - { - "index": 188, - "description": "Frequency-Watt Curve - Low Frequency Hysteresis Curve Index", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DLFW", - "minimum": 0, - "data_object": "HysCrv", - "name": "DLFW.HysCrv.AO188" - }, - { - "index": 189, - "description": "Frequency-Watt Curve Start Delay", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "ActStrDlTmms", - "name": "DHFW.ActStrDlTmms.AO189" - }, - { - "index": 190, - "description": "Frequency-Watt Curve Stop Delay", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "ActStopDlTmms", - "name": "DHFW.ActStopDlTmms.AO190" - }, - { - "index": 191, - "description": "Frequency-Watt Curve Ramp Up Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DHFW.OpnLoopMax.AO191" - }, - { - "index": 192, - "description": "Frequency-Watt Curve Ramp Down Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DHFW.OpnLoopMax.AO192" - }, - { - "index": 193, - "description": "Frequency-Watt Curve Discharge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DHFW.RpuRte.AO193" - }, - { - "index": 194, - "description": "Frequency-Watt Curve Discharge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DHFW.RpdRte.AO194" - }, - { - "index": 195, - "description": "Frequency-Watt Curve Charge Ramp Up Rate", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuChaRte", - "name": "DHFW.RpuChaRte.AO195" - }, - { - "index": 196, - "description": "Frequency-Watt Curve Charge Ramp Down Rate", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdChaRte", - "name": "DHFW.RpdChaRte.AO196" - }, - { - "index": 197, - "description": "Frequency-Watt Curve Minimum Usable SOC", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DHFW", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMinPct", - "name": "DHFW.SocUseMinPct.AO197" - }, - { - "index": 198, - "description": "Frequency-Watt Curve Maximum Usable SOC", - "data_type": "AO", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DHFW", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMaxPct", - "name": "DHFW.SocUseMaxPct.AO198" - }, - { - "index": 199, - "description": "Constant VArs Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVAR", - "minimum": 0, - "data_object": "ModPrio", - "name": "DVAR.ModPrio.AO199" - }, - { - "index": 200, - "description": "Constant VArs Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DVAR.WinTms.AO200" - }, - { - "index": 201, - "description": "Constant VArs Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DVAR.RmpTms.AO201" - }, - { - "index": 202, - "description": "Constant VArs Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DVAR.RvrtTms.AO202" - }, - { - "index": 203, - "description": "Constant VArs Reactive Power Target. Percentage of maxmum reactive power.", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DVAR", - "units": "Percent", - "minimum": -1000, - "data_object": "VArTgtPct", - "name": "DVAR.VArTgtPct.AO203" - }, - { - "index": 204, - "description": "Constant VArs Ramp Up Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVAR.OpnLoopMax.AO204" - }, - { - "index": 205, - "description": "Constant VArs Ramp Down Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVAR.OpnLoopMax.AO205" - }, - { - "index": 206, - "description": "Fixed Power Factor Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DFPF", - "minimum": 0, - "data_object": "ModPrio", - "name": "DFPF.ModPrio.AO206" - }, - { - "index": 207, - "description": "Fixed Power Factor Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DFPF", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DFPF.WinTms.AO207" - }, - { - "index": 208, - "description": "Fixed Power Factor Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DFPF", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DFPF.RmpTms.AO208" - }, - { - "index": 209, - "description": "Fixed Power Factor Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DFPF", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DFPF.RvrtTms.AO209" - }, - { - "index": 210, - "description": "Fixed Power Factor Setpoint - Generation/Discharging", - "data_type": "AO", - "common_data_class": "APC", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DFPF", - "units": "None", - "minimum": 0, - "data_object": "PFGnTgt", - "name": "DFPF.PFGnTgt.AO210" - }, - { - "index": 211, - "description": "Fixed Power Factor Setpoint - Charging", - "data_type": "AO", - "common_data_class": "APC", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DFPF", - "units": "None", - "minimum": 0, - "data_object": "PFLodTgt", - "name": "DFPF.PFLodTgt.AO211" - }, - { - "index": 212, - "description": "Volt-VAr Control Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVVR", - "minimum": 0, - "data_object": "ModPrio", - "name": "DVVR.ModPrio.AO212" - }, - { - "index": 213, - "description": "Volt-VAr Control Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DVVR.WinTms.AO213" - }, - { - "index": 214, - "description": "Volt-VAr Control Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DVVR.RmpTms.AO214" - }, - { - "index": 215, - "description": "Volt-VAr Control Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DVVR.RvrtTms.AO215" - }, - { - "index": 216, - "description": "Volt-VAr Control Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DVVR", - "minimum": 0, - "data_object": "EcpRef", - "name": "DVVR.EcpRef.AO216" - }, - { - "index": 217, - "description": "Volt-VAr Curve Index", - "data_type": "AO", - "common_data_class": "CSG", - "ln_class": "DVVR", - "minimum": 0, - "data_object": "VVArCrv", - "name": "DVVR.VVArCrv.AO217" - }, - { - "index": 218, - "description": "Volt-VAr Ramp Up Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVVR.OpnLoopMax.AO218" - }, - { - "index": 219, - "description": "Volt-VAr Ramp Down Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVVR.OpnLoopMax.AO219" - }, - { - "index": 220, - "description": "Volt-VAr Autonomous Voltage Reference Adjustment Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "VRefTmms", - "name": "DVVR.VRefTmms.AO220" - }, - { - "index": 221, - "description": "Watt-VAr Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWVR", - "minimum": 0, - "data_object": "ModPrio", - "name": "DWVR.ModPrio.AO221" - }, - { - "index": 222, - "description": "Watt-VAr Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DWVR.WinTms.AO222" - }, - { - "index": 223, - "description": "Watt-VAr Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DWVR.RmpTms.AO223" - }, - { - "index": 224, - "description": "Watt-VAr Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DWVR.RvrtTms.AO224" - }, - { - "index": 225, - "description": "Watt-VAr Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DWVR", - "minimum": 0, - "data_object": "EcpRef", - "name": "DWVR.EcpRef.AO225" - }, - { - "index": 226, - "description": "Watt-VAr Curve Index", - "data_type": "AO", - "common_data_class": "CSG", - "ln_class": "DWVR", - "minimum": 0, - "data_object": "WVArCrv", - "name": "DWVR.WVArCrv.AO226" - }, - { - "index": 227, - "description": "Watt-VAr Ramp Up Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DWVR.OpnLoopMax.AO227" - }, - { - "index": 228, - "description": "Watt-VAr Ramp Down Time Constant", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DWVR.OpnLoopMax.AO228" - }, - { - "index": 229, - "description": "Power Factor Correction Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPFC", - "minimum": 0, - "data_object": "ModPrio", - "name": "DPFC.ModPrio.AO229" - }, - { - "index": 230, - "description": "Power Factor Correction Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPFC", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DPFC.WinTms.AO230" - }, - { - "index": 231, - "description": "Power Factor Correction Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DPFC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpRte", - "name": "DPFC.RmpRte.AO231" - }, - { - "index": 232, - "description": "Power Factor Correction Reversion Timeout Period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPFC", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DPFC.RvrtTms.AO232" - }, - { - "index": 233, - "description": "Power Factor Correction Signal Meter ID", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DPFC", - "minimum": 0, - "data_object": "EcpRef", - "name": "DPFC.EcpRef.AO233" - }, - { - "index": 234, - "description": "Power Factor Correction Average PF Target", - "data_type": "AO", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DPFC", - "units": "None", - "minimum": -100, - "data_object": "PFTrg", - "name": "DPFC.PFTrg.AO234" - }, - { - "index": 235, - "description": "Power Factor Correction Lower PF Limit", - "data_type": "AO", - "common_data_class": "Int", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DPFC", - "units": "None", - "minimum": -100, - "data_object": "PFCorRef.rangeC", - "name": "DPFC.PFCorRef.rangeC.AO235" - }, - { - "index": 236, - "description": "Power Factor Correction Upper PF Limit", - "data_type": "AO", - "common_data_class": "Int", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DPFC", - "units": "None", - "minimum": -100, - "data_object": "PFCorRef.rangeC", - "name": "DPFC.PFCorRef.rangeC.AO236" - }, - { - "index": 237, - "description": "Pricing Mode Priority", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPRG", - "units": "None", - "minimum": 0, - "data_object": "ModPrio", - "name": "DPRG.ModPrio.AO237" - }, - { - "index": 238, - "description": "Pricing Mode Enabling Time Window", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DPRG.WinTms.AO238" - }, - { - "index": 239, - "description": "Pricing Mode Enabling Ramp Time", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DPRG.RmpTms.AO239" - }, - { - "index": 240, - "description": "Pricing Mode Reversion Timeout period", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DPRG.RvrtTms.AO240" - }, - { - "index": 241, - "description": "Pricing Mode Setpoint. Hundredths of local currency per Kilowatt-Hr.", - "data_type": "AO", - "common_data_class": "MV", - "scaling_multiplier": 0.01, - "ln_class": "DPRG", - "units": "100ths of local currency", - "data_object": "PrcRef", - "name": "DPRG.PrcRef.AO241" - }, - { - "index": 242, - "description": "Pricing Mode Ramp Up Time Constant", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DPRG.OpnLoopMax.AO242" - }, - { - "index": 243, - "description": "Pricing Mode Ramp Down Time Constant", - "data_type": "AO", - "common_data_class": "ASG", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DPRG.OpnLoopMax.AO243" - }, - { - "index": 244, - "description": "Curve Edit Selector. Writing to this point selects which of the curves can currently be viewed and changed.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "DGSM", - "minimum": 1, - "data_object": "InCrv", - "name": "DGSMn.InCrv.AO244", - "type": "selector_block", - "selector_block_start": 244, - "selector_block_end": 448 - }, - { - "index": 245, - "description": "Curve Mode Type", - "data_type": "AO", - "common_data_class": "ENG", - "maximum": 20, - "ln_class": "DGSM", - "units": "None (list)", - "minimum": 0, - "data_object": "ModTyp", - "allowed_values": { - "0": "Curve disabled", - "1": "Not applicable / Unknown", - "2": "Volt-Var modes VV11-VV12", - "3": "Frequency-Watt mode FW22", - "4": "Watt-VAr mode WP42", - "5": "Voltage-Watt modes VW51-VW52", - "6": "Remain Connected", - "7": "Temperature mode", - "8": "Pricing signal mode", - "9": "HVRT Must Trip", - "10": "HVRT Momentary Cessation", - "11": "LVRT Must Trip", - "12": "LVRT Momentary Cessation", - "13": "HFRT Must Trip", - "14": "HFRT Momentary Cessation", - "15": "LFRT Must Trip", - "16": "LFRT Mandatory Operation" - }, - "type": "enumerated", - "name": "DGSMn.ModTyp.AO245" - }, - { - "index": 246, - "description": "Curve Number of Points", - "data_type": "AO", - "common_data_class": "CSG", - "maximum": 100, - "ln_class": "FMAR", - "minimum": 0, - "data_object": "PairArr.NumPts", - "name": "FMARn.PairArr.NumPts.AO246" - }, - { - "index": 247, - "description": "Independent (X-Value) Units for Curve", - "data_type": "AO", - "common_data_class": "ENG", - "maximum": 255, - "ln_class": "FMAR", - "units": "None (list)", - "minimum": 0, - "data_object": "IndpUnits", - "allowed_values": { - "0": "Curve disabled", - "1": "Not applicable / Unknown", - "4": "Time", - "23": "Celsius Temperature", - "29": "Voltage", - "33": "Frequency", - "38": "Watts", - "100": "Price in hundredths of local currency", - "129": "Percent Voltage", - "133": "Percent Frequency", - "138": "Percent Watts", - "233": "Frequency Deviation" - }, - "type": "enumerated", - "name": "FMARn.IndpUnits.AO247" - }, - { - "index": 248, - "description": "Dependent (Y-Value) Units for Curve", - "data_type": "AO", - "common_data_class": "ENG", - "maximum": 255, - "ln_class": "FMAR", - "units": "None (list)", - "minimum": 0, - "data_object": "DepRef", - "allowed_values": { - "0": "Curve disabled", - "1": "Not applicable / unknown", - "2": "VArs as percent of max VArs (VARMax)", - "3": "VArs as percent of max available VArs (VArAval)", - "4": "Vars as percent of max Watts (Wmax) not used", - "5": "Watts as percent of max Watts (Wmax)", - "6": "Watts as percent of frozen active power (DeptSnptRef)", - "7": "Power Factor in EEI notation", - "8": "Volts as a percent of the nominal voltage (VRef)", - "9": "Frequency as a percent of the nominal grid frequency (ECPNomHz)" - }, - "type": "enumerated", - "name": "FMARn.DepRef.AO248" - }, - { - "index": 249, - "description": "Curve X-Value and Y-Value pairs for curve points 1 - 100", - "data_type": "AO", - "common_data_class": "CSG", - "ln_class": "FMAR", - "units": "Varies", - "data_object": "PairArr.CrvPts", - "name": "FMARn.PairArr.CrvPts.AO249", - "type": "array", - "array_times_repeated": 100, - "array_points": [ - { - "name": "FMARn.PairArr.CrvPts.AO249.xVal" - }, - { - "name": "FMARn.PairArr.CrvPts.AO249.yVal" - } - ] - }, - { - "index": 449, - "description": "System Meter Active Power - High Threshold", - "data_type": "AO", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "minimum": 0, - "data_object": "TotW.rangeC.hLim", - "name": "MMXU.TotW.rangeC.hLim.AO449" - }, - { - "index": 450, - "description": "System Meter Active Power - Low Threshold", - "data_type": "AO", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "minimum": 0, - "data_object": "TotW.rangeC.lLim", - "name": "MMXU.TotW.rangeC.lLim.AO450" - }, - { - "index": 451, - "description": "System Meter Reactive Power - High Threshold", - "data_type": "AO", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "VARs", - "minimum": 0, - "data_object": "TotVAr.rangeC.hLim", - "name": "MMXU.TotVAr.rangeC.hLim.AO451" - }, - { - "index": 452, - "description": "System Meter at Reactive Power - Low Threshold", - "data_type": "AO", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "VARs", - "minimum": 0, - "data_object": "TotVAr.rangeC.lLim", - "name": "MMXU.TotVAr.rangeC.lLim.AO452" - }, - { - "index": 453, - "description": "System Meter at Power Factor - High Threshold", - "data_type": "AO", - "common_data_class": "MV", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "MMXU", - "units": "None", - "minimum": -100, - "data_object": "TotPF.rangeC.hLim", - "name": "MMXU.TotPF.rangeC.hLim.AO453" - }, - { - "index": 454, - "description": "System Meter at Power Factor - Low Threshold", - "data_type": "AO", - "common_data_class": "MV", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "MMXU", - "units": "None", - "minimum": -100, - "data_object": "TotPF.rangeC.lLim", - "name": "MMXU.TotPF.rangeC.lLim.AO454" - }, - { - "index": 455, - "description": "System Meter Phase A Volts - High Threshold", - "data_type": "AO", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsA.rangeC.hLim", - "name": "MMXU.PhV.phsA.rangeC.hLim.AO455" - }, - { - "index": 456, - "description": "System Meter Phase A Volts - Low Threshold", - "data_type": "AO", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsA.rangeC.lLim", - "name": "MMXU.PhV.phsA.rangeC.lLim.AO456" - }, - { - "index": 457, - "description": "System Meter Phase B Volts High Threshold", - "data_type": "AO", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsB.rangeC.hLim", - "name": "MMXU.PhV.phsB.rangeC.hLim.AO457" - }, - { - "index": 458, - "description": "System Meter Phase B Volts - Low Threshold", - "data_type": "AO", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsB.rangeC.lLim", - "name": "MMXU.PhV.phsB.rangeC.lLim.AO458" - }, - { - "index": 459, - "description": "System Meter Phase C Volts - High Threshold", - "data_type": "AO", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsC.rangeC.hLim", - "name": "MMXU.PhV.phsC.rangeC.hLim.AO459" - }, - { - "index": 460, - "description": "System Meter Phase C Volts - Low Threshold", - "data_type": "AO", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsC.rangeC.lLim", - "name": "MMXU.PhV.phsC.rangeC.lLim.AO460" - }, - { - "index": 461, - "description": "Schedule to Edit Selector. Selects which of the schedules can be currently viewed and changed.", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "FSCC", - "minimum": 0, - "data_object": "Schd", - "name": "FSCC.Schd.AO461", - "type": "selector_block", - "selector_block_start": 461, - "selector_block_end": 669 - - }, - { - "index": 462, - "description": "Selected Schedule Identity", - "data_type": "AO", - "common_data_class": "ORG", - "ln_class": "FSCC", - "minimum": 0, - "data_object": "Schd", - "name": "FSCC.Schd.AO462" - }, - { - "index": 463, - "description": "Selected Schedule Priority. Priority of the schedule relative to other running schedules. Lower values have higher priority over higher values.", - "data_type": "AO", - "common_data_class": "ING", - "ln_class": "FSCH", - "minimum": 0, - "data_object": "SchdPrio", - "name": "FSCH1.SchdPrio.AO463" - }, - { - "index": 464, - "description": "Selected Schedule Type", - "data_type": "AO", - "common_data_class": "SCR", - "maximum": 30, - "ln_class": "FSCH", - "units": "None (list)", - "minimum": 0, - "data_object": "SchdVal.valEq", - "allowed_values": { - "1": "Low/High Voltage Ride-Through - Hi Must Trip", - "2": "Low/High Voltage Ride-Through - Low Must Trip", - "3": "Low/High Voltage Ride-Through - Hi Momentary", - "4": "Low/High Voltage Ride-Through - Lo Momentary", - "5": "Low/High Frequency Ride-Through - Hi Must Trip", - "6": "Low/High Frequency Ride-Through - Lo Must Trip", - "7": "Low/High Frequency Ride-Through - Hi Momentary", - "8": "Low/High Frequency Ride-Through - Low Momentary", - "9": "Dynamic Reactive Current Support - On/Off", - "10": "Dynamic Volt-Watt - On/Off", - "11": "Frequency-Watt - On/Off", - "12": "Active Power Limit - Charging", - "13": "Active Power Limit - Generating", - "14": "Charge/Discharge - Percent of Maximum", - "15": "Coordinated Charge/Discharge - SOC Target", - "16": "Active Power Response #1 - On/Off", - "17": "Active Power Response #2 - On/Off", - "18": "Active Power Response #3 - On/Off", - "19": "AGC - Watts", - "20": "Active Power Smoothing - On/Off", - "21": "Volt-Watt - Curve Index", - "22": "Frequency-Watt Curve - Curve Index", - "23": "Frequency-Watt Curve - High Hysteresis", - "24": "Frequency-Watt Curve - Low Hysteresis", - "25": "Constant VArs - Percent of Maximum", - "26": "Fixed Power Factor - Power Factor", - "27": "Volt-VAr - Curve Index", - "28": "Watt-VAr - Curve Index", - "29": "Power Factor Correction - On/Off", - "30": "Reserved - For pricing mode" - }, - "type": "enumerated", - "name": "FSCH.SchdVal.valEq.AO464" - }, - { - "index": 465, - "description": "Selected Schedule Start Date. Number of days since January 1, 1970, UTC.", - "data_type": "AO", - "common_data_class": "TSG", - "ln_class": "FSCH", - "units": "Days", - "minimum": 0, - "data_object": "StrTm", - "name": "FSCH.StrTm.AO465" - }, - { - "index": 466, - "description": "Selected Schedule Start Time. Milliseconds since the start of Schedule Start Date.", - "data_type": "AO", - "common_data_class": "TSG", - "maximum": 86400000, - "ln_class": "FSCH", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "StrTm", - "name": "FSCH.StrTm.AO466" - }, - { - "index": 467, - "description": "Selected Schedule Repeat Interval. Interval between actions after the initial occurrence. A zero value means the schedule is not repeated.", - "data_type": "AO", - "common_data_class": "TCS", - "ln_class": "FSCH", - "minimum": 0, - "data_object": "NxtStrTm", - "name": "FSCH.NxtStrTm.AO467" - }, - { - "index": 468, - "description": "Selected Schedule Repeat Interval Units", - "data_type": "AO", - "common_data_class": "SPG", - "maximum": 8, - "ln_class": "FSCH", - "units": "None (list)", - "minimum": 0, - "data_object": "SchdReuse", - "allowed_values": { - "0": "No Repeat", - "1": "Seconds", - "2": "Minutes", - "3": "Hours", - "4": "Days", - "5": "Weeks", - "6": "Months", - "7": "Months on Same Day of Week", - "8": "Months on Same Day of Week from End" - }, - "type": "enumerated", - "name": "FSCH.SchdReuse.AO468" - }, - { - "index": 469, - "description": "Selected Schedule Number of Points", - "data_type": "AO", - "common_data_class": "ING", - "maximum": 100, - "ln_class": "FSCH", - "minimum": 0, - "data_object": "NumEntr", - "name": "FSCH.NumEntr.AO469" - }, - { - "index": 470, - "description": "Select schedule time offset and value pairs for points 1 - 100", - "data_type": "AO", - "minimum": 0, - "name": "FSCHn.SchdEntr.AO470", - "type": "array", - "array_times_repeated": 100, - "array_points": [ - { - "name": "FSCHn.SchdEntr.AO470.time", - "description": "Number of seconds from the start of the schedule when this point becomes active", - "units": "Seconds" - }, - { - "name": "FSCHn.SchdEntr.AO470.val", - "ln_class": "FSCH", - "data_object": "SchdEntr" - } - ] - }, - { - "index": 0, - "description": "DER Profile Version Number. Always the number 1.00 for this specification.", - "data_type": "AI", - "scaling_multiplier": 0.01, - "maximum": 100, - "minimum": 100, - "event_class": 3, - "name": "AI0" - }, - { - "index": 1, - "description": "DER Profile Implementation Level. 1, 2 or 3 to indicate support for Level 1, Level 2 or Level 3 respectively.", - "data_type": "AI", - "maximum": 3, - "minimum": 1, - "event_class": 3, - "name": "AI1" - }, - { - "index": 2, - "description": "Nameplate Minimum Voltage Rating", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DGEN", - "units": "Volts", - "minimum": 0, - "data_object": "VMinRtg", - "event_class": 3, - "name": "DGEN.VMinRtg.AI2" - }, - { - "index": 3, - "description": "Nameplate Maximum Voltage Rating", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DGEN", - "units": "Volts", - "minimum": 0, - "data_object": "VMaxRtg", - "event_class": 3, - "name": "DGEN.VMaxRtg.AI3" - }, - { - "index": 4, - "description": "Nameplate Active Generation Power Rating at Unity Power Factor", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "Watts", - "minimum": 0, - "data_object": "WMaxRtg", - "event_class": 3, - "name": "DGEN.WMaxRtg.AI4" - }, - { - "index": 5, - "description": "Nameplate Active Charging Power Rating at Unity Power Factor", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DSTO", - "units": "Watts", - "data_object": "ChaWMaxRtg", - "event_class": 3, - "name": "DSTO.ChaWMaxRtg.AI5" - }, - { - "index": 6, - "description": "Nameplate Active Generation Power Rating at Specified Over-Excited Power Factor", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "Watts", - "minimum": 0, - "data_object": "WOvPFRtg", - "event_class": 3, - "name": "DGEN.WOvPFRtg.AI6" - }, - { - "index": 7, - "description": "Nameplate Active Charging Power Rating at Specified Over-Excited Power Factor", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DSTO", - "units": "Watts", - "data_object": "ChaWOvPFRtg", - "event_class": 3, - "name": "DSTO.ChaWOvPFRtg.AI7" - }, - { - "index": 8, - "description": "Specified Over-Excited Power Factor", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DGEN", - "units": "None", - "minimum": -100, - "data_object": "OvPFRtg", - "event_class": 3, - "name": "DGEN.OvPFRtg.AI8" - }, - { - "index": 9, - "description": "Nameplate Active Generation Power Rating at Specified Under-Excited Power Factor", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "Watts", - "minimum": 0, - "data_object": "WUnPFRtg", - "event_class": 3, - "name": "DGEN.WUnPFRtg.AI9" - }, - { - "index": 10, - "description": "Nameplate Active Charging Power Rating at Specified Under-Excited Power Factor", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DSTO", - "units": "Watts", - "data_object": "ChaWUnPFRtg", - "event_class": 3, - "name": "DSTO.ChaWUnPFRtg.AI10" - }, - { - "index": 11, - "description": "Specified Under-Excited Power Factor", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DGEN", - "units": "None", - "minimum": -100, - "data_object": "UnPFRtg", - "event_class": 3, - "name": "DGEN.UnPFRtg.AI11" - }, - { - "index": 12, - "description": "Nameplate Reactive Supply (Injection) Power Rating", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "VARs", - "minimum": 0, - "data_object": "IvarMaxRtg", - "event_class": 3, - "name": "DGEN.IvarMaxRtg.AI12" - }, - { - "index": 13, - "description": "Nameplate Reactive Absorption Power Rating", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DGEN", - "units": "VARs", - "data_object": "AvarMaxRtg", - "event_class": 3, - "name": "DGEN.AvarMaxRtg.AI13" - }, - { - "index": 14, - "description": "Nameplate Apparent Generation Power Rating", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "VAs", - "minimum": 0, - "data_object": "VAMaxRtg", - "event_class": 3, - "name": "DGEN.VAMaxRtg.AI14" - }, - { - "index": 15, - "description": "Nameplate Apparent Charging Power Rating", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DSTO", - "units": "VAs", - "data_object": "ChaVAMaxRtg", - "event_class": 3, - "name": "DSTO.ChaVAMaxRtg.AI15" - }, - { - "index": 16, - "description": "Nameplate Storage Actual Energy Capacity. Nameplate (original) actual total energy capacity of the storage system expressed in Storage Capacity Units.", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DSTO", - "units": "Amp-hrs or Watt-hrs", - "minimum": 0, - "data_object": "WhRtg", - "event_class": 3, - "name": "DSTO.WhRtg.AI16" - }, - { - "index": 17, - "description": "Storage Effective Actual Energy Capacity. Present actual total energy capacity of the storage system expressed in Storage Capacity Units.", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DSTO", - "units": "Amp-hrs or Watt-hrs", - "minimum": 0, - "data_object": "EffWh", - "event_class": 3, - "name": "DSTO.EffWh.AI17" - }, - { - "index": 18, - "description": "Storage Usable Energy Capacity. Usable energy capacity of the storage system expressed in Storage Capacity Units.", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DSTO", - "units": "Amp-hrs or Watt-hrs", - "minimum": 0, - "data_object": "UseWh", - "event_class": 3, - "name": "DSTO.UseWh.AI18" - }, - { - "index": 19, - "description": "Nameplate AC Current Maximum Generation Rating", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DGEN", - "units": "Amps", - "minimum": 0, - "data_object": "AMaxRtg", - "event_class": 3, - "name": "DGEN.AMaxRtg.AI19" - }, - { - "index": 20, - "description": "Nameplate AC Current Maximum Charging Rating", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 0, - "ln_class": "DSTO", - "units": "Amps", - "data_object": "ChaAMaxRtg", - "event_class": 3, - "name": "DSTO.ChaAMaxRtg.AI20" - }, - { - "index": 21, - "description": "Remaining Reactive Susceptance", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "Siemens", - "data_object": "SuscRtg", - "event_class": 3, - "name": "DGEN.SuscRtg.AI21" - }, - { - "index": 22, - "description": "IEEE 1547 Normal Operating Performance Category.", - "data_type": "AI", - "maximum": 2, - "minimum": 0, - "units": "None (list)", - "event_class": 3, - "allowed_values": { - "0": "unknown", - "1": "Category A", - "2": "Category B" - }, - "type": "enumerated", - "name": "AI22" - }, - { - "index": 23, - "description": "IEEE 1547 Abnormal Operating Performance Category.", - "data_type": "AI", - "maximum": 3, - "minimum": 0, - "units": "None (list)", - "event_class": 3, - "allowed_values": { - "0": "unknown", - "1": "Category I", - "2": "Category II", - "3": "Category III" - }, - "type": "enumerated", - "name": "AI23" - }, - { - "index": 24, - "description": "Number of System Schedules", - "data_type": "AI", - "minimum": 0, - "units": "None", - "event_class": 3, - "name": "AI24" - }, - { - "index": 25, - "description": "Number of Meters", - "data_type": "AI", - "minimum": 0, - "units": "None", - "event_class": 3, - "name": "AI25" - }, - { - "index": 26, - "description": "Number of Inverters", - "data_type": "AI", - "minimum": 0, - "units": "None", - "event_class": 3, - "name": "AI26" - }, - { - "index": 27, - "description": "Number of Batteries", - "data_type": "AI", - "minimum": 0, - "units": "None", - "event_class": 3, - "name": "AI27" - }, - { - "index": 28, - "description": "Number of DER units connected to controller", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DSTO", - "units": "None", - "minimum": 0, - "data_object": "InclDER", - "event_class": 3, - "name": "DSTO.InclDER.AI28" - }, - { - "index": 29, - "description": "Reference Voltage", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DECP", - "units": "Volts", - "minimum": 0, - "data_object": "VRef", - "name": "DECP.VRef.AI29" - }, - { - "index": 30, - "description": "Reference Voltage Offset", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DECP", - "units": "Volts", - "data_object": "VRefOfs", - "name": "DECP.VRefOfs.AI30" - }, - { - "index": 31, - "description": "Nominal Grid Frequency", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DECP", - "units": "Hz", - "minimum": 0, - "data_object": "EcpNomHz", - "name": "DECP.EcpNomHz.AI31" - }, - { - "index": 32, - "description": "Maximum Active Generation Power", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "Watts", - "minimum": 0, - "data_object": "WMax", - "name": "DGEN.WMax.AI32" - }, - { - "index": 33, - "description": "Maximum Active Charging Power", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DSTO", - "units": "Watts", - "data_object": "ChaWMax", - "name": "DSTO.ChaWMax.AI33" - }, - { - "index": 34, - "description": "Maximum Reactive Injection Power", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "VARs", - "minimum": 0, - "data_object": "IvarMax", - "name": "DGEN.IvarMax.AI34" - }, - { - "index": 35, - "description": "Maximum Reactive Absorption Power", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DGEN", - "units": "VARs", - "data_object": "AvarMax", - "name": "DGEN.AvarMax.AI35" - }, - { - "index": 36, - "description": "Maximum Apparent Generation Power", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGEN", - "units": "VA", - "minimum": 0, - "data_object": "VAMax", - "name": "DGEN.VAMax.AI36" - }, - { - "index": 37, - "description": "Maximum Apparent Charging Power", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DSTO", - "units": "VA", - "data_object": "ChaVAMax", - "name": "DSTO.ChaVAMax.AI37" - }, - { - "index": 38, - "description": "Minimum Voltage", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DECP", - "units": "Volts", - "minimum": 0, - "data_object": "VMin", - "name": "DECP.VMin.AI38" - }, - { - "index": 39, - "description": "Maximum Voltage", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "ln_class": "DECP", - "units": "Volts", - "minimum": 0, - "data_object": "VMax", - "name": "DECP.VMax.AI39" - }, - { - "index": 40, - "description": "Open Loop Response Time Percentage. Percent of target to reach within the open loop response time. Default is 90%.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DGEN", - "units": "Percent", - "minimum": 0, - "data_object": "OpnLoopPct", - "name": "DGEN.OpnLoopPct.AI40" - }, - { - "index": 41, - "description": "Power Factor Sign Convention.", - "data_type": "AI", - "common_data_class": "ENG", - "maximum": 2, - "ln_class": "MMXU", - "units": "None", - "minimum": 1, - "data_object": "PFSign", - "allowed_values": { - "1": "IEC active power", - "2": "IEEE lead/lag" - }, - "type": "enumerated", - "name": "MMXU.PFSign.AI41" - }, - { - "index": 42, - "description": "Reference for Reactive Power Setpoints. Selects which setpoint is active. Default is <3>.", - "data_type": "AI", - "common_data_class": "ENG", - "maximum": 3, - "ln_class": "DGEN", - "units": "None (list)", - "minimum": 0, - "data_object": "VArSetRef", - "allowed_values": { - "0": "Not applicable / Unknown", - "1": "Percent of Maximum Active Power (WMax)", - "2": "Percent of Maximum Reactive Power (VArMax)", - "3": "Percent of Available Reactive Power (VArAvl)" - }, - "type": "enumerated", - "name": "DGEN.VArSetRef.AI42" - }, - { - "index": 43, - "description": "System Available Active Generation Power", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "minimum": 0, - "data_object": "TotW", - "name": "MMXU.TotW.AI43" - }, - { - "index": 44, - "description": "System Available Active Charging Power", - "data_type": "AI", - "common_data_class": "MV", - "maximum": 0, - "ln_class": "MMXU", - "units": "Watts", - "data_object": "TotChaW", - "name": "MMXU.TotChaW.AI44" - }, - { - "index": 45, - "description": "System Available Reactive Injection Power", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DGEN", - "units": "VARs", - "minimum": 0, - "data_object": "AvarAvl", - "name": "DGEN.AvarAvl.AI45" - }, - { - "index": 46, - "description": "System Available Reactive Absorption Power", - "data_type": "AI", - "common_data_class": "MV", - "maximum": 0, - "ln_class": "DGEN", - "units": "VARs", - "data_object": "IvarAvl", - "name": "DGEN.IvarAvl.AI46" - }, - { - "index": 47, - "description": "System Available Actual State of Charge - Present energy in the DER as a percentage of Storage Effective Actual Capacity", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DSTO", - "units": "Percent", - "minimum": 0, - "data_object": "SocPct", - "name": "DSTO.SocPct.AI47" - }, - { - "index": 48, - "description": "System Usable State of Charge - Present usable energy in the DER as a percentage of Nameplate Storage Usable Capacity", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DSTO", - "units": "Percent", - "minimum": 0, - "data_object": "UseSocPct", - "name": "DSTO.UseSocPct.AI48" - }, - { - "index": 49, - "description": "System Start-up Status", - "data_type": "AI", - "common_data_class": "ENS", - "maximum": 99, - "ln_class": "DGEN", - "units": "None (list)", - "minimum": -1, - "data_object": "DEROpSt", - "name": "DGEN.DEROpSt.AI49" - }, - { - "index": 50, - "description": "DER Start (Return to Service) Voltage High Limit. Percent of Reference Voltage.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 20000, - "ln_class": "DCTE", - "units": "Percent", - "minimum": 0, - "data_object": "VHiLim", - "name": "DCTE.VHiLim.AI50" - }, - { - "index": 51, - "description": "DER Start (Return to Service) Voltage Low Limit. Percent of Reference Voltage.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 10000, - "ln_class": "DCTE", - "units": "Percent", - "minimum": 0, - "data_object": "VLoLim", - "name": "DCTE.VLoLim.AI51" - }, - { - "index": 52, - "description": "DER Start (Return to Service) Frequency High Limit", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DCTE", - "units": "Hz", - "minimum": 0, - "data_object": "HzHiLim", - "name": "DCTE.HzHiLim.AI52" - }, - { - "index": 53, - "description": "DER Start (Return to Service) Frequency Low Limit", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DCTE", - "units": "Hz", - "minimum": 0, - "data_object": "HzLoLim", - "name": "DCTE.HzLoLim.AI53" - }, - { - "index": 54, - "description": "DER Start (Return to Service) Delay", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RtnDlTmms", - "name": "DCTE.RtnDlTmms.AI54" - }, - { - "index": 55, - "description": "DER Start (Return to Service) Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DCTE.WinTms.AI55" - }, - { - "index": 56, - "description": "DER Start (Return to Service) Ramp Up Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RtnRmpTmms", - "name": "DCTE.RtnRmpTmms.AI56" - }, - { - "index": 57, - "description": "DER Stop (Cease to Energize) Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DCTE.WinTms.AI57" - }, - { - "index": 58, - "description": "DER Stop (Cease to Energize) Ramp Down Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DCTE.RmpTms.AI58" - }, - { - "index": 59, - "description": "DER Stop (Cease to Energize) Reversion Timeout Period. Time to revert from the stopped state and return to service.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DCTE.RvrtTms.AI59" - }, - { - "index": 60, - "description": "Connect/Disconnect Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DCTE.WinTms.AI60" - }, - { - "index": 61, - "description": "Connect/Disconnect Reversion Timeout Period. Timeout (reversion time is for the Disconnect only).", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DCTE", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DCTE.RvrtTms.AI61" - }, - { - "index": 62, - "description": "Maximum Generation Ramp Up Rate. The maximum generation ramp up rate expressed as a percentage of the Maximum Generation Rate (WMax) per second.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DCTE", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRteMax", - "name": "DCTE.RpuRteMax.AI62" - }, - { - "index": 63, - "description": "Maximum Generation Ramp Down Rate. The maximum generation ramp down rate expressed as a percentage of the Maximum Generation Rate (WMax) per second.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DCTE", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRteMax", - "name": "DCTE.RpdRteMax.AI63" - }, - { - "index": 64, - "description": "Maximum Charging Ramp Up Rate. The maximum charging ramp up rate expressed as a percentage of the Maximum Charging Rate (WChaMax) per second.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DCTE", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuChaRteMax", - "name": "DCTE.RpuChaRteMax.AI64" - }, - { - "index": 65, - "description": "Maximum Charging Ramp Down Rate. The maximum charging ramp down rate expressed as a percentage of the Maximum Charnging Rate (WChaMax) per second.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DCTE", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdChaRteMax", - "name": "DCTE.RpdChaRteMax.AI65" - }, - { - "index": 66, - "description": "Requested Settings Group.", - "data_type": "AI", - "common_data_class": "ENG", - "maximum": 255, - "ln_class": "DECP", - "units": "None (list)", - "minimum": 0, - "data_object": "EcpIsldSt", - "allowed_values": { - "0": "Not Used", - "1": "Unspecified / Autonomously Determined (see BO Enable Sensed Grid Config Detection)", - "2": "Factory Configuration", - "3": "Default Configuration / Comms Lost", - "4": "Normal Grid-Connected Configuration", - "5": "Islanded Condition 1 (small, local island)", - "6": "Islanded Condition 2 (larger, area island)", - "7": "Islanded Condition 3 (largest, regional island)", - "8": "1st Alternate Grid-Connected Configuration", - "9": "2nd Alternate Grid-Connected Configuration", - "10": "3rd Alternate Grid-Connected Configuration" - }, - "type": "enumerated", - "name": "DECP.EcpIsldSt.AI66" - }, - { - "index": 67, - "description": "Settings Group Being Edited.", - "data_type": "AI", - "common_data_class": "ENG", - "maximum": 255, - "ln_class": "DECP", - "units": "None (list)", - "minimum": 0, - "data_object": "EcpIsldSt", - "allowed_values": { - "0": "Not Used", - "1": "Unspecified / Autonomously Determined (see BO Enable Sensed Grid Config Detection)", - "2": "Factory Configuration", - "3": "Default Configuration / Comms Lost", - "4": "Normal Grid-Connected Configuration", - "5": "Islanded Condition 1 (small, local island)", - "6": "Islanded Condition 2 (larger, area island)", - "7": "Islanded Condition 3 (largest, regional island)", - "8": "1st Alternate Grid-Connected Configuration", - "9": "2nd Alternate Grid-Connected Configuration", - "10": "3rd Alternate Grid-Connected Configuration" - }, - "type": "enumerated", - "name": "DECP.EcpIsldSt.AI67" - }, - { - "index": 68, - "description": "Active Settings Group. Note this may differ from the Requested Settings Group or Settings Group Being Edited analog outputs depending on whether communications has been lost and how the Enable Sensed Grid Config Detection binary output is set.", - "data_type": "AI", - "common_data_class": "ENS", - "maximum": 255, - "ln_class": "DECP", - "units": "None (list)", - "minimum": 0, - "data_object": "EcpIsldSt", - "allowed_values": { - "0": "Not Used", - "1": "Unspecified / Autonomously Determined (see BO42)", - "2": "Factory Configuration", - "3": "Default Configuration / Comms Lost", - "4": "Normal Grid-Connected Configuration", - "5": "Islanded Condition 1 (small, local island)", - "6": "Islanded Condition 2 (larger, area island)", - "7": "Islanded Condition 3 (largest, regional island)", - "8": "1st Alternate Grid-Connected Configuration", - "9": "2nd Alternate Grid-Connected Configuration", - "10": "3rd Alternate Grid-Connected Configuration" - }, - "type": "enumerated", - "name": "DECP.EcpIsldSt.AI68" - }, - { - "index": 69, - "description": "Freeze Counter Interval. interval between freeze counter operations after the initial occurrence. A zero value means the free counter operation is not repeated.", - "data_type": "AI", - "minimum": 0, - "name": "AI69" - }, - { - "index": 70, - "description": "Freeze Counter Interval Units. Units of the interval between freeze counter operations.", - "data_type": "AI", - "maximum": 9, - "minimum": 0, - "units": "None (list)", - "allowed_values": { - "0": "The outstation does not repeat the action,regardless of the Interval count.", - "1": "Milliseconds - In this case the interval is always counted relative to the Start Time and is constant regardless of the clock time set at the Outstation.", - "2": "Seconds - At the same millisecond within the second that is specified in the Start Time.", - "3": "Minutes - At the same second and millisecond within the minute that is specified in the Start Time.", - "4": "Hours - At the same minute,second and B7millisecond within the hour that is specified in the Start Time.", - "5": "Days - At the same time of day that is specified in the Start Time.", - "6": "Weeks - On the same day of the week at the same time of day that is specified in the Start Time", - "7": "Months - On the same day of each month at the same time of day that is specified in the Start Time. If the Start Time falls on the 29th or greater day of the month,the outstation shall not perform the action in months that do not have such a day", - "8": "Months on Same Day of Week from Start of Month - At the same timeof day on the same day of the week after the beginning of the month as the day specified in the Start Time. For instance,if the Start Time specifies the second Tuesday of February and the Interval Count is 2,the next action shall occur on the second Tuesday of April. In the same example,if the Interval Count is set to 12,this is the same as specifying,Every year on the second Tuesday in February. If the specified day does not occur in a given month when an action was scheduled to occur,the outstation shall not perform the action that month but shall perform it at the next valid scheduled time.", - "9": "Months on Same Day of Week from End of Month - The outstation shall interpret this setting as in <8>,but the day of the week shall be measured from the end of the month,e.g.,the second-last Tuesday in February." - }, - "type": "enumerated", - "name": "AI70" - }, - { - "index": 71, - "description": "Low/High Voltage Ride-Through Signal Meter ID. Referenced ECP. This is the meter from which current is being read to evaluate and provide support.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DHVT", - "minimum": 0, - "data_object": "EcpRef", - "name": "DHVT.EcpRef.AI71" - }, - { - "index": 72, - "description": "Low/High Voltage Ride-Through Voltage Reference Input. Active voltage measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "MMXN", - "units": "Volts", - "minimum": 0, - "data_object": "Vol", - "name": "MMXN.Vol.AI72" - }, - { - "index": 73, - "description": "Low/High Voltage Ride-Through High Must Trip Curve Index. Index of the Voltage Ride-through curve which specifies trip points when the voltage is high.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "PTOV", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTOV.BlkRef.AI73" - }, - { - "index": 74, - "description": "Low/High Voltage Ride-Through Low Must Trip Curve Index. Index of the Voltage Ride-through curve which specifies trip points when the voltage is low.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "PTUV", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTUV.BlkRef.AI74" - }, - { - "index": 75, - "description": "Low/High Voltage Ride-Through High Momentary Cessation Curve Index. Index of the Voltage Ride-through curve which specifies where generation/discharging must stop when the voltage is high.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "PTOV", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTOV.BlkRef.AI75" - }, - { - "index": 76, - "description": "Low/High Voltage Ride-Through Low Momentary Cessation Curve Index. Index of the Voltage Ride-through curve which specifies where generation/discharging must stop when the voltage is low.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "PTUV", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTUV.BlkRef.AI76" - }, - { - "index": 77, - "description": "Low/High Frequency Ride-Through Signal Meter ID. Referenced ECP. This is the meter from which current is being read to evaluate and provide support.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DHFT", - "minimum": 0, - "data_object": "EcpRef", - "name": "DHFT.EcpRef.AI77" - }, - { - "index": 78, - "description": "Low/High Frequency Ride-Through Frequency Reference Input. Active frequency measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "Hz", - "name": "MMXU.Hz.AI78" - }, - { - "index": 79, - "description": "Low/High Frequency Ride-Through High Must Trip Curve Index. Index of the Frequency Ride-through curve which specifies trip points when the frequency is high.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "PTOF", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTOF.BlkRef.AI79" - }, - { - "index": 80, - "description": "Low/High Frequency Ride-Through Low Must Trip Curve Index. Index of the Frequency Ride-through curve which specifies trip points when the frequency is low.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "PTUF", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTUF.BlkRef.AI80" - }, - { - "index": 81, - "description": "Low/High Frequency Ride-Through High Momentary Cessation Curve Index. Index of the Frequency Ride-through curve which specifies where generation/discharging must stop when the frequency is high.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "PTOF", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTOF.BlkRef.AI81" - }, - { - "index": 82, - "description": "Low/High Frequency Ride-Through Low Momentary Cessation Curve Index. Index of the Frequency Ride-through curve which specifies where generation/discharging must stop when the frequency is low.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "PTUF", - "minimum": 0, - "data_object": "BlkRef", - "name": "PTUF.BlkRef.AI82" - }, - { - "index": 83, - "description": "Dynamic Reactive Current Support Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DRGS", - "minimum": 0, - "data_object": "ModPrio", - "name": "DRGS.ModPrio.AI83" - }, - { - "index": 84, - "description": "Dynamic Reactive Current Support Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DRGS.WinTms.AI84" - }, - { - "index": 85, - "description": "Dynamic Reactive Current Support Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DRGS.RmpTms.AI85" - }, - { - "index": 86, - "description": "Dynamic Reactive Current Support Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DRGS.RvrtTms.AI86" - }, - { - "index": 87, - "description": "Dynamic Reactive Current Support Signal Meter ID. Referenced ECP. This is the meter from which current is being read to evaluate and provide support.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DRGS", - "minimum": 0, - "data_object": "EcpRef", - "name": "DRGS.EcpRef.AI87" - }, - { - "index": 88, - "description": "Dynamic Reactive Current Support Voltage Reference Input. Votltage measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "MMXN", - "units": "Volts", - "minimum": 0, - "data_object": "Vol", - "name": "MMXN2.Vol.AI88" - }, - { - "index": 89, - "description": "Dynamic Reactive Current Support Moving Average Voltage", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "DRGS", - "units": "Volts", - "minimum": 0, - "data_object": "VAv", - "name": "DRGS.VAv.AI89" - }, - { - "index": 90, - "description": "Dynamic Reactive Current Support Present Delta Voltage. Difference in Volts between the present measured Voltage and the Moving Average Voltage (RDGS.Vav) as a percentage of the reference voltage (VRef).", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "maximum": 10000, - "ln_class": "DRGS", - "units": "Percent", - "minimum": -10000, - "data_object": "DelV", - "name": "DRGS.DelV.AI90" - }, - { - "index": 91, - "description": "Dynamic Reactive Current Support - Gradient Mode.", - "data_type": "AI", - "common_data_class": "SPG", - "maximum": 2, - "ln_class": "DRGS", - "units": "None (list)", - "minimum": 0, - "data_object": "ArGraMod", - "allowed_values": { - "0": "Undefined", - "1": "Gradients reach 0 at the moving average Voltage", - "2": "Gradients reach 0 at the Voltage deadbands" - }, - "type": "enumerated", - "name": "DRGS.ArGraMod.AI91" - }, - { - "index": 92, - "description": "Dynamic Reactive Current Support Deadband Minimum Voltage. Percentage of the nominal voltage (DRCT.Vref), measured from the moving average voltage (RDGS.VAv). Support is no longer applied when the voltage stays above this value for the length of the Hold Time.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 0, - "ln_class": "DRGS", - "units": "Percent", - "minimum": -10000, - "data_object": "DbVMin", - "name": "DRGS.DbVMin.AI92" - }, - { - "index": 93, - "description": "Dynamic Reactive Current Support Deadband Maximum Voltage. Percentage of the nominal voltage (DRCT.Vref), measured from the moving average voltage (RDGS.VAv). Support is no longer applied when the voltage stays below this value for the length of the Hold Time.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 10000, - "ln_class": "DRGS", - "units": "Percent", - "minimum": 0, - "data_object": "DbVMax", - "name": "DRGS.DbVMax.AI93" - }, - { - "index": 94, - "description": "Dynamic Reactive Current Support Gradient for Sags. Percentage of the rated current (DRAT.ARtg) to apply capacitively per percentage of the negative deviation from the moving average voltage (RDGS.Av). It is a ratio of percent and is therefore unitless.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DRGS", - "units": "Percent current per percent voltage deviation", - "data_object": "ArGraSag", - "name": "DRGS.ArGraSag.AI94" - }, - { - "index": 95, - "description": "Dynamic Reactive Current Support Gradient for Swells. Percentage of the rated current (DRAT.ARtg) to apply inductively per percentage of the positive deviation from the moving average voltage (RDGS.Av). It is a ratio of percent and is therefore unitless.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DRGS", - "units": "Percent current per percent voltage deviation", - "data_object": "ArGraSwl", - "name": "DRGS.ArGraSwl.AI95" - }, - { - "index": 96, - "description": "Dynamic Reactive Current Support Filter Time for Moving Average Voltage (RDGS.VAv). Used to determine amount of dynamic reactive current support.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "Seconds", - "minimum": 0, - "data_object": "FilTms", - "name": "DRGS.FilTms.AI96" - }, - { - "index": 97, - "description": "Dynamic Reactive Current Support Block Zone Voltage. Percentage of the nominal voltage (DRCT.VRef) below which no reactive current support shall be applied.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DRGS", - "units": "Percent", - "minimum": 0, - "data_object": "BlkZnV", - "name": "DRGS.BlkZnV.AI97" - }, - { - "index": 98, - "description": "Dynamic Reactive Current Support Hysteresis Block Zone Voltage. Percentage of the nominal voltage (DRCT.VRef). After being blocked,reactive current support shall not resume until the voltage has been above BlkZnV + HysBlkZnV.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DRGS", - "units": "Percent", - "minimum": 0, - "data_object": "HysBlkZnV", - "name": "DRGS.HysBlkZnV.AI98" - }, - { - "index": 99, - "description": "Dynamic Reactive Current Support Block Zone Time. Time in milliseconds from the beginning of any \"sag\" event, before which dynamic reactive current support will always continue, regardless of how low voltage may sag.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "ms", - "minimum": 0, - "data_object": "BlkZnTmms", - "name": "DRGS.BlkZnTmms.AI99" - }, - { - "index": 100, - "description": "Dynamic Reactive Current Support Hold Time. When the voltage returns to within the deadband limits (RDGS.dbVMin annd RDGS.dbVMax) for this length of time (measured in milliseconds), the \"sag\" or \"swell\" event is considered to be over. Reactive current support ends, frozen values are unfrozen, and a new event can begin.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DRGS", - "units": "ms", - "minimum": 0, - "data_object": "HoldTmms", - "name": "DRGS.HoldTmms.AI100" - }, - { - "index": 101, - "description": "Dynamic Reactive Current Attempted Output. Current output that the mode is attempting to achieve based on the Voltage input and other parameters.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "DRGS", - "units": "Amps", - "minimum": 0, - "data_object": "ReqA", - "name": "DRGS.ReqA.AI101" - }, - { - "index": 102, - "description": "Dynamic Volt-Watt Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWD", - "minimum": 0, - "data_object": "ModPrio", - "name": "DVWD.ModPrio.AI102" - }, - { - "index": 103, - "description": "Dynamic Volt-Watt Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWD", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DVWD.WinTms.AI103" - }, - { - "index": 104, - "description": "Dynamic Volt-Watt Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWD", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DVWD.RmpTms.AI104" - }, - { - "index": 105, - "description": "Dynamic Volt-Watt Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWD", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DVWD.RvrtTms.AI105" - }, - { - "index": 106, - "description": "Dynamic Volt-Watt Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DVWD", - "minimum": 0, - "data_object": "EcpRef", - "name": "DVWD2.EcpRef.AI106" - }, - { - "index": 107, - "description": "Dynamic Volt-Watt Voltage Reference Input. Votltage measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "MMXN", - "units": "Volts", - "minimum": 0, - "data_object": "Vol", - "name": "MMXN2.Vol.AI107" - }, - { - "index": 108, - "description": "Dynamic Volt-Watt Moving Average Voltage", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "DVWD", - "units": "Volts", - "minimum": 0, - "data_object": "VAv", - "name": "DVWD2.VAv.AI108" - }, - { - "index": 109, - "description": "Dynamic Volt-Watt Present Delta Voltage", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "DVWD", - "units": "Volts", - "minimum": 0, - "data_object": "DelV", - "name": "DVWD2.DelV.AI109" - }, - { - "index": 110, - "description": "Dynamic Volt-Watt Gradient. Signed quantity that establishes the ratio of additional Watts supplied (expressed in terms of % DRCT.WMax) to the present difference from the moving average voltage (expressed as % DRCT.VRef).", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DVWD", - "units": "Percent watts per percent voltage difference", - "data_object": "DynVWGra", - "name": "DVWD.DynVWGra.AI110" - }, - { - "index": 111, - "description": "Dynamic Volt-Watt Filter Time. The time in seconds used to calculate the moving average voltage for dynamic Volt-Watt support.", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DVWD", - "units": "Seconds", - "minimum": 0, - "data_object": "VWFilTms", - "name": "DVWD.VWFilTms.AI111" - }, - { - "index": 112, - "description": "Dynamic Volt-Watt Lower Deadband. Percentage of the nominal voltage (DRCT.Vref) measured below the moving average voltage. If the present voltage is above this value,no additional Watts shall be supplied.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 0, - "ln_class": "DVWD", - "units": "Percent", - "minimum": -1000, - "data_object": "DbVWLo", - "name": "DVWD.DbVWLo.AI112" - }, - { - "index": 113, - "description": "Dynamic Volt-Watt Upper Deadband. Percentage of the nominal voltage (DRCT.Vref) measured above the moving average voltage. If the present voltage is below this value, no additional Watts shall be supplied.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DVWD", - "units": "Percent", - "minimum": 0, - "data_object": "DbVWHi", - "name": "DVWD.DbVWHi.AI113" - }, - { - "index": 114, - "description": "Dynamic Volt-Watt Attempted Output. Watt output that the mode is attempting to achieve based on the Voltage input and other parameters.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DVWD", - "units": "Watts", - "data_object": "ReqWSet", - "name": "DVWD.ReqWSet.AI114" - }, - { - "index": 115, - "description": "Frequency-Watt Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "ModPrio", - "name": "DHFW2.ModPrio.AI115" - }, - { - "index": 116, - "description": "Frequency-Watt Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DHFW.WinTms.AI116" - }, - { - "index": 117, - "description": "Frequency-Watt Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DHFW.RmpTms.AI117" - }, - { - "index": 118, - "description": "Frequency-Watt Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DHFW.RvrtTms.AI118" - }, - { - "index": 119, - "description": "Frequency-Watt Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "EcpRef", - "name": "DHFW2.EcpRef.AI119" - }, - { - "index": 120, - "description": "Frequency-Watt Frequency Reference Input. Frequency measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "MMXU", - "units": "Hz", - "minimum": 0, - "data_object": "Hz", - "name": "MMXU2.Hz.AI120" - }, - { - "index": 121, - "description": "Frequency-Watt High Starting Frequency. Delta frequency between start frequency and nominal grid frequency for high frequency events.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DHFW", - "units": "Hz", - "minimum": 0, - "data_object": "HzStr", - "name": "DHFW2.HzStr.AI121" - }, - { - "index": 122, - "description": "Frequency-Watt High Stopping Frequency. Delta frequency between stop frequency and nominal grid frequency for high frequency events.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "DHFW", - "units": "Hz", - "minimum": 0, - "data_object": "HzStop", - "name": "DHFW2.HzStop.AI122" - }, - { - "index": 123, - "description": "Frequency-Watt High Discharging/Generating Gradient", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DHFW", - "units": "Percent watts per percent frequency difference", - "data_object": "WGra", - "name": "DHFW.WGra.AI123" - }, - { - "index": 124, - "description": "Frequency-Watt High Charging Gradient", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DHFW", - "units": "Percent watts per percent frequency difference", - "data_object": "WChaGra", - "name": "DHFW.WChaGra.AI124" - }, - { - "index": 125, - "description": "Frequency-Watt Low Starting Frequency. Delta frequency between start frequency and nominal grid frequency for low frequency events.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 0, - "ln_class": "DLFW", - "units": "Hz", - "minimum": -70000, - "data_object": "HzStr", - "name": "DLFW2.HzStr.AI125" - }, - { - "index": 126, - "description": "Frequency-Watt Low Stopping Frequency. Delta frequency between stop frequency and nominal grid frequency for low frequency events.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "maximum": 0, - "ln_class": "DLFW", - "units": "Hz", - "minimum": -70000, - "data_object": "HzStop", - "name": "DLFW2.HzStop.AI126" - }, - { - "index": 127, - "description": "Frequency-Watt Low Discharging/Generating Gradient", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DLFW", - "units": "Percent watts per percent frequency difference", - "data_object": "WGra", - "name": "DLFW.WGra.AI127" - }, - { - "index": 128, - "description": "Frequency-Watt Low Charging Gradient", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DLFW", - "units": "Percent watts per percent frequency difference", - "data_object": "WChaGra", - "name": "DLFW.WChaGra.AI128" - }, - { - "index": 129, - "description": "Frequency-Watt Start Delay", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "ActStrDlTmms", - "name": "DHFW2.ActStrDlTmms.AI129" - }, - { - "index": 130, - "description": "Frequency-Watt Stop Delay", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "ActStopDlTmms", - "name": "DHFW2.ActStopDlTmms.AI130" - }, - { - "index": 131, - "description": "Frequency-Watt Ramp Up Time Constant. Time constant or open loop response time for moving from the current active power target to a higher active power target.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DLFW", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DLFW.OpnLoopMax.AI131" - }, - { - "index": 132, - "description": "Frequency-Watt Ramp Down Time Constant. Time constant or open loop response time for moving from the current active power target to a lower active power target.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DHFW.OpnLoopMax.AI132" - }, - { - "index": 133, - "description": "Frequency-Watt Discharge Ramp Up Rate", - "data_type": "AI", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DHFW.RpuRte.AI133" - }, - { - "index": 134, - "description": "Frequency-Watt Discharge Ramp Down Rate", - "data_type": "AI", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRteMax", - "name": "DHFW.RpdRteMax.AI134" - }, - { - "index": 135, - "description": "Frequency-Watt Charge Ramp Up Rate", - "data_type": "AI", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuChaRte", - "name": "DHFW.RpuChaRte.AI135" - }, - { - "index": 136, - "description": "Frequency-Watt Charge Ramp Down Rate", - "data_type": "AI", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdChaRteMax", - "name": "DHFW.RpdChaRteMax.AI136" - }, - { - "index": 137, - "description": "Frequency-Watt High Return Gradient", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DHFW", - "units": "Percent watts per percent frequency difference", - "minimum": 0, - "data_object": "RtnRmpRte", - "name": "DHFW2.RtnRmpRte.AI137" - }, - { - "index": 138, - "description": "Frequency-Watt Low Return Gradient", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DLFW", - "units": "Percent watts per percent frequency difference", - "minimum": 0, - "data_object": "RtnRmpRte", - "name": "DLFW2.RtnRmpRte.AI138" - }, - { - "index": 139, - "description": "Frequency-Watt Attempted Output. Watt output that the mode is attempting to achieve based on the Frequency input and other parameters.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DHFW", - "units": "Watts", - "data_object": "ReqWLim", - "name": "DHFW.ReqWLim.AI139" - }, - { - "index": 140, - "description": "Frequency-Watt Minimum Usable SOC", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DAGC", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMinPct", - "name": "DAGC.SocUseMinPct.AI140" - }, - { - "index": 141, - "description": "Frequency-Watt Maximum Usable SOC", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DAGC", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMaxPct", - "name": "DAGC.SocUseMaxPct.AI141" - }, - { - "index": 142, - "description": "Active Power Limit Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWMX", - "minimum": 0, - "data_object": "ModPrio", - "name": "DWMX.ModPrio.AI142" - }, - { - "index": 143, - "description": "Active Power Limit Enabling Time Window. Time window (in seconds) within which to randomly execute a command. If the time window is zero, the command will be executed immediately.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWMX", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DWMX.WinTms.AI143" - }, - { - "index": 144, - "description": "Active Power Limit Enabling Ramp Time. Ramp time, in seconds, for moving from current operational mode settings to new operational mode settings.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWMX", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DWMX.RmpTms.AI144" - }, - { - "index": 145, - "description": "Active Power Limit Reversion Timeout Period. Reversion Timeout Period (in seconds), after which the device will revert to its default status, such as closing the switch to reconnect to the grid or allowing maximum watts output, in case communications are lost or mitigating messages are not received.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWMX", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DWMX.RvrtTms.AI145" - }, - { - "index": 146, - "description": "Active Power Limit Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DWMX", - "minimum": 0, - "data_object": "EcpRef", - "name": "DWMX.EcpRef.AI146" - }, - { - "index": 147, - "description": "Active Power Limit Reference Input. Active Power measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "data_object": "TotW", - "name": "MMXU.TotW.AI147" - }, - { - "index": 148, - "description": "Active Power Limit Charge Setpoint", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWMX", - "units": "Percent", - "minimum": 0, - "data_object": "WLimPct", - "name": "DWMX.WLimPct.AI148" - }, - { - "index": 149, - "description": "Active Power Limit Generation Setpoint", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWMN", - "units": "Percent", - "minimum": 0, - "data_object": "WLimPct", - "name": "DWMN.WLimPct.AI149" - }, - { - "index": 150, - "description": "Charge/Discharge Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWGC", - "minimum": 0, - "data_object": "ModPrio", - "name": "DWGC.ModPrio.AI150" - }, - { - "index": 151, - "description": "Charge/Discharge Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DWGC.WinTms.AI151" - }, - { - "index": 152, - "description": "Charge/Discharge Enabling Ramp Time. Ramp time, in seconds, for moving from current operational mode settings to new operational mode settings.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DWGC.RmpTms.AI152" - }, - { - "index": 153, - "description": "Charge/Discharge Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DWGC.RvrtTms.AI153" - }, - { - "index": 154, - "description": "Charge/Discharge Active Power Target. Percentage of maximum active power.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWGC", - "units": "Percent", - "minimum": -1000, - "data_object": "GnWPctSpt", - "name": "DWGC.GnWPctSpt.AI154" - }, - { - "index": 155, - "description": "Charge/Discharge Ramp Up Time Constant. Ramp time, in seconds, for moving from the current active power target to a higher active power target.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DWGC.OpnLoopMax.AI155" - }, - { - "index": 156, - "description": "Charge/Discharge Ramp Down Time Constant. Ramp time, in seconds, for moving from the current active power target to a lower active power target.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWGC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DWGC.OpnLoopMax.AI156" - }, - { - "index": 157, - "description": "Charge/Discharge Discharge Ramp Up Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DWGC.RpuRte.AI157" - }, - { - "index": 158, - "description": "Charge/Discharge Discharge Ramp Down Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRteMax", - "name": "DWGC.RpdRteMax.AI158" - }, - { - "index": 159, - "description": "Charge/Discharge Charge Ramp Up Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuChaRte", - "name": "DWGC.RpuChaRte.AI159" - }, - { - "index": 160, - "description": "Charge/Discharge Charge Ramp Down Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdChaRteMax", - "name": "DWGC.RpdChaRteMax.AI160" - }, - { - "index": 161, - "description": "Charge/Discharge Minimum Reserve for Storage. The reserve level below which the storage system may be only be discharged in emergency situations, expressed as a percentage of the usable capacity.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWGC", - "units": "Percent", - "minimum": -1000, - "data_object": "SocUseMinPct", - "name": "DWGC.SocUseMinPct.AI161" - }, - { - "index": 162, - "description": "Charge/Discharge Maximum Reserve for Storage. The reserve level above which the storage system may be only be charged in emergency situations, expressed as a percentage of the usable capacity.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DWGC", - "units": "Percent", - "minimum": -1000, - "data_object": "SocUseMaxPct", - "name": "DWGC.SocUseMaxPct.AI162" - }, - { - "index": 163, - "description": "Coordinated Charge/Discharge Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "minimum": 0, - "data_object": "ModPrio", - "name": "DTCD.ModPrio.AI163" - }, - { - "index": 164, - "description": "Coordinated Charge/Discharge Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DTCD.WinTms.AI164" - }, - { - "index": 165, - "description": "Coordinated Charge/Discharge Enabling Ramp Time. Ramp time, in seconds, for moving from current operational mode settings to new operational mode settings", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DTCD.RmpTms.AI165" - }, - { - "index": 166, - "description": "Coordinated Charge/Discharge Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DTCD.RvrtTms.AI166" - }, - { - "index": 167, - "description": "Coordinated Charge/Discharge Target State of Charge. Charge that the system is expected to achieve, as a percentage of the usable capacity.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DTCD", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseTgtPct", - "name": "DTCD.SocUseTgtPct.AI167" - }, - { - "index": 168, - "description": "Coordinated Charge/Discharge Target Date. Date by which the storage system must reach the target SOC. Days since January 1, 1970, UTC.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Days", - "minimum": 0, - "data_object": "DateTgt", - "name": "DTCD.DateTgt.AI168" - }, - { - "index": 169, - "description": "Coordinated Charge/Discharge Target Time. Time by when the storage system must reach the target SOC. Expressed as the number of seconds since the start of Target Date.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Milliseconds", - "minimum": 0, - "data_object": "DateTgtTms", - "name": "DTCD.DateTgtTms.AI169" - }, - { - "index": 170, - "description": "Coordinated Charge/Discharge Energy Request. Amount of energy that must be transferred from the grid to the charger to move the SOC from the value at the specific time of reference to the target SOC.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Watt-hours", - "minimum": 0, - "data_object": "SocWReq", - "name": "DTCD.SocWReq.AI170" - }, - { - "index": 171, - "description": "Coordinated Charge/Discharge Minimum Charging Duration. Minimum duration to move from the SOC at the time of reference to the target SOC.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "ChaDurTms", - "name": "DTCD.ChaDurTms.AI171" - }, - { - "index": 172, - "description": "Coordinated Charge/Discharge Date of Reference. Date that the SOC is measured or computed by the storage system and is the basis for the Energy Request, Minimum Charging Duration, and other parameters.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Days", - "minimum": 0, - "data_object": "DateTgt", - "name": "DTCD.DateTgt.AI172" - }, - { - "index": 173, - "description": "Coordinated Charge/Discharge Time of Reference. Time that the SOC is measured or computed by the storage system and is the basis for the Energy Request, Minimum Charging Duration, and other parameters.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Milliseconds", - "minimum": 0, - "data_object": "SocDateTms", - "name": "DTCD.SocDateTms.AI173" - }, - { - "index": 174, - "description": "Coordinated Charge/Discharge Duration at Maximum Charge Rate. Duration that energy can be stored at the Maximum Charge Rate.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "ChaDurMax", - "name": "DTCD.ChaDurMax.AI174" - }, - { - "index": 175, - "description": "Coordinated Charge/Discharge Duration Maximum Discharge Rate. Duration that energy can be delivered at the Maximum Discharge Rate.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DTCD", - "units": "Seconds", - "minimum": 0, - "data_object": "DschDurMax", - "name": "DTCD.DschDurMax.AI175" - }, - { - "index": 176, - "description": "Active Power Response Mode #1 Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPKP", - "minimum": 0, - "data_object": "ModPrio", - "name": "DPKP.ModPrio.AI176" - }, - { - "index": 177, - "description": "Active Power Response Mode #1 Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPKP", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DPKP.WinTms.AI177" - }, - { - "index": 178, - "description": "Active Power Response Mode #1 Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPKP", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DPKP.RmpTms.AI178" - }, - { - "index": 179, - "description": "Active Power Response Mode #1 Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPKP", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DPKP.RvrtTms.AI179" - }, - { - "index": 180, - "description": "Active Power Response Mode #1 Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DPKP", - "minimum": 0, - "data_object": "EcpRef", - "name": "DPKP.EcpRef.AI180" - }, - { - "index": 181, - "description": "Active Power Response Mode #1 Reference Power Measured", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "data_object": "TotW", - "name": "MMXU.TotW.AI181" - }, - { - "index": 182, - "description": "Active Power Response Mode #1 Power Threshold", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DPKP", - "units": "Watts", - "data_object": "PkPwrWLim", - "name": "DPKP.PkPwrWLim.AI182" - }, - { - "index": 183, - "description": "Active Power Response Mode #1 Ratio", - "data_type": "AI", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DPKP", - "units": "Percent", - "minimum": 0, - "data_object": "PkPwrFolPct", - "name": "DPKP.PkPwrFolPct.AI183" - }, - { - "index": 184, - "description": "Active Power Response Mode #1 Ramp Up Rate. Maximum ramp up rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DPKP", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DPKP.RpuRte.AI184" - }, - { - "index": 185, - "description": "Active Power Response Mode #1 Ramp Down Rate. Maximum ramp down rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DPKP", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DPKP.RpdRte.AI185" - }, - { - "index": 186, - "description": "Active Power Response Mode #1 Attempted Output. Watt output that the mode is attempting to achieve based on the Watts input and other parameters.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DPKP", - "units": "Watts", - "data_object": "ReqWSet", - "name": "DPKP.ReqWSet.AI186" - }, - { - "index": 187, - "description": "Active Power Response Mode #2 Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DGFL", - "minimum": 0, - "data_object": "ModPrio", - "name": "DGFL.ModPrio.AI187" - }, - { - "index": 188, - "description": "Active Power Response Mode #2 Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DGFL", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DGFL.WinTms.AI188" - }, - { - "index": 189, - "description": "Active Power Response Mode #2 Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DGFL", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DGFL.RmpTms.AI189" - }, - { - "index": 190, - "description": "Active Power Response Mode #2 Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DGFL", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DGFL.RvrtTms.AI190" - }, - { - "index": 191, - "description": "Active Power Response Mode #2 Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DGFL", - "minimum": 0, - "data_object": "EcpRef", - "name": "DGFL.EcpRef.AI191" - }, - { - "index": 192, - "description": "Active Power Response Mode #2 Reference Power Measured", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "data_object": "TotW", - "name": "MMXU.TotW.AI192" - }, - { - "index": 193, - "description": "Active Power Response Mode #2 Power Threshold", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DGFL", - "units": "Watts", - "data_object": "PkPwrWLim", - "name": "DGFL.PkPwrWLim.AI193" - }, - { - "index": 194, - "description": "Active Power Response Mode #2 Ratio", - "data_type": "AI", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DGFL", - "units": "Percent", - "minimum": 0, - "data_object": "PkPwrFolPct", - "name": "DGFL.PkPwrFolPct.AI194" - }, - { - "index": 195, - "description": "Active Power Response Mode #2 Ramp Up Rate. Maximum ramp up rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DGFL", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DGFL.RpuRte.AI195" - }, - { - "index": 196, - "description": "Active Power Response Mode #2 Ramp Down Rate. Maximum ramp down rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DGFL", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DGFL.RpdRte.AI196" - }, - { - "index": 197, - "description": "Active Power Response Mode #2 Attempted Output. Watt output that the mode is attempting to achieve based on the Watts input and other parameters.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DGFL", - "units": "Watts", - "data_object": "ReqWSet", - "name": "DGFL.ReqWSet.AI197" - }, - { - "index": 198, - "description": "Active Power Response Mode #3 Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DLFL", - "minimum": 0, - "data_object": "ModPrio", - "name": "DLFL.ModPrio.AI198" - }, - { - "index": 199, - "description": "Active Power Response Mode #3 Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DLFL", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DLFL.WinTms.AI199" - }, - { - "index": 200, - "description": "Active Power Response Mode #3 Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DLFL", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DLFL.RmpTms.AI200" - }, - { - "index": 201, - "description": "Active Power Response Mode #3 Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DLFL", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DLFL.RvrtTms.AI201" - }, - { - "index": 202, - "description": "Active Power Response Mode #3 Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DLFL", - "minimum": 0, - "data_object": "EcpRef", - "name": "DLFL.EcpRef.AI202" - }, - { - "index": 203, - "description": "Active Power Response Mode #3 Reference Power Measured", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "data_object": "TotW", - "name": "MMXU.TotW.AI203" - }, - { - "index": 204, - "description": "Active Power Response Mode #3 Power Threshold", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DLFL", - "units": "Watts", - "data_object": "PkPwrWLim", - "name": "DLFL.PkPwrWLim.AI204" - }, - { - "index": 205, - "description": "Active Power Response Mode #3 Ratio", - "data_type": "AI", - "common_data_class": "ING", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DLFL", - "units": "Percent", - "minimum": 0, - "data_object": "PkPwrFolPct", - "name": "DLFL.PkPwrFolPct.AI205" - }, - { - "index": 206, - "description": "Active Power Response Mode #3 Ramp Up Rate. Maximum ramp up rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DLFL", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DLFL.RpuRte.AI206" - }, - { - "index": 207, - "description": "Active Power Response Mode #3 Ramp Down Rate. Maximum ramp down rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DLFL", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DLFL.RpdRte.AI207" - }, - { - "index": 208, - "description": "Active Power Response Mode #3 Attempted Output. Watt output that the mode is attempting to achieve based on the Watts input and other parameters.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DLFL", - "units": "Watts", - "data_object": "ReqWSet", - "name": "DLFL.ReqWSet.AI208" - }, - { - "index": 209, - "description": "AGC Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DAGC", - "minimum": 0, - "data_object": "ModPrio", - "name": "DAGC.ModPrio.AI209" - }, - { - "index": 210, - "description": "AGC Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DAGC.WinTms.AI210" - }, - { - "index": 211, - "description": "AGC Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DAGC.RmpTms.AI211" - }, - { - "index": 212, - "description": "AGC Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DAGC.RvrtTms.AI212" - }, - { - "index": 213, - "description": "AGC Active Power Target", - "data_type": "AI", - "common_data_class": "APC", - "ln_class": "DAGC", - "units": "Watts", - "data_object": "GnWSpt", - "name": "DAGC.GnWSpt.AI213" - }, - { - "index": 214, - "description": "AGC Ramp Up Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpUpTms", - "name": "DAGC.RmpUpTms.AI214" - }, - { - "index": 215, - "description": "AGC Ramp Down Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpDnTms", - "name": "DAGC.RmpDnTms.AI215" - }, - { - "index": 216, - "description": "AGC Discharge Ramp Up Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DAGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DAGC.RpuRte.AI216" - }, - { - "index": 217, - "description": "AGC Discharge Ramp Down Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DAGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DAGC.RpdRte.AI217" - }, - { - "index": 218, - "description": "AGC Charge Ramp Up Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DAGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuChaRte", - "name": "DAGC.RpuChaRte.AI218" - }, - { - "index": 219, - "description": "AGC Charge Ramp Down Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DAGC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdChaRte", - "name": "DAGC.RpdChaRte.AI219" - }, - { - "index": 220, - "description": "AGC Minimum Usable SOC", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DAGC", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMinPct", - "name": "DAGC.SocUseMinPct.AI220" - }, - { - "index": 221, - "description": "AGC Maximum Usable SOC", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DAGC", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMaxPct", - "name": "DAGC.SocUseMaxPct.AI221" - }, - { - "index": 222, - "description": "AGC Maximum Watts Available", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DAGC", - "units": "Watts", - "data_object": "WMaxAvl", - "name": "DAGC.WMaxAvl.AI222" - }, - { - "index": 223, - "description": "AGC Minimum Watts Available", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DAGC", - "units": "Watts", - "data_object": "WMinAvl", - "name": "DAGC.WMinAvl.AI223" - }, - { - "index": 224, - "description": "AGC Expected State of Charge", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DAGC", - "units": "Percent", - "minimum": 0, - "data_object": "SocExpc", - "name": "DAGC.SocExpc.AI224" - }, - { - "index": 225, - "description": "AGC Expected State of Energy", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DAGC", - "units": "Percent", - "minimum": 0, - "data_object": "SoeExpc", - "name": "DAGC.SoeExpc.AI225" - }, - { - "index": 226, - "description": "AGC Expected State of Charge Time Interval", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DAGC", - "units": "Seconds", - "minimum": 0, - "data_object": "SocExpcTms", - "name": "DAGC.SocExpcTms.AI226" - }, - { - "index": 227, - "description": "Active Power Smoothing Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWSM", - "minimum": 0, - "data_object": "ModPrio", - "name": "DWSM.ModPrio.AI227" - }, - { - "index": 228, - "description": "Active Power Smoothing Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWSM", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DWSM.WinTms.AI228" - }, - { - "index": 229, - "description": "Active Power Smoothing Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWSM", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DWSM.RmpTms.AI229" - }, - { - "index": 230, - "description": "Active Power Smoothing Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWSM", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DWSM.RvrtTms.AI230" - }, - { - "index": 231, - "description": "Active Power Smoothing Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DWSM", - "minimum": 0, - "data_object": "EcpRef", - "name": "DWSM.EcpRef.AI231" - }, - { - "index": 232, - "description": "Active Power Smoothing Reference Power Input. Active Power measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "minimum": 0, - "data_object": "TotW", - "name": "MMXU.TotW.AI232" - }, - { - "index": 233, - "description": "Active Power Smoothing Gradient. Signed quantity that establishes the ratio of additional smoothing Watts provided to the present delta-watts of the reference load or generation. Delta Watts is the difference between the moving average and the present value of the reference power. Positive values of this gradient are for following load (increased reference load results in a dynamic increase in DER output), and negative values are for following generation (increased reference generation results in a dynamic decrease in DER output).", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.001, - "ln_class": "DWSM", - "units": "Watts per Delta-watt", - "data_object": "WSmthGra", - "name": "DWSM.WSmthGra.AI233" - }, - { - "index": 234, - "description": "Active Power Smoothing Lower Limit. Difference in Watts from the moving average of the reference power above which no smoothing shall be applied.", - "data_type": "AI", - "common_data_class": "ASG", - "maximum": 0, - "ln_class": "DWSM", - "units": "Watts", - "data_object": "WSmthLoLim", - "name": "DWSM.WSmthLoLim.AI234" - }, - { - "index": 235, - "description": "Active Power Smoothing Upper Limit. Difference in Watts from the moving average of the reference power below which no smoothing shall be applied.", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DWSM", - "units": "Watts", - "minimum": 0, - "data_object": "WSmthHiLim", - "name": "DWSM.WSmthHiLim.AI235" - }, - { - "index": 236, - "description": "Active Power Smoothing Filter Time (Seconds). Time in seconds used to calculate the moving average of the reference load or generation being smoothed.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWSM", - "units": "Seconds", - "minimum": 0, - "data_object": "FilTms", - "name": "DWSM.FilTms.AI236" - }, - { - "index": 237, - "description": "Active Power Smoothing Discharge Ramp Up Rate. The maximum generation ramp up rate expressed as a percentage of the Maximum Generation Rate (WMax) per second.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWSM", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DWSM.RpuRte.AI237" - }, - { - "index": 238, - "description": "Active Power Smoothing Discharge Ramp Down Rate. The maximum generation ramp down rate expressed as a percentage of the Maximum Generation Rate (WMax) per second.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWSM", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DWSM.RpdRte.AI238" - }, - { - "index": 239, - "description": "Active Power Smoothing Charge Ramp Up Rate. The maximum charging ramp up rate expressed as a percentage of the Maximum Charging Rate (WChaMax) per second.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWSM", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuChaRte", - "name": "DWSM.RpuChaRte.AI239" - }, - { - "index": 240, - "description": "Active Power Smoothing Charge Ramp Down Rate. The maximum charging ramp down rate expressed as a percentage of the Maximum Charnging Rate (WChaMax) per second.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DWSM", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdChaRte", - "name": "DWSM.RpdChaRte.AI240" - }, - { - "index": 241, - "description": "Active Power Smoothing Attempted Output. Watt output that the mode is attempting to achieve based on the Watt input and other parameters.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DWSM", - "units": "Watts", - "data_object": "ReqWSet", - "name": "DWSM.ReqWSet.AI241" - }, - { - "index": 242, - "description": "Volt-Watt Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWC", - "minimum": 0, - "data_object": "ModPrio", - "name": "DVWC.ModPrio.AI242" - }, - { - "index": 243, - "description": "Volt-Watt Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DVWC.WinTms.AI243" - }, - { - "index": 244, - "description": "Volt-Watt Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DVWC.RmpTms.AI244" - }, - { - "index": 245, - "description": "Volt-Watt Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DVWC.RvrtTms.AI245" - }, - { - "index": 246, - "description": "Volt-Watt Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DVWC", - "minimum": 0, - "data_object": "EcpRef", - "name": "DVWC.EcpRef.AI246" - }, - { - "index": 247, - "description": "Volt-Watt Reference Voltage Input. Voltage measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "MMXN", - "units": "Volts", - "minimum": 0, - "data_object": "Vol", - "name": "MMXN.Vol.AI247" - }, - { - "index": 248, - "description": "Volt-Watt Curve Index. Index of the Volt-Watt curve that should be used by the mode.", - "data_type": "AI", - "common_data_class": "CSG", - "ln_class": "DVWC", - "minimum": 0, - "data_object": "VWCrv", - "name": "DVWC.VWCrv.AI248" - }, - { - "index": 249, - "description": "Volt-Watt Attempted Output. Maximum active power the outstation will attempt to generate or absorb based on the Voltage input and selected curve.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DVWC", - "units": "Watts", - "data_object": "ReqWLim", - "name": "DVWC.ReqWLim.AI249" - }, - { - "index": 250, - "description": "Volt-Watt Filter Time (Seconds)", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "FilTms", - "name": "DVWC.FilTms.AI250" - }, - { - "index": 251, - "description": "Volt-Watt Ramp Up Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVWC.OpnLoopMax.AI251" - }, - { - "index": 252, - "description": "Volt-Watt Ramp Down Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVWC", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVWC.OpnLoopMax.AI252" - }, - { - "index": 253, - "description": "Volt-Watt Discharging Ramp Up Rate. Maximum ramp up rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DVWC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DVWC.RpuRte.AI253" - }, - { - "index": 254, - "description": "Volt-Watt Discharging Ramp Down Rate. Maximum ramp down rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DVWC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DVWC.RpdRte.AI254" - }, - { - "index": 255, - "description": "Volt-Watt Charging Ramp Up Rate. Maximum charging ramp up rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DVWC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuChaRte", - "name": "DVWC.RpuChaRte.AI255" - }, - { - "index": 256, - "description": "Volt-Watt Charging Ramp Down Rate. Maximum charging ramp down rate.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DVWC", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdChaRte", - "name": "DVWC.RpdChaRte.AI256" - }, - { - "index": 257, - "description": "Frequency-Watt Curve Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "ModPrio", - "name": "DHFW.ModPrio.AI257" - }, - { - "index": 258, - "description": "Frequency-Watt Curve Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DHFW.WinTms.AI258" - }, - { - "index": 259, - "description": "Frequency-Watt Curve Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DHFW.RmpTms.AI259" - }, - { - "index": 260, - "description": "Frequency-Watt Curve Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DHFW.RvrtTms.AI260" - }, - { - "index": 261, - "description": "Frequency-Watt Curve Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "EcpRef", - "name": "DHFW.EcpRef.AI261" - }, - { - "index": 262, - "description": "Frequency-Watt Curve Frequency Reference Input. Frequency measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "MMXU", - "units": "Hz", - "minimum": 0, - "data_object": "Hz", - "name": "MMXU.Hz.AI262" - }, - { - "index": 263, - "description": "Frequency-Watt Curve - Curve Index. Index of the Frequency-Watt curve that should be used by the mode.", - "data_type": "AI", - "common_data_class": "CSG", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "HzWCrv", - "name": "DHFW.HzWCrv.AI263" - }, - { - "index": 264, - "description": "Frequency-Watt Curve - High Frequency Hysteresis Curve Index", - "data_type": "AI", - "common_data_class": "CSG", - "ln_class": "DHFW", - "minimum": 0, - "data_object": "HysCrv", - "name": "DHFW.HysCrv.AI264" - }, - { - "index": 265, - "description": "Frequency-Watt Curve - Low Frequency Hysteresis Curve Index", - "data_type": "AI", - "common_data_class": "CSG", - "ln_class": "DLFW", - "minimum": 0, - "data_object": "HysCrv", - "name": "DLFW.HysCrv.AI265" - }, - { - "index": 266, - "description": "Frequency-Watt Curve Start Delay", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "ActStrDlTmms", - "name": "DHFW.ActStrDlTmms.AI266" - }, - { - "index": 267, - "description": "Frequency-Watt Curve Stop Delay", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "ActStopDlTmms", - "name": "DHFW.ActStopDlTmms.AI267" - }, - { - "index": 268, - "description": "Frequency-Watt Curve Ramp Up Time Constant. Time constant or open loop response time for moving from the current active power target to a higher active power target.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DHFW.OpnLoopMax.AI268" - }, - { - "index": 269, - "description": "Frequency-Watt Curve Ramp Down Time Constant. Time constant or open loop response time for moving from the current active power target to a lower active power target.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DHFW", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DHFW.OpnLoopMax.AI269" - }, - { - "index": 270, - "description": "Frequency-Watt Curve Discharge Ramp Up Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuRte", - "name": "DHFW.RpuRte.AI270" - }, - { - "index": 271, - "description": "Frequency-Watt Curve Discharge Ramp Down Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdRte", - "name": "DHFW.RpdRte.AI271" - }, - { - "index": 272, - "description": "Frequency-Watt Curve Charge Ramp Up Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpuChaRte", - "name": "DHFW.RpuChaRte.AI272" - }, - { - "index": 273, - "description": "Frequency-Watt Curve Charge Ramp Down Rate", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 500000, - "ln_class": "DHFW", - "units": "Percent per Second", - "minimum": 0, - "data_object": "RpdChaRte", - "name": "DHFW.RpdChaRte.AI273" - }, - { - "index": 274, - "description": "Frequency-Watt Attempted Output. Watt output that the mode is attempting to achieve based on the Frequency input and selected curve. If Snapshot of Power is not enabled,this is the maximum active power the outstation will attempt to generate or absorb.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DHFW", - "units": "Watts", - "data_object": "ReqWLim", - "name": "DHFW.ReqWLim.AI274" - }, - { - "index": 275, - "description": "Frequency-Watt Curve Minimum Usable SOC", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DHFW", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMinPct", - "name": "DHFW.SocUseMinPct.AI275" - }, - { - "index": 276, - "description": "Frequency-Watt Curve Maximum Usable SOC", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DHFW", - "units": "Percent", - "minimum": 0, - "data_object": "SocUseMaxPct", - "name": "DHFW.SocUseMaxPct.AI276" - }, - { - "index": 277, - "description": "Constant VArs Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVAR", - "minimum": 0, - "data_object": "ModPrio", - "name": "DVAR.ModPrio.AI277" - }, - { - "index": 278, - "description": "Constant VArs Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DVAR.WinTms.AI278" - }, - { - "index": 279, - "description": "Constant VArs Enabling Ramp Time. Ramp time, in seconds, for moving from current operational mode settings to new operational mode settings.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DVAR.RmpTms.AI279" - }, - { - "index": 280, - "description": "Constant VArs Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DVAR.RvrtTms.AI280" - }, - { - "index": 281, - "description": "Constant VArs Reactive Power Target. Percentage of maximum reactive power.", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.1, - "maximum": 1000, - "ln_class": "DVAR", - "units": "Percent", - "minimum": -1000, - "data_object": "VArTgtPct", - "name": "DVAR.VArTgtPct.AI281" - }, - { - "index": 282, - "description": "Constant VArs Ramp Up Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVAR.OpnLoopMax.AI282" - }, - { - "index": 283, - "description": "Constant VArs Ramp Down Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVAR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVAR.OpnLoopMax.AI283" - }, - { - "index": 284, - "description": "Fixed Power Factor Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DFPF", - "minimum": 0, - "data_object": "ModPrio", - "name": "DFPF.ModPrio.AI284" - }, - { - "index": 285, - "description": "Fixed Power Factor Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DFPF", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DFPF.WinTms.AI285" - }, - { - "index": 286, - "description": "Fixed Power Factor Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DFPF", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DFPF.RmpTms.AI286" - }, - { - "index": 287, - "description": "Fixed Power Factor Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DFPF", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DFPF.RvrtTms.AI287" - }, - { - "index": 288, - "description": "Fixed Power Factor Setpoint - Generation/Discharging", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DFPF", - "units": "None", - "minimum": 0, - "data_object": "PFGnTgt", - "name": "DFPF.PFGnTgt.AI288" - }, - { - "index": 289, - "description": "Fixed Power Factor Setpoint - Charging", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DFPF", - "units": "None", - "minimum": 0, - "data_object": "PFLodTgt", - "name": "DFPF.PFLodTgt.AI289" - }, - { - "index": 290, - "description": "Volt-Var Control Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVVR", - "minimum": 0, - "data_object": "ModPrio", - "name": "DVVR.ModPrio.AI290" - }, - { - "index": 291, - "description": "Volt-VAr Control Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DVVR.WinTms.AI291" - }, - { - "index": 292, - "description": "Volt-VAr Control Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DVVR.RmpTms.AI292" - }, - { - "index": 293, - "description": "Volt-VAr Control Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DVVR.RvrtTms.AI293" - }, - { - "index": 294, - "description": "Volt-VAr Control Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DVVR", - "minimum": 0, - "data_object": "EcpRef", - "name": "DVVR.EcpRef.AI294" - }, - { - "index": 295, - "description": "Volt-VAr Control Voltage Input. Voltage measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "MMXN", - "units": "Volts", - "minimum": 0, - "data_object": "Vol", - "name": "MMXN.Vol.AI295" - }, - { - "index": 296, - "description": "Volt-VAr Control Adjusted Voltage Reference. The Voltage used as reference for Volt-VAr control. If Autonomous Voltage Reference Adjustment is disabled,this is the same fixed value as the Reference Voltage.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.1, - "ln_class": "DVVR", - "units": "Volts", - "minimum": 0, - "data_object": "VRefSet", - "name": "DVVR.VRefSet.AI296" - }, - { - "index": 297, - "description": "Volt-VAr Curve Index. Index of the Volt-VAr curve that should be used by the mode.", - "data_type": "AI", - "common_data_class": "CSG", - "ln_class": "DVVR", - "minimum": 0, - "data_object": "VVArCrv", - "name": "DVVR.VVArCrv.AI297" - }, - { - "index": 298, - "description": "Volt-VAr Ramp Up Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVVR.OpnLoopMax.AI298" - }, - { - "index": 299, - "description": "Volt-VAr Ramp Down Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DVVR.OpnLoopMax.AI299" - }, - { - "index": 300, - "description": "Volt-VAr Autonomous Voltage Reference Adjustment Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DVVR", - "units": "Seconds", - "minimum": 0, - "data_object": "VRefTmms", - "name": "DVVR.VRefTmms.AI300" - }, - { - "index": 301, - "description": "Volt-VAr Attempted Output. VAr output that the mode is attempting to achieve based on the Voltage input and selected curve.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DVVR", - "units": "VARs", - "data_object": "ReqVAr", - "name": "DVVR.ReqVAr.AI301" - }, - { - "index": 302, - "description": "Watt-VAr Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWVR", - "minimum": 0, - "data_object": "ModPrio", - "name": "DWVR.ModPrio.AI302" - }, - { - "index": 303, - "description": "Watt-VAr Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DWVR.WinTms.AI303" - }, - { - "index": 304, - "description": "Watt-VAr Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DWVR.RmpTms.AI304" - }, - { - "index": 305, - "description": "Watt-VAr Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DWVR.RvrtTms.AI305" - }, - { - "index": 306, - "description": "Watt-VAr Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DWVR", - "minimum": 0, - "data_object": "EcpRef", - "name": "DWVR.EcpRef.AI306" - }, - { - "index": 307, - "description": "Watt-VAr Reference Power Input. Power measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "minimum": 0, - "data_object": "TotW", - "name": "MMXU.TotW.AI307" - }, - { - "index": 308, - "description": "Watt-VAr Curve Index. Index of the Watt-VAr curve that should be used by the mode.", - "data_type": "AI", - "common_data_class": "CSG", - "ln_class": "DWVR", - "minimum": 0, - "data_object": "WVArCrv", - "name": "DWVR.WVArCrv.AI308" - }, - { - "index": 309, - "description": "Watt-VAr Ramp Up Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DWVR.OpnLoopMax.AI309" - }, - { - "index": 310, - "description": "Watt-VAr Ramp Down Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DWVR", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DWVR.OpnLoopMax.AI310" - }, - { - "index": 311, - "description": "Watt-VAr Attempted Output. VAr output that the mode is attempting to achieve based on the Watt input and selected curve.", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "DWVR", - "units": "VARs", - "data_object": "ReqVAr", - "name": "DWVR.ReqVAr.AI311" - }, - { - "index": 312, - "description": "Power Factor Correction Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPFC", - "minimum": 0, - "data_object": "ModPrio", - "name": "DPFC.ModPrio.AI312" - }, - { - "index": 313, - "description": "Power Factor Correction Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPFC", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DPFC.WinTms.AI313" - }, - { - "index": 314, - "description": "Power Factor Correction Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DPFC", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DPFC.RmpTms.AI314" - }, - { - "index": 315, - "description": "Power Factor Correction Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPFC", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DPFC.RvrtTms.AI315" - }, - { - "index": 316, - "description": "Power Factor Correction Signal Meter ID", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DPFC", - "minimum": 0, - "data_object": "EcpRef", - "name": "DPFC.EcpRef.AI316" - }, - { - "index": 317, - "description": "Power Factor Correction Reference Power Factor Input. Power factor measurement read from the meter and used as an input to the mode.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "MMXU", - "units": "None", - "minimum": -100, - "data_object": "TotPF", - "name": "MMXU.TotPF.AI317" - }, - { - "index": 318, - "description": "Power Factor Correction Average PF Target", - "data_type": "AI", - "common_data_class": "ASG", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DPFC", - "units": "None", - "minimum": -100, - "data_object": "PFTrg", - "name": "DPFC.PFTrg.AI318" - }, - { - "index": 319, - "description": "Power Factor Correction Lower PF Limit", - "data_type": "AI", - "common_data_class": "Int", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DPFC", - "units": "None", - "minimum": -100, - "data_object": "PFCorRef.rangeC", - "name": "DPFC.PFCorRef.rangeC.AI319" - }, - { - "index": 320, - "description": "Power Factor Correction Upper PF Limit", - "data_type": "AI", - "common_data_class": "Int", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "DPFC", - "units": "None", - "minimum": -100, - "data_object": "PFCorRef.rangeC", - "name": "DPFC.PFCorRef.rangeC.AI320" - }, - { - "index": 321, - "description": "Pricing Mode Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPRG", - "minimum": 0, - "data_object": "ModPrio", - "name": "DPRG.ModPrio.AI321" - }, - { - "index": 322, - "description": "Pricing Mode Enabling Time Window", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "WinTms", - "name": "DPRG.WinTms.AI322" - }, - { - "index": 323, - "description": "Pricing Mode Enabling Ramp Time", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "RmpTms", - "name": "DPRG.RmpTms.AI323" - }, - { - "index": 324, - "description": "Pricing Mode Reversion Timeout Period", - "data_type": "AI", - "common_data_class": "ASG", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "RvrtTms", - "name": "DPRG.RvrtTms.AI324" - }, - { - "index": 325, - "description": "Pricing Mode Setpoint: Hundredths of local currency per Kilowatt-Hr.", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.01, - "ln_class": "DPRG", - "units": "100ths of local currency", - "data_object": "PrcRef", - "name": "DPRG.PrcRef.AI325" - }, - { - "index": 326, - "description": "Pricing Mode Ramp Up Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DPRG.OpnLoopMax.AI326" - }, - { - "index": 327, - "description": "Pricing Mode Ramp Down Time Constant", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "DPRG", - "units": "Seconds", - "minimum": 0, - "data_object": "OpnLoopMax", - "name": "DPRG.OpnLoopMax.AI327" - }, - { - "index": 328, - "description": "Curve Edit Selector Index of the curve which is currently being viewed and/or changed", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "DGSM", - "minimum": 1, - "data_object": "InCrv", - "name": "DGSMn.InCrv.AI328", - "type": "selector_block", - "selector_block_start": 328, - "selector_block_end": 532 - - }, - { - "index": 329, - "description": "Curve Mode Type", - "data_type": "AI", - "common_data_class": "ENG", - "maximum": 20, - "ln_class": "DGSM", - "units": "None (list)", - "minimum": 0, - "data_object": "ModTyp", - "allowed_values": { - "0": "Curve is not defined", - "1": "None, dimensionless", - "2": "Volt-Var modes VV11-VV12", - "3": "Frequency-Watt mode FW22", - "4": "Watt-VAr mode WP42", - "5": "Voltage-Watt modes VW51-VW52", - "6": "Remain Connected", - "7": "Temperature mode", - "8": "Pricing signal mode", - "9": "HVRT Must Trip", - "10": "HVRT Momentary Cessation", - "11": "LVRT Must Trip", - "12": "LVRT Momentary Cessation", - "13": "HFRT Must Trip", - "14": "HFRT Momentary Cessation", - "15": "LFRT Must Trip", - "16": "LFRT Momentary Cessation" - }, - "type": "enumerated", - "name": "DGSMn.ModTyp.AI329" - }, - { - "index": 330, - "description": "Curve Number of Points", - "data_type": "AI", - "common_data_class": "CSG", - "maximum": 100, - "ln_class": "FMAR", - "minimum": 0, - "data_object": "PairArr.NumPts", - "name": "FMARn.PairArr.NumPts.AI330" - }, - { - "index": 331, - "description": "Independent (X-Value) Units for Curve", - "data_type": "AI", - "common_data_class": "ENG", - "maximum": 255, - "ln_class": "FMAR", - "units": "None (list)", - "allowed_values": { - "0": "Curve is not defined", - "1": "Not applicable / Unknown", - "4": "Time", - "23": "Celsius Temperature", - "29": "Voltage", - "33": "Frequency", - "38": "Watts", - "100": "Price in hundredths of local currency", - "129": "Percent Voltage", - "133": "Percent Frequency", - "138": "Percent Watts", - "233": "Frequency Deviation" - }, - "type": "enumerated", - "minimum": 0, - "data_object": "IndpUnits", - "name": "FMARn.IndpUnits.AI331" - }, - { - "index": 332, - "description": "Dependent (Y-Value) Units for Curve", - "data_type": "AI", - "common_data_class": "ENG", - "maximum": 255, - "ln_class": "FMAR", - "units": "None (list)", - "minimum": 0, - "data_object": "DepRef", - "allowed_values": { - "0": "Curve is not defined", - "1": "Not applicable / unknown", - "2": "VArs as percent of max VArs (VARMax)", - "3": "VArs as percent of max available VArs (VArAval)", - "4": "Vars as percent of max Watts (Wmax) - not used", - "5": "Watts as percent of max Watts (Wmax)", - "6": "Watts as percent of frozen active power (DeptSnptRef)", - "7": "Power Factor in EEI notation", - "8": "Volts as a percent of the nominal voltage (VRef)", - "9": "Frequency as a percentage of the Nominal Grid Frequency (ECPNomHz)" - }, - "type": "enumerated", - "name": "FMARn.DepRef.AI332" - }, - { - "index": 333, - "description": "Curve X-Value and Y-Value pairs for curve points 1 - 100", - "data_type": "AI", - "common_data_class": "CSG", - "ln_class": "FMAR", - "units": "Varies", - "data_object": "PairArr.CrvPts", - "name": "FMARn.PairArr.CrvPts.AI333", - "type": "array", - "array_times_repeated": 100, - "array_points": [ - { - "name": "FMARn.PairArr.CrvPts.AI333.xVal" - }, - { - "name": "FMARn.PairArr.CrvPts.AI333.yVal" - } - ] - }, - { - "index": 533, - "description": "System Meter Type of Connection Point", - "data_type": "AI", - "common_data_class": "ENS", - "maximum": 99, - "ln_class": "DECP", - "units": "None (list)", - "minimum": 0, - "data_object": "EcpConnType", - "event_class": 3, - "allowed_values": { - "0": "unknown", - "1": "DER to local EPS", - "2": "Internal to DER", - "3": "local EPS with load to area EPS (PCC with load)", - "4": "local EPS w/o load to area EPS (PCC without load)", - "5": "Load to local EPS", - "6": "External to DER beyond the PCC", - "7": "External to DER within the local EPS", - "8": "Auxiliary DER Load", - "9": "Group of DERs to the area EPS", - "99": "Other" - }, - "type": "enumerated", - "name": "DECP.EcpConnType.AI533" - }, - { - "index": 534, - "description": "System Meter Type of Circuit Phases", - "data_type": "AI", - "common_data_class": "ENS", - "maximum": 8, - "ln_class": "DECP", - "units": "None (list)", - "minimum": 0, - "data_object": "PhsConnTyp", - "event_class": 3, - "allowed_values": { - "0": "unknown", - "1": "Single phase", - "2": "Split phase", - "3": "2-phase", - "4": "3-phase delta", - "5": "3-phase wye", - "6": "3-phase wye grounded", - "7": "3-phase / 3-wire (inverter type)", - "8": "3-phase / 4-wire (inverter type)" - }, - "type": "enumerated", - "name": "DECP.PhsConnTyp.AI534" - }, - { - "index": 535, - "description": "System Meter Apparent Power Calculation Method. Calculation method for total apparent power calculation.", - "data_type": "AI", - "common_data_class": "ENG", - "maximum": 2, - "ln_class": "MMXU", - "units": "None (list)", - "minimum": 0, - "data_object": "ClcTotVA", - "allowed_values": { - "0": "unknown", - "1": "vector", - "2": "arithmetic" - }, - "type": "enumerated", - "name": "MMXU.ClcTotVA.AI535" - }, - { - "index": 536, - "description": "System Meter Frequency", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.001, - "maximum": 70000, - "ln_class": "MMXU", - "units": "Hz", - "minimum": 0, - "data_object": "Hz", - "event_class": 3, - "name": "MMXU.Hz.AI536" - }, - { - "index": 537, - "description": "System Meter Active Power", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "data_object": "TotW", - "event_class": 3, - "name": "MMXU.TotW.AI537" - }, - { - "index": 538, - "description": "System Meter Active Power A", - "data_type": "AI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "units": "Watts", - "data_object": "W.phsA.mag", - "event_class": 1, - "name": "MMXU.W.phsA.mag.AI538" - }, - { - "index": 539, - "description": "System Meter Active Power B", - "data_type": "AI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "units": "Watts", - "data_object": "W.phsB.mag", - "event_class": 1, - "name": "MMXU.W.phsB.mag.AI539" - }, - { - "index": 540, - "description": "System Meter Active Power C", - "data_type": "AI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "units": "Watts", - "data_object": "W.phsC.mag", - "event_class": 1, - "name": "MMXU.W.phsC.mag.AI540" - }, - { - "index": 541, - "description": "System Meter Reactive Power", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "VARs", - "data_object": "TotVAr", - "event_class": 3, - "name": "MMXU.TotVAr.AI541" - }, - { - "index": 542, - "description": "System Meter Reactive Power A", - "data_type": "AI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "units": "VAr", - "data_object": "VAr.phsA.mag", - "event_class": 1, - "name": "MMXU.VAr.phsA.mag.AI542" - }, - { - "index": 543, - "description": "System Meter Reactive Power B", - "data_type": "AI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "units": "VAr", - "data_object": "VAr.phsB.mag", - "event_class": 1, - "name": "MMXU.VAr.phsB.mag.AI543" - }, - { - "index": 544, - "description": "System Meter Reactive Power C", - "data_type": "AI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "units": "VAr", - "data_object": "VAr.phsC.mag", - "event_class": 1, - "name": "MMXU.VAr.phsC.mag.AI544" - }, - { - "index": 545, - "description": "System Meter Power Factor", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "MMXU", - "units": "None", - "minimum": -100, - "data_object": "TotPF", - "event_class": 3, - "name": "MMXU.TotPF.AI545" - }, - { - "index": 546, - "description": "System Meter Apparent Power", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "VA", - "data_object": "TotVA", - "event_class": 3, - "name": "MMXU.TotVA.AI546" - }, - { - "index": 547, - "description": "System Meter Phase A Volts", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsA.mag", - "event_class": 3, - "name": "MMXU.PhV.phsA.mag.AI547" - }, - { - "index": 548, - "description": "System Meter Phase A Angle", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "maximum": 3600, - "ln_class": "MMXU", - "units": "Degrees", - "minimum": 0, - "data_object": "PhV.phsA.ang", - "event_class": 3, - "name": "MMXU.PhV.phsA.ang.AI548" - }, - { - "index": 549, - "description": "System Meter Phase B Volts", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsB.mag", - "event_class": 3, - "name": "MMXU.PhV.phsB.mag.AI549" - }, - { - "index": 550, - "description": "System Meter Phase B Angle", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "maximum": 3600, - "ln_class": "MMXU", - "units": "Degrees", - "minimum": 0, - "data_object": "PhV.phsB.ang", - "event_class": 3, - "name": "MMXU.PhV.phsB.ang.AI550" - }, - { - "index": 551, - "description": "System Meter Phase C Volts", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsC.mag", - "event_class": 3, - "name": "MMXU.PhV.phsC.mag.AI551" - }, - { - "index": 552, - "description": "System Meter Phase C Angle", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "maximum": 3600, - "ln_class": "MMXU", - "units": "Degrees", - "minimum": 0, - "data_object": "PhV.phsC.ang", - "event_class": 3, - "name": "MMXU.PhV.phsC.ang.AI552" - }, - { - "index": 553, - "description": "System Meter Average Line to Line Voltage", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "AvPPVPhs", - "event_class": 1, - "name": "MMXU.AvPPVPhs.AI553" - }, - { - "index": 554, - "description": "System Meter Current A", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Amps", - "data_object": "A.phsA.mag", - "event_class": 1, - "name": "MMXU.A.phsA.mag.AI554" - }, - { - "index": 555, - "description": "System Meter Current B", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Amps", - "data_object": "A.phsB.mag", - "event_class": 1, - "name": "MMXU.A.phsB.mag.AI555" - }, - { - "index": 556, - "description": "System Meter Current C", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Amps", - "data_object": "A.phsC.mag", - "event_class": 1, - "name": "MMXU.A.phsC.mag.AI556" - }, - { - "index": 557, - "description": "System Meter Active Power - High Threshold", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "minimum": 0, - "data_object": "TotW.rangeC.hLim", - "event_class": 3, - "name": "MMXU.TotW.rangeC.hLim.AI557" - }, - { - "index": 558, - "description": "System Meter Active Power - Low Threshold", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "Watts", - "minimum": 0, - "data_object": "TotW.rangeC.lLim", - "event_class": 3, - "name": "MMXU.TotW.rangeC.lLim.AI558" - }, - { - "index": 559, - "description": "System Meter Reactive Power - High Threshold", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "VARs", - "minimum": 0, - "data_object": "TotVAr.rangeC.hLim", - "event_class": 3, - "name": "MMXU.TotVAr.rangeC.hLim.AI559" - }, - { - "index": 560, - "description": "System Meter Reactive Power - Low Threshold", - "data_type": "AI", - "common_data_class": "MV", - "ln_class": "MMXU", - "units": "VARs", - "minimum": 0, - "data_object": "TotVAr.rangeC.lLim", - "event_class": 3, - "name": "MMXU.TotVAr.rangeC.lLim.AI560" - }, - { - "index": 561, - "description": "System Meter Power Factor - High Threshold", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "MMXU", - "units": "None", - "minimum": -100, - "data_object": "TotPF.rangeC.hLim", - "event_class": 3, - "name": "MMXU.TotPF.rangeC.hLim.AI561" - }, - { - "index": 562, - "description": "System Meter Power Factor - Low Threshold", - "data_type": "AI", - "common_data_class": "MV", - "scaling_multiplier": 0.01, - "maximum": 100, - "ln_class": "MMXU", - "units": "None", - "minimum": -100, - "data_object": "TotPF.rangeC.lLim", - "event_class": 3, - "name": "MMXU.TotPF.rangeC.lLim.AI562" - }, - { - "index": 563, - "description": "System Meter Phase A Volts - High Threshold", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsA.rangeC.hLim", - "event_class": 3, - "name": "MMXU.PhV.phsA.rangeC.hLim.AI563" - }, - { - "index": 564, - "description": "System Meter Phase A Volts - Low Threshold", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsA.rangeC.lLim", - "event_class": 3, - "name": "MMXU.PhV.phsA.rangeC.lLim.AI564" - }, - { - "index": 565, - "description": "System Meter Phase B Volts - High Threshold", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsB.rangeC.hLim", - "event_class": 3, - "name": "MMXU.PhV.phsB.rangeC.hLim.AI565" - }, - { - "index": 566, - "description": "System Meter Phase B Volts - Low Threshold", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsB.rangeC.lLim", - "event_class": 3, - "name": "MMXU.PhV.phsB.rangeC.lLim.AI566" - }, - { - "index": 567, - "description": "System Meter Phase C Volts - High Threshold", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsC.rangeC.hLim", - "event_class": 3, - "name": "MMXU.PhV.phsC.rangeC.hLim.AI567" - }, - { - "index": 568, - "description": "System Meter Phase C Volts - Low Threshold", - "data_type": "AI", - "common_data_class": "WYE", - "scaling_multiplier": 0.1, - "ln_class": "MMXU", - "units": "Volts", - "minimum": 0, - "data_object": "PhV.phsC.rangeC.lLim", - "event_class": 3, - "name": "MMXU.PhV.phsC.rangeC.lLim.AI568" - }, - { - "index": 569, - "description": "Running Schedule Index. Index of the highest priority schedule that is currently running or 0 if no schedule is currently running.", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "FSCC", - "minimum": 0, - "data_object": "ActSchdRef", - "name": "FSCC1.ActSchdRef.AI569" - }, - { - "index": 570, - "description": "Schedule to Edit Selector", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "FSCC", - "minimum": 0, - "data_object": "Schd", - "event_class": 3, - "name": "FSCC.Schd.AI570", - "type": "selector_block", - "selector_block_start": 570, - "selector_block_end": 780 - - }, - { - "index": 571, - "description": "Selected Schedule Identity", - "data_type": "AI", - "common_data_class": "ORG", - "ln_class": "FSCC", - "minimum": 0, - "data_object": "Schd", - "event_class": 3, - "name": "FSCC.Schd.AI571" - }, - { - "index": 572, - "description": "Selected Schedule Priority. Priority of the schedule relative to other running schedules. Lower values have higher priority over higher values.", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "FSCH", - "minimum": 1, - "data_object": "SchdPrio", - "event_class": 3, - "name": "FSCH.SchdPrio.AI572" - }, - { - "index": 573, - "description": "Selected Schedule Type.", - "data_type": "AI", - "maximum": 21, - "minimum": 0, - "units": "None (list)", - "event_class": 3, - "allowed_values": { - "1": "Low/High Voltage Ride-Through Hi Must Trip", - "2": "Low/High Voltage Ride-Through Low Must Trip", - "3": "Low/High Voltage Ride-Through Hi Momentary", - "4": "Low/High Voltage Ride-Through Lo Momentary", - "5": "Low/High Frequency Ride-Through Hi Must Trip", - "6": "Low/High Frequency Ride-Through Lo Must Trip", - "7": "Low/High Frequency Ride-Through Hi Momentary", - "8": "Low/High Frequency Ride-Through Low Momentary", - "9": "Dynamic Reactive Current Support - On/Off", - "10": "Dynamic Volt-Watt - On/Off", - "11": "Frequency-Watt - On/Off", - "12": "Active Power Limit - Charging", - "13": "Active Power Limit - Generating", - "14": "Charge/Discharge - Percent of Maximum", - "15": "Coordinated Charge/Discharge - SOC Target", - "16": "Active Power Response #1 - On/Off", - "17": "Active Power Response #2 - On/Off", - "18": "Active Power Response #3 - On/Off", - "19": "AGC Watts", - "20": "Active Power Smoothing - On/Off", - "21": "Volt-Watt Curve Index", - "22": "Frequency-Watt Curve Curve Index", - "23": "Frequency-Watt Curve High Hysteresis", - "24": "Frequency-Watt Curve Low Hysteresis", - "25": "Constant VArs - Percent of Maximum", - "26": "Fixed Power Factor - Power Factor", - "27": "Volt-VAr Curve Index", - "28": "Watt-VAr Curve Index", - "29": "Power Factor Correction - On/Off", - "30": "Reserved - For pricing mode" - }, - "type": "enumerated", - "name": "AI573" - }, - { - "index": 574, - "description": "Selected Schedule Start Date. Number of days since January 1, 1970, UTC.", - "data_type": "AI", - "common_data_class": "TSG", - "ln_class": "FSCH", - "units": "Days", - "minimum": 0, - "data_object": "StrTm", - "event_class": 3, - "name": "FSCH.StrTm.AI574" - }, - { - "index": 575, - "description": "Selected Schedule Start Time. Milliseconds since the start of Schedule Start Date.", - "data_type": "AI", - "common_data_class": "TSG", - "maximum": 86400000, - "ln_class": "FSCH", - "units": "Milli-seconds", - "minimum": 0, - "data_object": "StrTm", - "event_class": 3, - "name": "FSCH.StrTm.AI575" - }, - { - "index": 576, - "description": "Selected Schedule Repeat Interval. Interval between actions after the initial occurrence. A zero value means the schedule is not repeated.", - "data_type": "AI", - "common_data_class": "TCS", - "ln_class": "FSCH", - "units": "Varies", - "minimum": 0, - "data_object": "NxtStrTm", - "event_class": 3, - "name": "FSCH.NxtStrTm.AI576" - }, - { - "index": 577, - "description": "Selected Schedule Repeat Interval Units", - "data_type": "AI", - "common_data_class": "SPG", - "maximum": 8, - "ln_class": "FSCH", - "minimum": 0, - "data_object": "SchdReuse", - "event_class": 3, - "allowed_values": { - "0": "No Repeat", - "1": "sec", - "2": "Minutes", - "3": "Hours", - "4": "Days", - "5": "Weeks", - "6": "Months", - "7": "Months on Same Day of Week", - "8": "Months on Same Day of Week from End" - }, - "type": "enumerated", - "name": "FSCH.SchdReuse.AI577" - }, - { - "index": 578, - "description": "Selected Schedule Validation Status", - "data_type": "AI", - "common_data_class": "ENSScheduleState", - "maximum": 4, - "ln_class": "FSCH", - "minimum": 0, - "data_object": "SchdSt", - "event_class": 3, - "name": "FSCH1.SchdSt.AI578" - }, - { - "index": 579, - "description": "Selected Schedule Status", - "data_type": "AI", - "common_data_class": "ENSScheduleState", - "maximum": 4, - "ln_class": "FSCH", - "units": "None (list)", - "minimum": 0, - "data_object": "SchdSt", - "allowed_values": { - "0": "unknown", - "1": "Not available", - "2": "Inactive", - "3": "Ready-to-Run", - "4": "Running" - }, - "type": "enumerated", - "name": "FSCH1.SchdSt.AI579" - }, - { - "index": 580, - "description": "Selected Schedule Number of Points", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "FSCH", - "minimum": 0, - "data_object": "NumEntr", - "event_class": 3, - "name": "FSCH.NumEntr.AI580" - }, - { - "index": 581, - "description": "Select schedule time offset and value pairs for points 1 - 100", - "data_type": "AI", - "minimum": 0, - "name": "FSCHn.SchdEntr.AI581", - "type": "array", - "array_times_repeated": 100, - "array_points": [ - { - "name": "FSCHn.SchdEntr.AI581.time", - "units": "Seconds" - }, - { - "name": "FSCHn.SchdEntr.AI581.val", - "ln_class": "FSCH", - "data_object": "SchdEntr" - } - ] - }, - { - "index": 781, - "description": "Schedule 1 Status", - "data_type": "AI", - "common_data_class": "ENSScheduleState", - "maximum": 4, - "ln_class": "FSCH", - "units": "None (list)", - "minimum": 0, - "data_object": "SchdSt", - "allowed_values": { - "0": "unknown", - "1": "Not available", - "2": "Inactive", - "3": "Ready-to-Run", - "4": "Running" - }, - "type": "enumerated", - "name": "FSCH1.SchdSt.AI781" - }, - { - "index": 782, - "description": "Schedule 1 Priority", - "data_type": "AI", - "common_data_class": "ING", - "ln_class": "FSCH", - "minimum": 1, - "data_object": "SchdPrio", - "name": "FSCH.SchdPrio.AI782" - }, - { - "index": 783, - "description": "Schedule 1 Active Time Value. This is the index of the time value entry the schedule is currently running. First entry is 1. Zero if the schedule is not running.", - "data_type": "AI", - "common_data_class": "INS", - "maximum": 10, - "ln_class": "FSCH", - "minimum": 0, - "data_object": "ActStrTm", - "name": "FSCH1.ActStrTm.AI783" - }, - { - "category": "alarm", - "index": 0, - "description": "System Communication Error", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "LCCH", - "data_object": "ChLiv", - "allowed_values": { - "0": "Normal", - "1": "Alarm: Communications error exists in the ESS" - }, - "event_class": 1, - "name": "LCCH.ChLiv.BI0" - }, - { - "category": "alarm", - "index": 1, - "description": "System Has Priority 1 Alarms", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "CALH", - "data_object": "GrAlm", - "allowed_values": { - "0": "No P1 Alarms Active", - "1": "Alarm: One or More P1 Alarms Active" - }, - "event_class": 1, - "name": "CALH.GrAlm.BI1" - }, - { - "category": "alarm", - "index": 2, - "description": "System Has Priority 2 Alarms", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "CALH", - "data_object": "GrWrn", - "allowed_values": { - "0": "No P2 Alarms Active", - "1": "Alarm: One or More P2 Alarms Active" - }, - "event_class": 1, - "name": "CALH.GrWrn.BI2" - }, - { - "category": "alarm", - "index": 3, - "description": "System Has Priority 3 Alarms", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "CALH", - "data_object": "GrInd", - "allowed_values": { - "0": "No P3 Alarms Active", - "1": "Alarm: One or More P3 Alarms Active" - }, - "event_class": 1, - "name": "CALH.GrInd.BI3" - }, - { - "category": "alarm", - "index": 4, - "description": "Storage State of Charge at Maximum. Maximum Usable State of Charge Reached.", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "DSTO", - "data_object": "SocHiWrn", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "DSTO.SocHiWrn.BI4" - }, - { - "category": "alarm", - "index": 5, - "description": "Storage State of Charge is Too High. Maximum Reserve Percentage (of usable capacity) reached.", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "DSTO", - "data_object": "SocHiAlm", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "DSTO.SocHiAlm.BI5" - }, - { - "category": "alarm", - "index": 6, - "description": "Storage State of Charge is Too Low. Minimum Reserve Percentage (of usable capacity) reached.", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "DSTO", - "data_object": "SocLoAlm", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "DSTO.SocLoAlm.BI6" - }, - { - "category": "alarm", - "index": 7, - "description": "Storage State of Charge is Depleted. Minimum Usable State of Charge Reached.", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "DSTO", - "data_object": "SohLoAlm", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "DSTO.SohLoAlm.BI7" - }, - { - "category": "alarm", - "index": 8, - "description": "Storage Internal Temperature is Too High", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "DBAT", - "data_object": "IntnTmpHiAlm", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "DBAT.IntnTmpHiAlm.BI8" - }, - { - "category": "alarm", - "index": 9, - "description": "Storage External (Ambient) Temperature is Too High", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "DBAT", - "data_object": "ExtTmpHiAlm", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "DBAT.ExtTmpHiAlm.BI9" - }, - { - "index": 10, - "description": "System Is In Local State. System has been locked by a local operator which prevents other operators from executing commands. Note: Local State is also sometimes referred to as Maintenance State. Local State overrides Lockout State.", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.disconnected and in maintenance", - "allowed_values": { - "0": "System not in local state", - "1": "System in local state" - }, - "name": "DSTO.DEROpSt.disconnectedandinmaintenance.BI10" - }, - { - "index": 11, - "description": "System Is In Lockout State. System has been locked by an operator such that other operators may not execute commands. Lockout State is also sometimes referred to as Blocked State.", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.disconnected and blocked", - "allowed_values": { - "0": "System not locked out", - "1": "System locked out" - }, - "name": "DSTO.DEROpSt.disconnectedandblocked.BI11" - }, - { - "index": 12, - "description": "System Is Starting Up. Set to 1 when a BO \"System Initiate Start-up Sequence\" command has been received.", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.starting and synchronizing", - "allowed_values": { - "0": "Not Starting Up", - "1": "Start command has been received." - }, - "name": "DSTO.DEROpSt.startingandsynchronizing.BI12" - }, - { - "index": 13, - "description": "System Is Stopping. Set to 1 when an B0 \"System Execute Stop\" command has been received.", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.stopping", - "allowed_values": { - "0": "Not Stopping", - "1": "Emergency stop command has been received." - }, - "name": "DSTO.DEROpSt.stopping.BI13" - }, - { - "index": 14, - "description": "System is Started (Return to Service). If any of the DER Units are started,then true. DER Units in the maintenance operational state are excluded.", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.connected and idle", - "allowed_values": { - "0": "Null", - "1": "Started" - }, - "name": "DSTO.DEROpSt.connectedandidle.BI14" - }, - { - "index": 15, - "description": "System is Stopped (Cease to Energize). If all of the DER Units are stopped, then true. DER Units in the maintenance operational state are excluded.", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.ceased to energize", - "allowed_values": { - "0": "Null", - "1": "Stopped" - }, - "name": "DSTO.DEROpSt.ceasedtoenergize.BI15" - }, - { - "index": 16, - "description": "System Permission to Start Status", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DSTO", - "data_object": "PrmConn", - "allowed_values": { - "0": "Start Permission Not Granted", - "1": "Start Permission Granted" - }, - "name": "DSTO.PrmConn.BI16" - }, - { - "index": 17, - "description": "System Permission to Stop Status", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DSTO", - "data_object": "PrmDscon", - "allowed_values": { - "0": "Stop Permission Not Granted", - "1": "Stop Permission Granted" - }, - "name": "DSTO.PrmDscon.BI17" - }, - { - "index": 18, - "description": "DER is Connected and Idle", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.connected and idle", - "allowed_values": { - "0": "Null", - "1": "Idle-Connected" - }, - "name": "DSTO.DEROpSt.connectedandidle.BI18" - }, - { - "index": 19, - "description": "DER is Connected and Generating", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.connected and generating", - "allowed_values": { - "0": "Null", - "1": "On-Connected" - }, - "name": "DSTO.DEROpSt.connectedandgenerating.BI19" - }, - { - "index": 20, - "description": "DER is Connected and Charging", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.connected and consuming", - "allowed_values": { - "0": "Null", - "1": "On-Charging-Connected" - }, - "name": "DSTO.DEROpSt.connectedandconsuming.BI20" - }, - { - "index": 21, - "description": "DER is Off but Available to Start", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.disconnected and available", - "allowed_values": { - "0": "Null", - "1": "Off-Available" - }, - "name": "DSTO.DEROpSt.disconnectedandavailable.BI21" - }, - { - "index": 22, - "description": "DER is Off and Not Available to Start", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.disconnected and stand-by", - "allowed_values": { - "0": "Null", - "1": "Off-Not-Available" - }, - "name": "DSTO.DEROpSt.disconnectedandstand-by.BI22" - }, - { - "index": 23, - "description": "DER Connect/Disconnect Switch Closed Status", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.off", - "allowed_values": { - "0": "Open", - "1": "Closed" - }, - "name": "DSTO.DEROpSt.off.BI23" - }, - { - "index": 24, - "description": "DER Connect/Disconnect Switch Movement Status", - "data_type": "BI", - "common_data_class": "DPC", - "ln_class": "CSWI", - "data_object": "Pos", - "allowed_values": { - "0": "Not Moving", - "1": "Moving" - }, - "name": "CSWI.Pos.BI24" - }, - { - "index": 25, - "description": "Islanded Mode. Determines how the DER behaves when in an Islanded configuration.", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DSTO", - "data_object": "IsldCtlFol", - "allowed_values": { - "0": "Isochronous Mode. DER attempts to control voltage and frequency independent of configured curves and settings up to the limits of the machine's capabilities in order to achieve the AO Reference Voltage and AO nominal frequency.", - "1": "Droop Mode. DER acts as a follower using Volt/VAR and Freq/Watt curves." - }, - "event_class": 3, - "name": "DSTO.IsldCtlFol.BI25" - }, - { - "index": 26, - "description": "Sensed Grid Config Detection Enabled. If Enabled, the DER may independently change its Active Settings Group based on locally observed grid conditions.", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DECP", - "data_object": "ECPIsldAuto", - "allowed_values": { - "0": "No Autonomous Detection.", - "1": "Autonomous Detection. Inverter's Active Settings Group may differ from the Requested Settings Group" - }, - "event_class": 3, - "name": "DECP.ECPIsldAuto.BI26" - }, - { - "index": 27, - "description": "Storage Capacity Units. Determines whether energy storage values are expressed in units of Amp-hrs or Watt-hrs.", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DSTO", - "data_object": "AGra", - "allowed_values": { - "0": "Amp-hrs (default)", - "1": "Watt-hrs" - }, - "event_class": 3, - "name": "DSTO.AGra.BI27" - }, - { - "index": 28, - "description": "Time Constant Mode. Indicates whether Time Constant Ramp parameters are interpreted as Open Loop Response times or 3Tau values.", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DSTO", - "data_object": "OpnLoopTau", - "allowed_values": { - "0": "Open Loop Response Time", - "1": "3Tau Value" - }, - "event_class": 3, - "name": "DSTO.OpnLoopTau.BI28" - }, - { - "index": 29, - "description": "Power Factor Excitation When Discharging/Generating", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DFPF", - "data_object": "PFGnExtSet", - "allowed_values": { - "0": "Injecting VARs - Q1", - "1": "Absorbing VARs - Q4" - }, - "name": "DFPF.PFGnExtSet.BI29" - }, - { - "index": 30, - "description": "Power Factor Excitation When Charging", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DFPF", - "data_object": "PFLodExtSet", - "allowed_values": { - "0": "Injecting VARs - Q2", - "1": "Absorbing VARs - Q3" - }, - "name": "DFPF.PFLodExtSet.BI30" - }, - { - "index": 31, - "description": "Supports Low/High Voltage Ride-Through Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DHVT", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DHVT.BI31" - }, - { - "index": 32, - "description": "Supports Low/High Frequency Ride-Through Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DHFT", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DHFT.BI32" - }, - { - "index": 33, - "description": "Supports Dynamic Reactive Current Support Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DRGS", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DRGS.BI33" - }, - { - "index": 34, - "description": "Supports Dynamic Volt-Watt Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DVWD", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DVWD.BI34" - }, - { - "index": 35, - "description": "Supports Frequency-Watt Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DHFW", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DHFW.BI35" - }, - { - "index": 36, - "description": "Supports Active Power Limit Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DWLM", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DWLM.BI36" - }, - { - "index": 37, - "description": "Supports Charge/Discharge Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DWGC", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DWGC.BI37" - }, - { - "index": 38, - "description": "Supports Coordinated Charge/Discharge Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DTCD", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DTCD.BI38" - }, - { - "index": 39, - "description": "Supports Active Power Response Mode #1", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DLFL", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DLFL.BI39" - }, - { - "index": 40, - "description": "Supports Active Power Response Mode #2", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DGFL", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DGFL.BI40" - }, - { - "index": 41, - "description": "Supports Active Power Response Mode #3", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DGFL", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DGFL.BI41" - }, - { - "index": 42, - "description": "Supports Automatic Generation Control Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DAGC", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DAGC.BI42" - }, - { - "index": 43, - "description": "Supports Active Power Smoothing Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DWSM", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DWSM.BI43" - }, - { - "index": 44, - "description": "Supports Volt-Watt Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DVWC", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DVWC.BI44" - }, - { - "index": 45, - "description": "Supports Frequency-Watt Curve Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DFWC", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DFWC.BI45" - }, - { - "index": 46, - "description": "Supports Constant VArs Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DVAR", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DVAR.BI46" - }, - { - "index": 47, - "description": "Supports Fixed Power Factor Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DFPF", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DFPF.BI47" - }, - { - "index": 48, - "description": "Supports Volt-VAr Control Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DVVC", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DVVC.BI48" - }, - { - "index": 49, - "description": "Supports Watt-VAr Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DWVR", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DWVR.BI49" - }, - { - "index": 50, - "description": "Supports Power Factor Correction Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DPFC", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DPFC.BI50" - }, - { - "index": 51, - "description": "Supports Pricing Mode", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "DPRG", - "allowed_values": { - "0": "Not Supported", - "1": "Supported" - }, - "event_class": 0, - "name": "DPRG.BI51" - }, - { - "index": 52, - "description": "Overvoltage Disconnect Protection Blocked", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "PTOV", - "data_object": "Blk", - "allowed_values": { - "0": "Not Blocked", - "1": "Blocked (Disabled)" - }, - "event_class": 1, - "name": "PTOV.Blk.BI52" - }, - { - "index": 53, - "description": "Overvoltage Disconnect Protection Started", - "data_type": "BI", - "common_data_class": "ACD", - "ln_class": "PTOV", - "data_object": "Str.general", - "allowed_values": { - "0": "Not Started", - "1": "Started (Evaluating)" - }, - "event_class": 1, - "name": "PTOV.Str.general.BI53" - }, - { - "index": 54, - "description": "Overvoltage Disconnect Protection Operated", - "data_type": "BI", - "common_data_class": "ACT", - "ln_class": "PTOV", - "data_object": "Op.general", - "allowed_values": { - "0": "Not Operated", - "1": "Operated (Disconnected)" - }, - "event_class": 1, - "name": "PTOV.Op.general.BI54" - }, - { - "index": 55, - "description": "Undervoltage Disconnect Protection Blocked", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "PTUV", - "data_object": "Blk", - "allowed_values": { - "0": "Not Blocked", - "1": "Blocked (Disabled)" - }, - "event_class": 1, - "name": "PTUV.Blk.BI55" - }, - { - "index": 56, - "description": "Undervoltage Disconnect Protection Started", - "data_type": "BI", - "common_data_class": "ACD", - "ln_class": "PTUV", - "data_object": "Str.general", - "allowed_values": { - "0": "Not Started", - "1": "Started (Evaluating)" - }, - "event_class": 1, - "name": "PTUV.Str.general.BI56" - }, - { - "index": 57, - "description": "Undervoltage Disconnect Protection Operated", - "data_type": "BI", - "common_data_class": "ACT", - "ln_class": "PTUV", - "data_object": "Op.general", - "allowed_values": { - "0": "Not Operated", - "1": "Operated (Disconnected)" - }, - "event_class": 1, - "name": "PTUV.Op.general.BI57" - }, - { - "index": 58, - "description": "Over Frequency Disconnect Protection Blocked", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "PTOV", - "data_object": "Blk", - "allowed_values": { - "0": "Not Blocked", - "1": "Blocked (Disabled)" - }, - "event_class": 1, - "name": "PTOV.Blk.BI58" - }, - { - "index": 59, - "description": "Over Frequency Disconnect Protection Started", - "data_type": "BI", - "common_data_class": "ACD", - "ln_class": "PTOV", - "data_object": "Str.general", - "allowed_values": { - "0": "Not Started", - "1": "Started (Evaluating)" - }, - "event_class": 1, - "name": "PTOV.Str.general.BI59" - }, - { - "index": 60, - "description": "Over Frequency Disconnect Protection Operated", - "data_type": "BI", - "common_data_class": "ACT", - "ln_class": "PTOV", - "data_object": "Op.general", - "allowed_values": { - "0": "Not Operated", - "1": "Operated (Disconnected)" - }, - "event_class": 1, - "name": "PTOV.Op.general.BI60" - }, - { - "index": 61, - "description": "Under Frequency Disconnect Protection Blocked", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "PTUV", - "data_object": "Blk", - "allowed_values": { - "0": "Not Blocked", - "1": "Blocked (Disabled)" - }, - "event_class": 1, - "name": "PTUV.Blk.BI61" - }, - { - "index": 62, - "description": "Under Frequency Disconnect Protection Started", - "data_type": "BI", - "common_data_class": "ACD", - "ln_class": "PTUV", - "data_object": "Str.general", - "allowed_values": { - "0": "Not Started", - "1": "Started (Evaluating)" - }, - "event_class": 1, - "name": "PTUV.Str.general.BI62" - }, - { - "index": 63, - "description": "Under Frequency Disconnect Protection Operated", - "data_type": "BI", - "common_data_class": "ACT", - "ln_class": "PTUV", - "data_object": "Op.general", - "allowed_values": { - "0": "Not Operated", - "1": "Operated (Disconnected)" - }, - "event_class": 1, - "name": "PTUV.Op.general.BI63" - }, - { - "category": "mode_enable", - "index": 64, - "description": "Operating Mode - Low/High Voltage Ride-Through", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DHVT", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DHVT.ModEna.BI64" - }, - { - "category": "mode_enable", - "index": 65, - "description": "Operating Mode - Low/High Frequency Ride-Through", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DHFT", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DHFT.ModEna.BI65" - }, - { - "category": "mode_enable", - "index": 66, - "description": "Operating Mode - Dynamic Reactive Current Support Enabled", - "data_type": "BI", - "common_data_class": "ENC", - "ln_class": "DRGS", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DRGS.ModEna.BI66" - }, - { - "category": "mode_enable", - "index": 67, - "description": "Operating Mode - Dynamic Volt-Watt Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DVWD", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DVWD.ModEna.BI67" - }, - { - "category": "mode_enable", - "index": 68, - "description": "Operating Mode - Frequency-Watt Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DHFW", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DHFW.ModEna.BI68" - }, - { - "category": "mode_enable", - "index": 69, - "description": "Operating Mode - Active Power Limit Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DWLM", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DWLM.ModEna.BI69" - }, - { - "category": "mode_enable", - "index": 70, - "description": "Operating Mode - Charge/Discharge Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DWGC", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DWGC.ModEna.BI70" - }, - { - "category": "mode_enable", - "index": 71, - "description": "Operating Mode - Coordinated Charge/Discharge Management Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DTCD", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DTCD.ModEna.BI71" - }, - { - "category": "mode_enable", - "index": 72, - "description": "Operating Mode - Active Power Response Mode #1 Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DPKP", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DPKP.ModEna.BI72" - }, - { - "category": "mode_enable", - "index": 73, - "description": "Operating Mode - Active Power Response Mode #2 Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DGFL", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DGFL.ModEna.BI73" - }, - { - "category": "mode_enable", - "index": 74, - "description": "Operating Mode - Active Power Response Mode #3 Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DLFL", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DLFL.ModEna.BI74" - }, - { - "category": "mode_enable", - "index": 75, - "description": "Operating Mode - Automatic Generation Control Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DAGC", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DAGC.ModEna.BI75" - }, - { - "category": "mode_enable", - "index": 76, - "description": "Operating Mode - Active Power Smoothing Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DWSM", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DWSM.ModEna.BI76" - }, - { - "category": "mode_enable", - "index": 77, - "description": "Operating Mode - Volt-Watt Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DVWC", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DVWC.ModEna.BI77" - }, - { - "category": "mode_enable", - "index": 78, - "description": "Operating Mode - Frequency-Watt Curve Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DHFW", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DHFW.ModEna.BI78" - }, - { - "category": "mode_enable", - "index": 79, - "description": "Operating Mode - Constant VArs Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DVAR", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DVAR.ModEna.BI79" - }, - { - "category": "mode_enable", - "index": 80, - "description": "Operating Mode - Fixed Power Factor Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DFPF", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DFPF.ModEna.BI80" - }, - { - "category": "mode_enable", - "index": 81, - "description": "Operating Mode - Volt-VAR Control Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DVVR", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DVVR.ModEna.BI81" - }, - { - "category": "mode_enable", - "index": 82, - "description": "Operating Mode - Watt-VAr Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DWVR", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DWVR.ModEna.BI82" - }, - { - "category": "mode_enable", - "index": 83, - "description": "Operating Mode - Power Factor Correction Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DPFC", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DPFC.ModEna.BI83" - }, - { - "category": "mode_enable", - "index": 84, - "description": "Operating Mode - Pricing Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DPRG", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DPRG.ModEna.BI84" - }, - { - "category": "mode_enable", - "index": 85, - "description": "Operating Mode - Event-Based Reactive Current Support Enabled", - "data_type": "BI", - "common_data_class": "SPC", - "ln_class": "DRGS", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DRGS.ModEna.BI85" - }, - { - "index": 86, - "description": "Frequency-Watt Mode - Use Hysteresis", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DHFW", - "data_object": "HysEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DHFW.HysEna.BI86" - }, - { - "index": 87, - "description": "Frequency-Watt Mode - Snapshot of Power", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DHFW", - "data_object": "SnptEna", - "allowed_values": { - "0": "Not Active", - "1": "Active" - }, - "name": "DHFW.SnptEna.BI87" - }, - { - "index": 88, - "description": "Frequency-Watt Curve Mode - Use Hysteresis", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DLFW", - "data_object": "HysEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DLFW.HysEna.BI88" - }, - { - "index": 89, - "description": "Frequency-Watt Curve Mode - Snapshot of Power", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DLFW", - "data_object": "SnptEna", - "allowed_values": { - "0": "Not Active", - "1": "Active" - }, - "name": "DLFW.SnptEna.BI89" - }, - { - "index": 90, - "description": "Charge/Discharge Mode - Use Ramp Rates. Indicates whether or not Charge/Discharge should use specified ramp rates or ramp times.", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DWGC", - "data_object": "UseRmpRte", - "allowed_values": { - "0": "Use Time Constants", - "1": "Use Ramp Rates" - }, - "name": "DWGC.UseRmpRte.BI90" - }, - { - "index": 91, - "description": "AGC Mode - Use Ramp Rates. Indicates whether or not charge/discharge should use specified ramp rates or ramp times.", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DAGC", - "data_object": "UseRmpRte", - "allowed_values": { - "0": "Use Time Constants", - "1": "Use Ramp Rates" - }, - "name": "DAGC.UseRmpRte.BI91" - }, - { - "index": 92, - "description": "Volt-Watt - Use Ramp Rates and Time Constants", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DVWC", - "data_object": "UseRmpRte", - "allowed_values": { - "0": "Use Time Constants", - "1": "Use Ramp Rates AND Time Constants" - }, - "name": "DVWC.UseRmpRte.BI92" - }, - { - "index": 93, - "description": "Volt-VAr Enable Autonomous Voltage Reference Adjustment", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "DVVR", - "data_object": "VRefAdjEna", - "allowed_values": { - "0": "Disabled", - "1": "Enabled" - }, - "name": "DVVR.VRefAdjEna.BI93" - }, - { - "category": "alarm", - "index": 94, - "description": "System Meter Active Power is Too High", - "data_type": "BI", - "common_data_class": "MV", - "ln_class": "MMXU", - "data_object": "TotW.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.TotW.range.BI94" - }, - { - "category": "alarm", - "index": 95, - "description": "System Meter Active Power is Too Low", - "data_type": "BI", - "common_data_class": "MV", - "ln_class": "MMXU", - "data_object": "TotW.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.TotW.range.BI95" - }, - { - "category": "alarm", - "index": 96, - "description": "System Meter Reactive Power is Too High", - "data_type": "BI", - "common_data_class": "MV", - "ln_class": "MMXU", - "data_object": "TotVAr.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.TotVAr.range.BI96" - }, - { - "category": "alarm", - "index": 97, - "description": "System Meter Reactive Power is Too Low", - "data_type": "BI", - "common_data_class": "MV", - "ln_class": "MMXU", - "data_object": "TotVAr.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.TotVAr.range.BI97" - }, - { - "category": "alarm", - "index": 98, - "description": "System Meter Power Factor is Too High", - "data_type": "BI", - "common_data_class": "MV", - "ln_class": "MMXU", - "data_object": "TotPF.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.TotPF.range.BI98" - }, - { - "category": "alarm", - "index": 99, - "description": "System Meter Power Factor is Too Low", - "data_type": "BI", - "common_data_class": "MV", - "ln_class": "MMXU", - "data_object": "TotPF.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.TotPF.range.BI99" - }, - { - "category": "alarm", - "index": 100, - "description": "System Meter Phase A Voltage is Too High", - "data_type": "BI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "data_object": "PhV.phsA.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.PhV.phsA.range.BI100" - }, - { - "category": "alarm", - "index": 101, - "description": "System Meter Phase A Voltage is Too Low", - "data_type": "BI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "data_object": "PhV.phsA.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.PhV.phsA.range.BI101" - }, - { - "category": "alarm", - "index": 102, - "description": "System Meter Phase B Voltage is Too High", - "data_type": "BI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "data_object": "PhV.phsB.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.PhV.phsB.range.BI102" - }, - { - "category": "alarm", - "index": 103, - "description": "System Meter Phase B Voltage is Too Low", - "data_type": "BI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "data_object": "PhV.phsB.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.PhV.phsB.range.BI103" - }, - { - "category": "alarm", - "index": 104, - "description": "System Meter Phase C Voltage is Too High", - "data_type": "BI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "data_object": "PhV.phsC.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.PhV.phsC.range.BI104" - }, - { - "category": "alarm", - "index": 105, - "description": "System Meter Phase C Voltage is Too Low", - "data_type": "BI", - "common_data_class": "WYE", - "ln_class": "MMXU", - "data_object": "PhV.phsC.range", - "allowed_values": { - "0": "Normal", - "1": "Alarm" - }, - "event_class": 1, - "name": "MMXU.PhV.phsC.range.BI105" - }, - { - "category": "alarm", - "index": 106, - "description": "System Meter Communication Error", - "data_type": "BI", - "common_data_class": "SPS", - "ln_class": "LCCH", - "data_object": "ChLiv", - "allowed_values": { - "0": "Normal: No Active Communications Error", - "1": "Alarm: Active Communications Error" - }, - "event_class": 1, - "name": "LCCH.ChLiv.BI106" - }, - { - "index": 107, - "description": "Selected Curve is Referenced by a Mode", - "data_type": "BI", - "allowed_values": { - "0": "Curve is not Referenced", - "1": "Curve is Referenced" - }, - "event_class": 1, - "name": "BI107" - }, - { - "index": 108, - "description": "Selected Schedule Is Ready", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "FSCH", - "data_object": "SchdSt.3", - "allowed_values": { - "0": "Not Ready", - "1": "Ready" - }, - "name": "FSCH.SchdSt.3.BI108" - }, - { - "index": 109, - "description": "Selected Schedule is Validated", - "data_type": "BI", - "common_data_class": "ENS", - "ln_class": "FSCH", - "data_object": "SchdSt.2", - "allowed_values": { - "0": "Not validated", - "1": "Validated" - }, - "name": "FSCH.SchdSt.2.BI109" - }, - { - "index": 110, - "description": "Selected Schedule Repeat Weekly Sunday", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BI110" - }, - { - "index": 111, - "description": "Selected Schedule Repeat Weekly Monday", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BI111" - }, - { - "index": 112, - "description": "Selected Schedule Repeat Weekly Tuesday", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BI112" - }, - { - "index": 113, - "description": "Selected Schedule Repeat Weekly Wednesday", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BI113" - }, - { - "index": 114, - "description": "Selected Schedule Repeat Weekly Thursday", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BI114" - }, - { - "index": 115, - "description": "Selected Schedule Repeat Weekly Friday", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BI115" - }, - { - "index": 116, - "description": "Selected Schedule Repeat Weekly Saturday", - "data_type": "BI", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BI116" - }, - { - "index": 0, - "description": "System Set Lockout State", - "data_type": "BO", - "common_data_class": "ENS", - "ln_class": "DSTO", - "data_object": "DEROpSt.disconnected and blocked", - "allowed_values": { - "0": "Not Locked Out", - "1": "Lock Out" - }, - "name": "DSTO.DEROpSt.disconnectedandblocked.BO0" - }, - { - "index": 1, - "description": "System Initiate Start-up Sequence (Return to Service). Setting this to 1 does the following: - Sets BI \"System Is Starting Up\" to 1 indicating that the system is starting up. Additional start-up status can be found in AI \"System Start-up Status\". - Instructs all batteries to connect. - Once each battery has reported that it has connect successfully, instructs corresponding DER Unit to start. System can be shut down by executing B0 \"Emergency Stop\" command. This operation is the same as California Rule 21 \"Soft Start\".", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DCTE", - "data_object": "RtnSrvReq", - "allowed_values": { - "0": "No Change", - "1": "Initiate Start-up" - }, - "name": "DCTE.RtnSrvReq.BO1" - }, - { - "index": 2, - "description": "System Execute Stop (Cease to Energize). Setting this to 1 does the following: - Sets BI \"System Is Emergency Stopping\" to 1 indicating that an emergency stop is in progress. - Ensures that any executing operating modes are shut down (disabled). - Ensures that any executing schedules are shut down (disabled). - Instructs all inverters to shut down. - Instructs all batteries to disconnect. System can be started again by executing BO \"Initiate Start-up Sequence\" command.", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DCTE", - "data_object": "CeaEngzReq", - "allowed_values": { - "0": "No Change", - "1": "Stop (Emergency)" - }, - "name": "DCTE.CeaEngzReq.BO2" - }, - { - "index": 3, - "description": "System Permission to Start", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DSTO", - "data_object": "PrmConn", - "allowed_values": { - "0": "DCTE", - "1": "Start Permission Granted" - }, - "name": "DSTO.PrmConn.BO3" - }, - { - "index": 4, - "description": "System Permission to Stop", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DSTO", - "data_object": "PrmDscon", - "allowed_values": { - "0": "DCTE", - "1": "Stop Permission Granted" - }, - "name": "DSTO.PrmDscon.BO4" - }, - { - "index": 5, - "description": "DER Connect/Disconnect Switch", - "data_type": "BO", - "common_data_class": "DPC", - "ln_class": "CSWI", - "data_object": "Pos", - "allowed_values": { - "0": "Open Switch", - "1": "Close Switch" - }, - "name": "CSWI.Pos.BO5" - }, - { - "index": 6, - "description": "Islanded Mode. Determines how the DER behaves when in an Islanded configuration.", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DGEN", - "data_object": "IsldCtlFol", - "allowed_values": { - "0": "Isochronous Mode. DER attempts to control voltage and frequency independent of configured curves and settings up to the limits of the machine's capabilities in order to achieve AO reference voltage and AO nominal frequency.", - "1": "Droop Mode. DER acts as a follower using Volt/VAR and Freq/Watt curves." - }, - "name": "DGEN.IsldCtlFol.BO6" - }, - { - "index": 7, - "description": "Enable Sensed Grid Config Detection. If Enabled, the DER may independently change its Active Settings Group based on locally observed grid conditions.", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DECP", - "data_object": "ECPIsldAuto", - "allowed_values": { - "0": "No Autonomous Detection.", - "1": "Autonomous Detection. Inverter's Active Settings Group may differ from the Requested Settings Group" - }, - "name": "DECP.ECPIsldAuto.BO7" - }, - { - "index": 8, - "description": "Storage Capacity Units. Determines whether the energy storage values are expressed in Amp-hrs or Watt-hrs.", - "data_type": "BO", - "common_data_class": "ASG", - "ln_class": "DSTO", - "data_object": "AGra", - "allowed_values": { - "0": "Amp-hrs (default)", - "1": "Watt-hrs" - }, - "name": "DSTO.AGra.BO8" - }, - { - "index": 9, - "description": "Time Constant Mode. Indicates whether Time Constant Ramp parameters are interpreted as Open Loop Response times or 3Tau values.", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DSTO", - "data_object": "OpnLoopTau", - "allowed_values": { - "0": "Open Loop Response Time", - "1": "3Tau Value" - }, - "name": "DSTO.OpnLoopTau.BO9" - }, - { - "index": 10, - "description": "Power Factor Excitation When Discharging/Generating", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DFPF", - "data_object": "PFGnExtSet", - "allowed_values": { - "0": "Producing VARs - Q1", - "1": "Absorbing VARs - Q4" - }, - "name": "DFPF.PFGnExtSet.BO10" - }, - { - "index": 11, - "description": "Power Factor Excitation When Charging", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DFPF", - "data_object": "PFLodExtSet", - "allowed_values": { - "0": "Producing VARs - Q2", - "1": "Absorbing VARs - Q3" - }, - "name": "DFPF.PFLodExtSet.BO11" - }, - { - "category": "mode_enable", - "index": 12, - "description": "Enable Low/High Voltage Ride-Through Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DHVT", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DHVT.ModEna.BI64", - "name": "DHVT.ModEna.BO12" - }, - { - "category": "mode_enable", - "index": 13, - "description": "Enable Low/High Frequency Ride-Through Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DHFT", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DHFT.ModEna.BI65", - "name": "DHFT.ModEna.BO13" - }, - { - "category": "mode_enable", - "index": 14, - "description": "Enable Dynamic Reactive Current Support Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DRGS", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DRGS.ModEna.BI66", - "name": "DRGS.ModEna.BO14" - }, - { - "category": "mode_enable", - "index": 15, - "description": "Enable Dynamic Volt-Watt Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DVWD", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DVWD.ModEna.BI67", - "name": "DVWD.ModEna.BO15" - }, - { - "category": "mode_enable", - "index": 16, - "description": "Enable Frequency-Watt Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DHFW", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DHFW.ModEna.BI68", - "name": "DHFW.ModEna.BO16" - }, - { - "category": "mode_enable", - "index": 17, - "description": "Enable Active Power Limit Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DWLM", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DWLM.ModEna.BI69", - "name": "DWLM.ModEna.BO17" - }, - { - "category": "mode_enable", - "index": 18, - "description": "Enable Charge/Discharge Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DWGC", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DTCD.ModEna.BI71", - "name": "DWGC.ModEna.BO18" - }, - { - "category": "mode_enable", - "index": 19, - "description": "Enable Coordinated Charge/Discharge Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DTCD", - "action": "publish", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "name": "DTCD.ModEna.BO19" - }, - { - "category": "mode_enable", - "index": 20, - "description": "Enable Active Power Response Mode #1", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DPKP", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DPKP.ModEna.BI72", - "name": "DPKP.ModEna.BO20" - }, - { - "category": "mode_enable", - "index": 21, - "description": "Enable Active Power Response Mode #2", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DGFL", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DGFL.ModEna.BI73", - "name": "DGFL.ModEna.BO21" - }, - { - "category": "mode_enable", - "index": 22, - "description": "Enable Active Power Response Mode #3", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DLFL", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DLFL.ModEna.BI74", - "name": "DLFL.ModEna.BO22" - }, - { - "category": "mode_enable", - "index": 23, - "description": "Enable Automatic Generation Control Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DAGC", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DAGC.ModEna.BI75", - "name": "DAGC.ModEna.BO23" - }, - { - "category": "mode_enable", - "index": 24, - "description": "Enable Active Power Smoothing Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DWSM", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DWSM.ModEna.BI76", - "name": "DWSM.ModEna.BO24" - }, - { - "category": "mode_enable", - "index": 25, - "description": "Enable Volt-Watt Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DVWC", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DVWC.ModEna.BI77", - "name": "DVWC.ModEna.BO25" - }, - { - "category": "mode_enable", - "index": 26, - "description": "Enable Frequency-Watt Curve Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DHFW", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DHFW.ModEna.BI78", - "name": "DHFW.ModEna.BO26" - }, - { - "category": "mode_enable", - "index": 27, - "description": "Enable Constant VArs Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DVAR", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DVAR.ModEna.BI79", - "name": "DVAR.ModEna.BO27" - }, - { - "category": "mode_enable", - "index": 28, - "description": "Enable Fixed Power Factor Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DFPF", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DFPF.BI47", - "name": "DFPF.ModEna.BO28" - }, - { - "category": "mode_enable", - "index": 29, - "description": "Enable Volt-VAR Control Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DVVC", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DVVC.BI48", - "name": "DVVC.ModEna.BO29" - }, - { - "category": "mode_enable", - "index": 30, - "description": "Enable Watt-VAr Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DWVR", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DWVR.BI49", - "name": "DWVR.ModEna.BO30" - }, - { - "category": "mode_enable", - "index": 31, - "description": "Enable Power Factor Correction Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DPFC", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DPFC.ModEna.BI83", - "name": "DPFC.ModEna.BO31" - }, - { - "category": "mode_enable", - "index": 32, - "description": "Enable Pricing Mode", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DPRG", - "action": "publish_and_respond", - "data_object": "ModEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "response": "DPRG.ModEna.BI84", - "name": "DPRG.ModEna.BO32" - }, - { - "index": 33, - "description": "Enable Event-Based Reactive Current Support Mode, in which the moving average voltage and the base reactive current are frozen until after the voltage has returned to within the deadband for a specified hold time. Dynamic Reactive Current Support mode must be Enable for this setting to apply.", - "data_type": "BO", - "common_data_class": "SPC", - "ln_class": "DRGS", - "data_object": "ArGraMod", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "name": "DRGS.ArGraMod.BO33" - }, - { - "index": 34, - "description": "Frequency-Watt Mode - Use Hysteresis", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DHFW", - "data_object": "HysEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "name": "DHFW.HysEna.BO34" - }, - { - "index": 35, - "description": "Frequency-Watt Mode - Snapshot of Power", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DHFW", - "data_object": "SnptEna", - "allowed_values": { - "0": "Not Active", - "1": "Active" - }, - "name": "DHFW.SnptEna.BO35" - }, - { - "index": 36, - "description": "Frequency-Watt Curve Mode - Use Hysteresis", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DLFW", - "data_object": "HysEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "name": "DLFW.HysEna.BO36" - }, - { - "index": 37, - "description": "Frequency-Watt Curve Mode - Snapshot of Power", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DLFW", - "data_object": "SnptEna", - "allowed_values": { - "0": "Not Active", - "1": "Active" - }, - "name": "DLFW.SnptEna.BO37" - }, - { - "index": 38, - "description": "Charge/Discharge Mode - Use Ramp Rates. Indicates whether or not Charge/Discharge should use specified ramp rates or time constants.", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DWGC", - "data_object": "UseRmpRte", - "allowed_values": { - "0": "Use Time Constatnts", - "1": "Use Ramp Rates" - }, - "name": "DWGC.UseRmpRte.BO38" - }, - { - "index": 39, - "description": "AGC Mode - Use Ramp Rates. Indicates whether or not AGC mode should use specified ramp rates or time constants.", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DAGC", - "data_object": "UseRmpRte", - "allowed_values": { - "0": "Use Time Constatnts", - "1": "Use Ramp Rates" - }, - "name": "DAGC.UseRmpRte.BO39" - }, - { - "index": 40, - "description": "Volt-Watt - Use Ramp Rates and Time Constants. Indicates whether Volt-Watt mode should use only Time Constants,or both Time Constants and Ramp Rates", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DVWC", - "data_object": "UseRmpRte", - "allowed_values": { - "0": "Use Time Constatnts", - "1": "Use Ramp Rates AND Time Constants" - }, - "name": "DVWC.UseRmpRte.BO40" - }, - { - "index": 41, - "description": "Volt-VAr Enable Autonomous Voltage Reference Adjustment", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "DVVR", - "data_object": "VRefAdjEna", - "allowed_values": { - "0": "Disable", - "1": "Enable" - }, - "name": "DVVR.VRefAdjEna.BO41" - }, - { - "index": 42, - "description": "Set Selected Scheduled Ready", - "data_type": "BO", - "common_data_class": "ENC", - "ln_class": "FSCH", - "data_object": "Mod", - "allowed_values": { - "0": "Not Ready", - "1": "Ready" - }, - "name": "FSCHxx.Mod.BO42" - }, - { - "index": 43, - "description": "Set Selected Schedule Repeat Weekly Sunday", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BO43" - }, - { - "index": 44, - "description": "Set Selected Schedule Repeat Weekly Monday", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BO44" - }, - { - "index": 45, - "description": "Set Selected Schedule Repeat Weekly Tuesday", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BO45" - }, - { - "index": 46, - "description": "Set Selected Schedule Repeat Weekly Wednesday", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BO46" - }, - { - "index": 47, - "description": "Set Selected Schedule Repeat Weekly Thursday", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BO47" - }, - { - "index": 48, - "description": "Set Selected Schedule Repeat Weekly Friday", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BO48" - }, - { - "index": 49, - "description": "Set Selected Schedule Repeat Weekly Saturday", - "data_type": "BO", - "common_data_class": "SPG", - "ln_class": "FSCH", - "data_object": "SchdReuse", - "allowed_values": { - "0": "Do Not Repeat", - "1": "Repeat" - }, - "name": "FSCHxx.SchdReuse.BO49" - }, - { - "index": 900, - "description": "Test Point for Database Size", - "data_type": "BI", - "name": "TestPoint.BI900" - } -] \ No newline at end of file diff --git a/services/core/DNP3Agent/tests/data/mesaagent.config b/services/core/DNP3Agent/tests/data/mesaagent.config deleted file mode 100644 index ecfe2a73cb..0000000000 --- a/services/core/DNP3Agent/tests/data/mesaagent.config +++ /dev/null @@ -1,15 +0,0 @@ -{ - "points": "config://mesa_points.config", - "functions": "config://mesa_functions.config", - "point_topic": "mesa/point", - "function_topic": "mesa/function", - "outstation_status_topic": "mesa/outstation_status", - "outstation_config": { - "database_sizes": 2000, - "log_levels": ["NORMAL"] - }, - "local_ip": "0.0.0.0", - "port": 20000, - "all_functions_supported_by_default": true, - "function_validation": false -} diff --git a/services/core/DNP3Agent/tests/data/watt_var_curve.json b/services/core/DNP3Agent/tests/data/watt_var_curve.json deleted file mode 100644 index 5de59f07eb..0000000000 --- a/services/core/DNP3Agent/tests/data/watt_var_curve.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "function_id": "curve", - "function_name": "Watt-Var Curve", - "DGSMn.InCrv.AO244": 2, - "DGSMn.ModTyp.AO245": 4, - "FMARn.IndpUnits.AO247": 138, - "FMARn.DepRef.AO248": 2, - "FMARn.PairArr.CrvPts.AO249": [ - {"FMARn.PairArr.CrvPts.AO249.xVal": 1, - "FMARn.PairArr.CrvPts.AO249.yVal": 2 - }, - {"FMARn.PairArr.CrvPts.AO249.xVal": 3, - "FMARn.PairArr.CrvPts.AO249.yVal": 4 - }, - {"FMARn.PairArr.CrvPts.AO249.xVal": 5, - "FMARn.PairArr.CrvPts.AO249.yVal": 6 - }, - {"FMARn.PairArr.CrvPts.AO249.xVal": 7, - "FMARn.PairArr.CrvPts.AO249.yVal": 8 - }, - {"FMARn.PairArr.CrvPts.AO249.xVal": 9, - "FMARn.PairArr.CrvPts.AO249.yVal": 10 - } - ], - "FMARn.PairArr.NumPts.AO246": 5 -} \ No newline at end of file diff --git a/services/core/DNP3Agent/tests/data/watt_var_schedule.json b/services/core/DNP3Agent/tests/data/watt_var_schedule.json deleted file mode 100644 index 6a7a509aab..0000000000 --- a/services/core/DNP3Agent/tests/data/watt_var_schedule.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "function_id": "schedule", - "function_name": "Watt-Var Schedule", - "FSCC.Schd.AO461": 2, - "FSCC.Schd.AO462": 1, - "FSCH1.SchdPrio.AO463": 1, - "FSCH.SchdVal.valEq.AO464": 28, - "FSCH.StrTm.AO465": 19, - "FSCH.StrTm.AO466": 5, - "FSCH.NxtStrTm.AO467": 0, - "FSCH.SchdReuse.AO468": 0, - "FSCHn.SchdEntr.AO470": [ - {"FSCHn.SchdEntr.AO470.time": 1, - "FSCHn.SchdEntr.AO470.val": 2 - }, - {"FSCHn.SchdEntr.AO470.time": 3, - "FSCHn.SchdEntr.AO470.val": 4 - }, - {"FSCHn.SchdEntr.AO470.time": 5, - "FSCHn.SchdEntr.AO470.val": 6 - }, - {"FSCHn.SchdEntr.AO470.time": 7, - "FSCHn.SchdEntr.AO470.val": 8 - }, - {"FSCHn.SchdEntr.AO470.time": 9, - "FSCHn.SchdEntr.AO470.val": 10 - } - ], - "FSCH.NumEntr.AO469": 5 -} \ No newline at end of file diff --git a/services/core/DNP3Agent/tests/mesa_master_cmd.py b/services/core/DNP3Agent/tests/mesa_master_cmd.py deleted file mode 100644 index 62043a5d2a..0000000000 --- a/services/core/DNP3Agent/tests/mesa_master_cmd.py +++ /dev/null @@ -1,155 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, 8minutenergy / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Neither 8minutenergy nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by 8minutenergy or Kisensum. -# }}} - -import pytest -try: - import dnp3 -except ImportError: - pytest.skip("pydnp3 not found!", allow_module_level=True) - -import cmd -import logging -import sys - -from pydnp3 import opendnp3 - -from dnp3.points import PointDefinitions -from dnp3 import DIRECT_OPERATE -from mesa_master import MesaMaster -from function_test import FunctionTest - -LOG_LEVELS = opendnp3.levels.NORMAL -SERVER_IP = "127.0.0.1" -CLIENT_IP = "0.0.0.0" -PORT_NUMBER = 20000 -POINT_DEFINITIONS_PATH = 'tests/data/mesa_points.config' -FUNCTION_DEFINITIONS_PATH = 'tests/data/mesa_functions.yaml' - -CURVE_JSON = { - "name": "function_test_curve", - "function_id": "curve", - "function_name": "curve_function", - "Curve Edit Selector": 3, - "Curve Mode Type": 40, - "Curve Time Window": 5000, - "Curve Ramp Time": 24, - "Curve Revert Time": 51, - "Curve Maximum Number of Points": 671, - "Independent (X-Value) Units for Curve": 51, - "Dependent (Y-Value) Units for Curve": 625, - "Curve Time Constant": 612, - "Curve Decreasing Max Ramp Rate": 331, - "Curve Increasing Ramp Rate": 451, - "CurveStart-X": [ - {"Curve-X": 111, "Curve-Y": 2}, - {"Curve-X": 3, "Curve-Y": 4}, - {"Curve-X": 5, "Curve-Y": 6}, - {"Curve-X": 7, "Curve-Y": 8}, - {"Curve-X": 9, "Curve-Y": 10} - ], - "Curve Number of Points": 5 -} - -stdout_stream = logging.StreamHandler(sys.stdout) -stdout_stream.setFormatter(logging.Formatter('%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s')) - -_log = logging.getLogger(__name__) -_log.addHandler(stdout_stream) -_log.setLevel(logging.DEBUG) - - -class MesaMasterCmd(cmd.Cmd): - """ - Run MesaMaster from the command line in support of certain types of ad-hoc outstation testing. - """ - - def __init__(self): - cmd.Cmd.__init__(self) - self.prompt = 'master> ' # Used by the Cmd framework, displayed when issuing a command-line prompt. - self._application = None - - @property - def application(self): - if self._application is None: - self._application = MesaMaster(local_ip=SERVER_IP, port=PORT_NUMBER) - self._application.connect() - return self._application - - def startup(self): - """Display the command-line interface's menu and issue a prompt.""" - self.do_menu('') - self.cmdloop('Please enter a command.') - exit() - - def do_menu(self, line): - """Display a menu of command-line options. Command syntax is: menu""" - print('\tfunction\tSend all data/commands for the MESA-ESS function.') - print('\tquit') - - def do_function(self, line): - """Send a function test after validating the function test (as JSON).""" - point_defs = PointDefinitions(point_definitions_path=POINT_DEFINITIONS_PATH) - ftest = FunctionTest(FUNCTION_DEFINITIONS_PATH, CURVE_JSON) - ftest.is_valid() - for func_step_def in ftest.get_function_def().steps: - try: - point_value = ftest.points[func_step_def.name] - except KeyError: - continue - pdef = point_defs.point_named(func_step_def.name) - if not pdef: - raise ValueError("Point definition not found: {}".format(pdef.name)) - - if type(point_value) == list: - self.application.send_array(point_value, pdef) - else: - try: - send_func = self.application.SEND_FUNCTIONS[func_step_def.fcodes[0] - if func_step_def.fcodes - else DIRECT_OPERATE] - except (KeyError, IndexError): - raise ValueError("Unrecognized sent command function") - - self.application.send_command(send_func, pdef, point_value) - - def do_quit(self, line): - """Quit the command line interface. Command syntax is: quit""" - self.application.shutdown() - exit() - - -def main(): - cmd_interface = MesaMasterCmd() - _log.debug('Initialization complete. In command loop.') - cmd_interface.startup() - _log.debug('Exiting.') - - -if __name__ == '__main__': - main() diff --git a/services/core/DNP3Agent/tests/mesa_master_test.py b/services/core/DNP3Agent/tests/mesa_master_test.py deleted file mode 100644 index b1d301c458..0000000000 --- a/services/core/DNP3Agent/tests/mesa_master_test.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, 8minutenergy / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Neither 8minutenergy nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by 8minutenergy or Kisensum. -# }}} - -import pytest -try: - import dnp3 -except ImportError: - pytest.skip("pydnp3 not found!", allow_module_level=True) - -from volttron.platform import jsonapi -from collections import OrderedDict - -from dnp3 import DIRECT_OPERATE -from dnp3_master import SOEHandler -from mesa_master import MesaMaster -from dnp3.mesa.functions import FunctionDefinitions -from function_test import DATA_TYPE_TO_PYTHON_TYPE - - -class MesaMasterTestException(Exception): - pass - - -class MesaMasterTest(MesaMaster): - - def __init__(self, **kwargs): - MesaMaster.__init__(self, soe_handler=SOEHandler(), **kwargs) - - def shutdown(self): - """ - Override MesaMaster shutdown - """ - self.master.Disable() - del self.master - del self.channel - - def send_single_point(self, pdefs, point_name, point_value): - """ - Send a single non-function point to the outstation. Check for validation. - - Used by DNP3Agent (not MesaAgent). - - :param pdefs: point definitions - :param point_name: name of the point that will be sent - :param point_value: value of the point that will be sent - """ - pdef = pdefs.point_named(point_name) - if not pdef: - raise MesaMasterTestException("Point definition not found: {}".format(point_name)) - if not pdef.data_type: - raise MesaMasterTestException("Unrecognized data type: {}".format(pdef.data_type)) - if pdef.data_type in DATA_TYPE_TO_PYTHON_TYPE and \ - type(point_value) not in DATA_TYPE_TO_PYTHON_TYPE[pdef.data_type]: - raise MesaMasterTestException("Invalid point value: {}".format(pdef.name)) - self.send_command(self.send_direct_operate_command, pdef, point_value) - - def send_json(self, pdefs, func_def_path, send_json_path='', send_json=None): - """ - Send a json in order for testing purpose. - - :param pdefs: point definitions - :param func_def_path: path to function definition - :param send_json_path: path to json that will be sent to the outstation - :param send_json: json that will be sent to the outstation - :return: - """ - if send_json_path: - send_json = jsonapi.load(open(send_json_path), object_pairs_hook=OrderedDict) - - try: - function_id = send_json['function_id'] - except KeyError: - raise MesaMasterTestException('Missing function_id') - - fdefs = FunctionDefinitions(pdefs, function_definitions_path=func_def_path) - - try: - fdef = fdefs[function_id] - except KeyError: - raise MesaMasterTestException('Invalid function_id {}'.format(function_id)) - - step = 1 - for name, value in send_json.items(): - if name not in ['name', 'function_id', 'function_name']: - pdef = pdefs.point_named(name) - step_def = fdef[pdef] - if step != step_def.step_number: - raise MesaMasterTestException("Step not in order: {}".format(step)) - if type(value) == list: - self.send_array(value, pdef) - else: - send_func = self.SEND_FUNCTIONS.get(step_def.fcodes[0] if step_def.fcodes else DIRECT_OPERATE, None) - self.send_command(send_func, pdef, value) - step += 1 - - -def main(): - mesa_platform_test = MesaMasterTest() - mesa_platform_test.connect() - # Ad-hoc tests can be inserted here if desired. - mesa_platform_test.shutdown() - - -if __name__ == '__main__': - main() diff --git a/services/core/DNP3Agent/tests/test_dnp3_agent.py b/services/core/DNP3Agent/tests/test_dnp3_agent.py deleted file mode 100644 index e497beb30b..0000000000 --- a/services/core/DNP3Agent/tests/test_dnp3_agent.py +++ /dev/null @@ -1,273 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, SLAC / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# This material was prepared as an account of work sponsored by an agency of -# the United States Government. Neither the United States Government nor the -# United States Department of Energy, nor SLAC, nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by the United States Government or any agency thereof, or -# SLAC, or Kisensum. The views and opinions of authors expressed -# herein do not necessarily state or reflect those of the -# United States Government or any agency thereof. -# }}} - -import pytest -try: - import dnp3 -except ImportError: - pytest.skip("pydnp3 not found!", allow_module_level=True) - -import gevent -import os -import pytest - -from volttron.platform import get_services_core, jsonapi -from volttron.platform.agent.utils import strip_comments - -from dnp3.points import PointDefinitions -from mesa_master_test import MesaMasterTest - -from pydnp3 import asiodnp3, asiopal, opendnp3, openpal - -FILTERS = opendnp3.levels.NORMAL | opendnp3.levels.ALL_COMMS -HOST = "127.0.0.1" -LOCAL = "0.0.0.0" -PORT = 20000 - -DNP3_AGENT_ID = 'dnp3agent' -POINT_TOPIC = "dnp3/point" -TEST_GET_POINT_NAME = 'DCTE.WinTms.AO11' -TEST_SET_POINT_NAME = 'DCTE.WinTms.AI55' - -input_group_map = { - 1: "Binary", - 2: "Binary", - 30: "Analog", - 31: "Analog", - 32: "Analog", - 33: "Analog", - 34: "Analog" -} - -DNP3_AGENT_CONFIG = { - "points": "config://mesa_points.config", - "point_topic": POINT_TOPIC, - "outstation_config": { - "log_levels": ["NORMAL", "ALL_APP_COMMS"] - }, - "local_ip": "0.0.0.0", - "port": 20000 -} - -# Get point definitions from the files in the test directory. -POINT_DEFINITIONS_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), 'data', 'mesa_points.config')) - -pdefs = PointDefinitions(point_definitions_path=POINT_DEFINITIONS_PATH) - -AGENT_CONFIG = { - "points": "config://mesa_points.config", - "outstation_config": { - "database_sizes": 700, - "log_levels": ["NORMAL"] - }, - "local_ip": "0.0.0.0", - "port": 20000 -} - -messages = {} - - -def onmessage(peer, sender, bus, topic, headers, message): - """Callback: As DNP3Agent publishes mesa/point messages, store them in a multi-level global dictionary.""" - global messages - messages[topic] = {'headers': headers, 'message': message} - - -def dict_compare(source_dict, target_dict): - """Assert that the value for each key in source_dict matches the corresponding value in target_dict. - - Ignores keys in target_dict that are not in source_dict. - """ - for name, source_val in source_dict.items(): - target_val = target_dict.get(name, None) - assert source_val == target_val, "Source value of {}={}, target value={}".format(name, source_val, target_val) - - -def add_definitions_to_config_store(test_agent): - """Add PointDefinitions to the mesaagent's config store.""" - with open(POINT_DEFINITIONS_PATH, 'r') as f: - points_json = jsonapi.loads(strip_comments(f.read())) - test_agent.vip.rpc.call('config.store', 'manage_store', DNP3_AGENT_ID, - 'mesa_points.config', points_json, config_type='raw') - - -@pytest.fixture(scope="module") -def agent(request, volttron_instance): - """Build the test agent for rpc call.""" - - test_agent = volttron_instance.build_agent(identity="test_agent") - capabilities = {'edit_config_store': {'identity': 'dnp3agent'}} - volttron_instance.add_capabilities(test_agent.core.publickey, capabilities) - add_definitions_to_config_store(test_agent) - - print('Installing DNP3Agent') - os.environ['AGENT_MODULE'] = 'dnp3.agent' - agent_id = volttron_instance.install_agent(agent_dir=get_services_core("DNP3Agent"), - config_file=AGENT_CONFIG, - vip_identity=DNP3_AGENT_ID, - start=True) - - # Subscribe to DNP3 point publication - test_agent.vip.pubsub.subscribe(peer='pubsub', prefix=POINT_TOPIC, callback=onmessage) - - def stop(): - """Stop test agent.""" - if volttron_instance.is_running(): - volttron_instance.stop_agent(agent_id) - volttron_instance.remove_agent(agent_id) - test_agent.core.stop() - - gevent.sleep(12) # wait for agents and devices to start - - request.addfinalizer(stop) - - return test_agent - - -@pytest.fixture(scope="module") -def run_master(request): - """Run Mesa master application.""" - master = MesaMasterTest(local_ip=AGENT_CONFIG['local_ip'], port=AGENT_CONFIG['port']) - master.connect() - - def stop(): - master.shutdown() - - request.addfinalizer(stop) - - return master - - -@pytest.fixture(scope="function") -def reset(agent): - """Reset agent and global variable messages before running every test.""" - global messages - messages = {} - agent.vip.rpc.call(DNP3_AGENT_ID, 'reset').get() - - -class TestDNP3Agent: - """Regression tests for (non-MESA) DNP3Agent.""" - - @staticmethod - def get_point(agent, point_name): - """Ask DNP3Agent for a point value for a DNP3 resource.""" - return agent.vip.rpc.call(DNP3_AGENT_ID, 'get_point', point_name).get(timeout=10) - - @staticmethod - def get_point_definitions(agent, point_names): - """Ask DNP3Agent for a list of point definitions.""" - return agent.vip.rpc.call(DNP3_AGENT_ID, 'get_point_definitions', point_names).get(timeout=10) - - @staticmethod - def get_point_by_index(agent, data_type, index): - """Ask DNP3Agent for a point value for a DNP3 resource.""" - return agent.vip.rpc.call(DNP3_AGENT_ID, 'get_point_by_index', data_type, index).get(timeout=10) - - @staticmethod - def set_point(agent, point_name, value): - """Use DNP3Agent to set a point value for a DNP3 resource.""" - response = agent.vip.rpc.call(DNP3_AGENT_ID, 'set_point', point_name, value).get(timeout=10) - gevent.sleep(5) # Give the Master time to receive an echoed point value back from the Outstation. - return response - - @staticmethod - def set_points(agent, point_dict): - """Use DNP3Agent to set point values for a DNP3 resource.""" - return agent.vip.rpc.call(DNP3_AGENT_ID, 'set_points', point_dict).get(timeout=10) - - @staticmethod - def send_single_point(master, point_name, point_value): - """ - Send a point name and value from the Master to DNP3Agent. - - Return a dictionary with an exception key and error, empty if successful. - """ - try: - master.send_single_point(pdefs, point_name, point_value) - return {} - except Exception as err: - exception = {'key': type(err).__name__, 'error': str(err)} - print("Exception sending point from master: {}".format(exception)) - return exception - - @staticmethod - def get_value_from_master(master, point_name): - """Get value of the point from master after being set by test agent.""" - try: - pdef = pdefs.point_named(point_name) - group = input_group_map[pdef.group] - index = pdef.index - return master.soe_handler.result[group][index] - except KeyError: - return None - - def get_point_definition(self, agent, point_name): - """Confirm that the agent has a point definition named point_name. Return the definition.""" - point_defs = self.get_point_definitions(agent, [point_name]) - point_def = point_defs.get(point_name, None) - assert point_def is not None, "Agent has no point definition for {}".format(TEST_GET_POINT_NAME) - return point_def - - @staticmethod - def subscribed_points(): - """Return point values published by DNP3Agent using the dnp3/point topic.""" - return messages[POINT_TOPIC].get('message', {}) - - # ********** - # ********** OUTPUT TESTS (send data from Master to Agent to ControlAgent) ************ - # ********** - - def test_get_point_definition(self, run_master, agent, reset): - """Ask the agent whether it has a point definition for a point name.""" - self.get_point_definition(agent, TEST_GET_POINT_NAME) - - def test_send_point(self, run_master, agent, reset): - """Send a point from the master and get its value from DNP3Agent.""" - exceptions = self.send_single_point(run_master, TEST_GET_POINT_NAME, 45) - assert exceptions == {} - received_point = self.get_point(agent, TEST_GET_POINT_NAME) - # Confirm that the agent's received point value matches the value that was sent. - assert received_point == 45, "Expected {} = {}, got {}".format(TEST_GET_POINT_NAME, 45, received_point) - dict_compare({TEST_GET_POINT_NAME: 45}, self.subscribed_points()) - - # ********** - # ********** INPUT TESTS (send data from ControlAgent to Agent to Master) ************ - # ********** - - def test_set_point(self, run_master, agent, reset): - """Test set an input point and confirm getting the same value for that point.""" - self.set_point(agent, TEST_SET_POINT_NAME, 45) - received_val = self.get_value_from_master(run_master, TEST_SET_POINT_NAME) - assert received_val == 45, "Expected {} = {}, got {}".format(TEST_SET_POINT_NAME, 45, received_val) diff --git a/services/core/DNP3Agent/tests/test_functions.py b/services/core/DNP3Agent/tests/test_functions.py deleted file mode 100644 index ce3cdfc92f..0000000000 --- a/services/core/DNP3Agent/tests/test_functions.py +++ /dev/null @@ -1,369 +0,0 @@ -import pytest -try: - import dnp3 -except ImportError: - pytest.skip("pydnp3 not found!", allow_module_level=True) - -import copy - -from dnp3.points import PointDefinitions -from dnp3.mesa.functions import FunctionDefinitions, FunctionDefinition, StepDefinition - -from test_mesa_agent import POINT_DEFINITIONS_PATH, FUNCTION_DEFINITIONS_PATH - - -POINT_DEFINITIONS = PointDefinitions(point_definitions_path=POINT_DEFINITIONS_PATH) - -enable_high_voltage_ride_through_mode = { - 'id': 'enable_high_voltage_ride_through_mode', - 'name': 'Enable High Volatge Ride-Through Mode', - 'ref': 'AN2018 Spec section 2.5.1 Table 33', - 'steps': [ - { - 'step_number': 1, - 'description': 'Set the Reference Voltage if it is not already set', - 'point_name': 'DECP.VRef.AO0', - 'optional': 'I', - 'fcode': ['direct_operate'], - 'response': 'DECP.VRef.AI29' - }, - { - 'step_number': 2, - 'description': 'Set the Reference Voltage Offset if it is not already set', - 'point_name': 'DECP.VRefOfs.AO1', - 'optional': 'I', - 'fcode': ['direct_operate'], - 'response': 'DECP.VRefOfs.AI30' - }, - { - 'step_number': 3, - 'description': 'Identify the meter used to measure the voltage. By default this is the System Meter', - 'point_name': 'DHVT.EcpRef.AO22', - 'optional': 'I', - 'fcode': ['direct_operate'], - 'response': 'DHVT.EcpRef.AI71' - }, - { - 'step_number': 4, - 'description': 'Identify the index of the curve which specifies trip points when the voltage is high', - 'point_name': 'PTOV.BlkRef.AO23', - 'optional': 'M', - 'fcode': ['direct_operate'], - 'response': 'PTOV.BlkRef.AI73', - 'func_ref': 'curve' - }, - { - 'step_number': 5, - 'description': 'Enable the Low/High Voltage Ride-Through Mode', - 'point_name': 'DHVT.ModEna.BO12', - 'optional': 'M', - 'fcode': ['select', 'operate'], - 'response': 'DHVT.ModEna.BI64' - } - ] -} - -curve_selector_block = { - 'id': 'curve', - 'name': 'Curve', - 'ref': 'AN2018 Spec Curve Definition', - 'steps': [ - { - 'step_number': 1, - 'description': 'Select which curve to edit', - 'point_name': 'DGSMn.InCrv.AO244', - 'optional': 'M', - 'fcode': ['direct_operate'], - 'response': 'DGSMn.InCrv.AI328' - }, - { - 'step_number': 2, - 'description': 'Specify the Curve Mode Type', - 'point_name': 'DGSMn.ModTyp.AO245', - 'optional': 'M', - 'fcode': ['direct_operate'], - 'response': 'DGSMn.ModTyp.AI329' - }, - { - 'step_number': 3, - 'description': 'Specify that the Independent (X-Value) units for the curve', - 'point_name': 'FMARn.IndpUnits.AO247', - 'optional': 'M', - 'fcode': ['direct_operate'], - 'response': 'FMARn.IndpUnits.AI331' - }, - { - 'step_number': 4, - 'description': 'Specify the Dependent (Y-Value) units for the curve', - 'point_name': 'FMARn.DepRef.AO248', - 'optional': 'M', - 'fcode': ['direct_operate'], - 'response': 'FMARn.DepRef.AI332', - 'action': 'publish' - }, - { - 'step_number': 5, - 'description': 'Set X-Value and Y-Values pairs for the curve', - 'point_name': 'FMARn.PairArr.CrvPts.AO249', - 'optional': 'M', - 'fcode': ['direct_operate'], - 'response': 'FMARn.PairArr.CrvPts.AI333' - }, - { - 'step_number': 6, - 'description': 'Set number of points used for the curve', - 'point_name': 'FMARn.PairArr.NumPts.AO246', - 'optional': 'M', - 'fcode': ['direct_operate'], - 'response': 'FMARn.PairArr.NumPts.AI330' - } - ] -} - - -class TestStepDefinition: - """Regression tests for Step Definition.""" - - @property - def function_id(self): - return 'enable_high_voltage_ride_through_mode' - - @property - def step_number(self): - return 1 - - @property - def step_json(self): - """Return function enable_high_voltage_ride_through_mode step 1""" - return copy.deepcopy(enable_high_voltage_ride_through_mode)['steps'][self.step_number-1] - - def validate_step_definition(self, step_json): - exception = {} - try: - step_def = StepDefinition(POINT_DEFINITIONS, self.function_id, step_json) - step_def.validate() - except Exception as err: - exception['key'] = type(err).__name__ - exception['error'] = str(err) - return exception - - def test_valid_step_definition(self): - exception = self.validate_step_definition(self.step_json) - assert exception == {} - - def test_missing_step_number(self): - """Test raising exception if step missing step_number""" - step_json = self.step_json - step_json.pop('step_number') - exception = self.validate_step_definition(step_json) - assert exception == { - 'key': 'AttributeError', - 'error': 'Missing step number in function {}'.format(self.function_id) - } - - def test_missing_point_name(self): - """Test raising exception if step missing point_name""" - step_json = self.step_json - step_json.pop('point_name') - exception = self.validate_step_definition(step_json) - assert exception == { - 'key': 'AttributeError', - 'error': 'Missing name in function {} step {}'.format(self.function_id, self.step_number) - } - - def test_invalid_optionality(self): - """Test raising exception if optional not O, M, or I""" - step_json = self.step_json - step_json.update({ - 'optional': 'C' - }) - exception = self.validate_step_definition(step_json) - assert exception == { - 'key': 'AttributeError', - 'error': 'Invalid optional value in function {} step {}: C'.format(self.function_id, self.step_number) - } - - def test_invalid_fcodes_type(self): - """Test raising exception if fcodes is not a list""" - step_json = self.step_json - step_json.update({ - 'fcodes': 'direct_operate' - }) - exception = self.validate_step_definition(step_json) - assert exception == { - 'key': 'AttributeError', - 'error': "Invalid fcodes in function {} step {}, type=".format(self.function_id, - self.step_number) - } - - def test_invalid_fcode_value(self): - """Test raising exception if a str value in fcodes list is invalid""" - step_json = self.step_json - step_json.update({ - 'fcodes': ['select_operate'] - }) - exception = self.validate_step_definition(step_json) - assert exception == { - 'key': 'AttributeError', - 'error': 'Invalid fcode in function {} step {}, fcode=select_operate'.format(self.function_id, - self.step_number) - } - - def test_invalid_optionality_for_read_fcode(self): - """Test raising exception if optionality is not OPTIONAL when fcode is read""" - step_json = self.step_json - step_json.update({ - 'fcodes': ['read', 'response'] - }) - exception = self.validate_step_definition(step_json) - assert exception == { - 'key': 'AttributeError', - 'error': 'Invalid optionality in function {} step {}: must be OPTIONAL'.format(self.function_id, - self.step_number) - } - - def test_invalid_response_point(self): - step_json = self.step_json - step_json.update({ - 'response': 'invalid_point' - }) - exception = self.validate_step_definition(step_json) - assert exception == { - 'key': 'AttributeError', - 'error': 'Response point in function {} step {} does not match point definition. ' - 'Error=No point named invalid_point'.format(self.function_id, self.step_number) - } - - -class TestFunctionDefinition: - """Regression tests for Function Definition.""" - - @property - def function_json(self): - """Return function enable_high_voltage_ride_through_mode""" - return copy.deepcopy(enable_high_voltage_ride_through_mode) - - @staticmethod - def validate_function_definition(function_json): - exception = {} - try: - FunctionDefinition(POINT_DEFINITIONS, function_json) - except Exception as err: - exception['key'] = type(err).__name__ - exception['error'] = str(err) - return exception - - def test_valid_function_definition(self): - exception = self.validate_function_definition(self.function_json) - assert exception == {} - - def test_missing_function_id(self): - """Test raising exception if function missing id""" - function_json = self.function_json - function_json.pop('id') - exception = self.validate_function_definition(function_json) - assert exception == { - 'key': 'ValueError', - 'error': 'Missing function ID' - } - - def test_missing_function_steps(self): - """Test raising exception if function missing steps""" - function_json = self.function_json - function_json.pop('steps') - exception = self.validate_function_definition(function_json) - assert exception == { - 'key': 'ValueError', - 'error': 'Missing steps for function {}'.format(self.function_json['id']) - } - - def test_duplicated_step_number(self): - """Test raising exception if there is duplicated step_number in function""" - function_json = self.function_json - function_json['steps'][2].update({ - 'step_number': 1 - }) - exception = self.validate_function_definition(function_json) - assert exception == { - 'key': 'ValueError', - 'error': 'Duplicated step number 1 for function {}'.format(self.function_json['id']) - } - - def test_missing_a_step(self): - """Test raising exception if function missing a step""" - function_json = self.function_json - del function_json['steps'][1] - exception = self.validate_function_definition(function_json) - assert exception == { - 'key': 'ValueError', - 'error': 'There are missing steps for function {}'.format(self.function_json['id']) - } - - def test_selector_block_function(self): - """Test raising exception if one step in selector block function is optional""" - function_json = copy.deepcopy(curve_selector_block) - exception = self.validate_function_definition(function_json) - assert exception == {} - - # Change step 2 to optional - function_json['steps'][1]['optional'] = 'O' - exception = self.validate_function_definition(function_json) - assert exception == { - 'key': 'ValueError', - 'error': 'Function curve - Step 2: optionality must be either INITIALIZE or MANDATORY' - } - - -class TestFunctionDefinitions: - """Regression tests for Function Definitions.""" - - @property - def functions_json(self): - return [ - copy.deepcopy(enable_high_voltage_ride_through_mode), - copy.deepcopy(curve_selector_block) - ] - - @staticmethod - def validate_functions_definition(functions_json): - exception = {} - try: - function_definitions = FunctionDefinitions(POINT_DEFINITIONS) - function_definitions.load_functions(functions_json) - except Exception as err: - exception['key'] = type(err).__name__ - exception['error'] = str(err) - return exception - - def test_load_functions_yaml(self): - try: - FunctionDefinitions(POINT_DEFINITIONS, FUNCTION_DEFINITIONS_PATH) - assert True - except ValueError: - assert False - - def test_valid_functions_definitions(self): - exception = self.validate_functions_definition(self.functions_json) - assert exception == {} - - def test_duplicated_function_id(self): - """Test raising exception if there are multiple function with same id""" - functions_json = self.functions_json - functions_json[1]['id'] = self.functions_json[0]['id'] - exception = self.validate_functions_definition(functions_json) - assert exception == { - 'key': 'ValueError', - 'error': 'Problem parsing FunctionDefinitions. ' - 'Error=There are multiple functions for function id {}'.format(functions_json[1]['id']) - } - - def test_invalid_func_ref(self): - """Test raising exception if a step has an invalid func_ref""" - functions_json = self.functions_json - functions_json[0]['steps'][3]['func_ref'] = 'invalid_curve' - exception = self.validate_functions_definition(functions_json) - assert exception == { - 'key': 'ValueError', - 'error': 'Invalid Function Reference invalid_curve for Step 4 ' - 'in Function enable_high_voltage_ride_through_mode' - } diff --git a/services/core/DNP3Agent/tests/test_mesa_agent.py b/services/core/DNP3Agent/tests/test_mesa_agent.py deleted file mode 100644 index 0e3185207c..0000000000 --- a/services/core/DNP3Agent/tests/test_mesa_agent.py +++ /dev/null @@ -1,546 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, 8minutenergy / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Neither 8minutenergy nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by 8minutenergy or Kisensum. -# }}} - -import pytest -try: - import dnp3 -except ImportError: - pytest.skip("pydnp3 not found!", allow_module_level=True) - -import gevent -import os -import pytest -import yaml - -from dnp3.points import PointDefinitions -from mesa_master_test import MesaMasterTest -from volttron.platform import get_services_core, jsonapi -from volttron.platform.agent.utils import strip_comments - -MESA_AGENT_ID = 'mesaagent' - -# Get point and function definitions from the files in the test directory. -POINT_DEFINITIONS_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), 'data', 'mesa_points.config')) -FUNCTION_DEFINITIONS_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), 'data', 'mesa_functions.yaml')) - -pdefs = PointDefinitions(point_definitions_path=POINT_DEFINITIONS_PATH) - -input_group_map = { - 1: 'Binary', - 2: 'Binary', - 30: 'Analog', - 31: 'Analog', - 32: 'Analog', - 33: 'Analog', - 34: 'Analog' -} - -MESA_AGENT_CONFIG = { - 'points': 'config://mesa_points.config', - 'functions': 'config://mesa_functions.config', - 'point_topic': 'mesa/point', - 'function_topic': 'mesa/function', - 'outstation_status_topic': 'mesa/outstation_status', - 'outstation_config': { - 'database_sizes': 800, - 'log_levels': ['NORMAL'] - }, - 'local_ip': '0.0.0.0', - 'port': 20000, - 'all_functions_supported_by_default': True, - 'function_validation': False -} - -messages = {} - - -def onmessage(peer, sender, bus, topic, headers, message): - """Callback: As mesaagent publishes mesa/function messages, store them in a multi-level global dictionary.""" - global messages - messages[topic] = {'headers': headers, 'message': message} - - -def dict_compare(source_dict, target_dict): - """Assert that the value for each key in source_dict matches the corresponding value in target_dict. - - Ignores keys in target_dict that are not in source_dict. - """ - for name, source_val in source_dict.items(): - target_val = target_dict.get(name, None) - assert source_val == target_val, 'Source value of {}={}, target value={}'.format(name, source_val, target_val) - - -def add_definitions_to_config_store(test_agent): - """Add PointDefinitions and FunctionDefinitions to the mesaagent's config store.""" - with open(POINT_DEFINITIONS_PATH, 'r') as f: - points_json = jsonapi.loads(strip_comments(f.read())) - test_agent.vip.rpc.call('config.store', 'manage_store', MESA_AGENT_ID, - 'mesa_points.config', points_json, config_type='raw') - with open(FUNCTION_DEFINITIONS_PATH, 'r') as f: - functions_json = yaml.load(f.read()) - test_agent.vip.rpc.call('config.store', 'manage_store', MESA_AGENT_ID, - 'mesa_functions.config', functions_json, config_type='raw') - - -@pytest.fixture(scope="module") -def agent(request, volttron_instance): - """Build the test agent for rpc call.""" - - test_agent = volttron_instance.build_agent(identity="test_agent") - capabilities = {'edit_config_store': {'identity': 'mesaagent'}} - volttron_instance.add_capabilities(test_agent.core.publickey, capabilities) - - add_definitions_to_config_store(test_agent) - - print('Installing Mesa Agent') - os.environ['AGENT_MODULE'] = 'dnp3.mesa.agent' - agent_id = volttron_instance.install_agent(agent_dir=get_services_core('DNP3Agent'), - config_file=MESA_AGENT_CONFIG, - vip_identity=MESA_AGENT_ID, - start=True) - - # Subscribe to MESA functions - test_agent.vip.pubsub.subscribe(peer='pubsub', - prefix='mesa/function', - callback=onmessage) - - test_agent.vip.pubsub.subscribe(peer='pubsub', - prefix='mesa/point', - callback=onmessage) - - def stop(): - """Stop test agent.""" - if volttron_instance.is_running(): - volttron_instance.stop_agent(agent_id) - volttron_instance.remove_agent(agent_id) - test_agent.core.stop() - - gevent.sleep(3) # wait for agents and devices to start - - request.addfinalizer(stop) - - return test_agent - - -@pytest.fixture(scope="module") -def run_master(request): - """Run Mesa master application.""" - master = MesaMasterTest(local_ip=MESA_AGENT_CONFIG['local_ip'], - port=MESA_AGENT_CONFIG['port']) - master.connect() - - def stop(): - master.shutdown() - - request.addfinalizer(stop) - - return master - - -@pytest.fixture(scope="function") -def reset(agent): - """Reset agent and global variable messages before running every test.""" - global messages - messages = {} - agent.vip.rpc.call(MESA_AGENT_ID, 'reset') - - -class TestMesaAgent: - """Regression tests for the Mesa Agent.""" - - @staticmethod - def get_point(agent, point_name): - """Ask DNP3Agent for a point value for a DNP3 resource.""" - return agent.vip.rpc.call(MESA_AGENT_ID, 'get_point', point_name).get(timeout=10) - - @staticmethod - def get_point_definitions(agent, point_names): - """Ask DNP3Agent for a list of point definitions.""" - return agent.vip.rpc.call(MESA_AGENT_ID, 'get_point_definitions', point_names).get(timeout=10) - - @staticmethod - def get_point_by_index(agent, data_type, index): - """Ask DNP3Agent for a point value for a DNP3 resource.""" - return agent.vip.rpc.call(MESA_AGENT_ID, 'get_point_by_index', data_type, index).get(timeout=10) - - @staticmethod - def set_point(agent, point_name, value): - """Use DNP3Agent to set a point value for a DNP3 resource.""" - response = agent.vip.rpc.call(MESA_AGENT_ID, 'set_point', point_name, value).get(timeout=10) - gevent.sleep(1) # Give the Master time to receive an echoed point value back from the Outstation. - return response - - @staticmethod - def set_points(agent, point_dict): - """Use DNP3Agent to set point values for a DNP3 resource.""" - return agent.vip.rpc.call(MESA_AGENT_ID, 'set_points', point_dict).get(timeout=10) - - @staticmethod - def get_selector_block(agent, point_name, edit_selector): - """Get a selector block from the MesaAgent via an RPC call.""" - return agent.vip.rpc.call(MESA_AGENT_ID, 'get_selector_block', point_name, edit_selector).get(timeout=10) - - @staticmethod - def convert_json_file_to_dict(json_file): - """Convert a json file to a dictionary.""" - send_json = os.path.abspath(os.path.join(os.path.dirname(__file__), 'data', json_file)) - return jsonapi.load(open(send_json)) - - @staticmethod - def send_points(master, send_json, send_in_step_order=True): - """Master loads points from json and send them to mesa agent. - Return empty dictionary if function sent successfully, the dictionary with key and error otherwise.""" - exceptions = {} - try: - if send_in_step_order: - master.send_function_test(func_test_json=send_json) - else: - master.send_json(pdefs, FUNCTION_DEFINITIONS_PATH, send_json=send_json) - except Exception as err: - print('{}: {}'.format(type(err).__name__, str(err))) - exceptions['key'] = type(err).__name__ - exceptions['error'] = str(err) - - return exceptions - - @staticmethod - def get_value_from_master(master, point_name): - """Get value of the point from master after being set by test agent.""" - try: - pdef = pdefs.point_named(point_name) - group = input_group_map[pdef.group] - index = pdef.index - return master.soe_handler.result[group][index] - except KeyError: - return None - - def send_function_and_confirm(self, master, agent, json_file, func_ref=None): - """Test get points to confirm if points is set correctly by master.""" - send_function = self.convert_json_file_to_dict(json_file) - exceptions = self.send_points(master, send_function) - - for point_name in send_function.keys(): - if point_name not in ['name', 'function_id', 'function_name']: - - pdef = pdefs.point_named(point_name) - - if pdef.is_array_head_point: - for offset, value in enumerate( - record[field['name']] - for record in send_function[point_name] for field in pdef.array_points - ): - get_point = self.get_point_by_index(agent, pdef.data_type, pdef.index+offset) - assert get_point == value, 'Expected {} = {}, got {}'.format(point_name, value, get_point) - else: - get_point = self.get_point(agent, point_name) - # Ask the agent whether it has a point definition for that point name. - point_defs = self.get_point_definitions(agent, [point_name]) - point_def = point_defs.get(point_name, None) - assert point_def is not None, 'Agent has no point definition for {}'.format(point_name) - # Confirm that the agent's point value matches the value in the json. - json_val = send_function[point_name] - assert get_point == json_val, 'Expected {} = {}, got {}'.format(point_name, - json_val, - get_point) - if func_ref: - send_function.update({ - func_ref['name']: { - str(func_ref['index']): self.get_selector_block(agent, func_ref['name'], func_ref['index']) - } - }) - dict_compare(messages['mesa/function']['message']['points'], send_function) - assert exceptions == {} - - # ********** - # ********** OUTPUT TESTS (send data from Master to Agent to ControlAgent) ************ - # ********** - - def test_send_single_point_publish(self, run_master, agent, reset): - """Test send a single point with publish action.""" - test_point_name = 'DTCD.ModEna.BO19' - run_master.send_single_point(pdefs, test_point_name, True) - assert self.get_point(agent, test_point_name) == True - assert messages['mesa/point']['message'] == {test_point_name: True} - - def test_send_single_point_publish_and_respond(self, run_master, agent, reset): - """Test send a single point with publish_and_respond action.""" - test_point_name = 'DHVT.ModEna.BO12' - run_master.send_single_point(pdefs, test_point_name, True) - assert self.get_point(agent, test_point_name) == True - assert messages['mesa/point']['message'] == {test_point_name: True, - 'response': 'DHVT.ModEna.BI64'} - - def test_point_definition(self, agent, reset): - """Confirm whether the agent has a point def for a given name.""" - point_name = 'DCTE.VHiLim.AO6' - point_def = self.get_point_definitions(agent, [point_name]).get(point_name, None) - assert point_def is not None, 'Agent has no point definition for {}'.format(point_name) - - def test_simple_function(self, run_master, agent, reset): - """Test a simple function (not array or selector block).""" - self.send_function_and_confirm(run_master, agent, 'connect_and_disconnect.json') - - def test_curve(self, run_master, agent, reset): - """Test curve function.""" - assert self.get_selector_block(agent, 'DGSMn.InCrv.AO244', 2) is None - self.send_function_and_confirm(run_master, agent, 'watt_var_curve.json') - dict_compare(self.get_selector_block(agent, 'DGSMn.InCrv.AO244', 2), - self.convert_json_file_to_dict('watt_var_curve.json')) - - def test_enable_curve(self, run_master, agent, reset): - """Test curve function reference.""" - self.send_function_and_confirm(run_master, agent, 'watt_var_curve.json') - func_ref = { - 'name': 'DGSMn.InCrv.AO244', - 'index': 2.0 - } - self.send_function_and_confirm(run_master, agent, 'enable_watt_var_power_mode.json', func_ref) - assert messages['mesa/point']['message'] == {'DWVR.ModEna.BO30': True, - 'response': 'DWVR.BI49'} - - def test_schedule(self, run_master, agent, reset): - """Test schedule function.""" - assert self.get_selector_block(agent, 'FSCC.Schd.AO461', 2) is None - self.send_function_and_confirm(run_master, agent, 'watt_var_schedule.json') - dict_compare(self.get_selector_block(agent, 'FSCC.Schd.AO461', 2), - self.convert_json_file_to_dict('watt_var_schedule.json')) - - def test_enable_schedule(self, run_master, agent, reset): - """Test schedule function reference""" - self.send_function_and_confirm(run_master, agent, 'watt_var_schedule.json') - func_ref = { - 'name': 'FSCC.Schd.AO461', - 'index': 2.0 - } - self.send_function_and_confirm(run_master, agent, 'enable_watt_var_schedule.json', func_ref) - - def test_function_reference_fail(self, run_master, agent, reset): - """Test edit selector with Selector Block value have not set""" - send_function = self.convert_json_file_to_dict('enable_watt_var_schedule.json') - self.send_points(run_master, send_function) - assert messages == {} - - def test_invalid_function(self, run_master, agent, reset): - """Test send an invalid function, confirm getting exception error.""" - send_function = { - 'function_id': 'Invalid Function', - 'function_name': 'Testing Invalid Function', - 'point_1': 1, - 'point_2': 2 - } - exceptions = self.send_points(run_master, send_function) - assert exceptions == { - 'key': 'FunctionTestException', - 'error': 'Validation Error: Function definition not found: Invalid Function' - } - assert messages == {} - - def test_invalid_point_value(self, run_master, agent, reset): - """Test send a function with an invalid data type for a point, confirm getting exception error.""" - # Set the function support point to True - send_function = self.convert_json_file_to_dict('connect_and_disconnect.json') - - # Change the analog value to binary - send_function['DCTE.WinTms.AO16'] = True - - exceptions = self.send_points(run_master, send_function) - assert exceptions == { - 'key': 'FunctionTestException', - 'error': 'Validation Error: Invalid point value: DCTE.WinTms.AO16' - } - assert messages == {} - - # Change back to the valid point value - send_function['DCTE.WinTms.AO16'] = 10 - - # Change the binary value to analog - send_function['CSWI.Pos.BO5'] = 1 - - exceptions = self.send_points(run_master, send_function) - assert exceptions == { - 'key': 'FunctionTestException', - 'error': 'Validation Error: Invalid point value: CSWI.Pos.BO5' - } - assert messages == {} - - def test_invalid_array_value(self, run_master, agent, reset): - """Test send a function with an invalid data type for a point, confirm getting exception error.""" - send_function = self.convert_json_file_to_dict('watt_var_curve.json') - - # Change the analog array value to binary - send_function['FMARn.PairArr.CrvPts.AO249'] = [ - {'FMARn.PairArr.CrvPts.AO249.xVal': 1, - 'FMARn.PairArr.CrvPts.AO249.yVal': 2}, - {'FMARn.PairArr.CrvPts.AO249.xVal': 3, - 'FMARn.PairArr.CrvPts.AO249.yVal': 4}, - {'FMARn.PairArr.CrvPts.AO249.xVal': 5, - 'FMARn.PairArr.CrvPts.AO249.yVal': 6}, - {'FMARn.PairArr.CrvPts.AO249.xVal': 7, - 'FMARn.PairArr.CrvPts.AO249.yVal': 8}, - {'FMARn.PairArr.CrvPts.AO249.xVal': 9, - 'FMARn.PairArr.CrvPts.AO249.yVal': True} - ] - exceptions = self.send_points(run_master, send_function) - assert exceptions == { - 'key': 'FunctionTestException', - 'error': 'Validation Error: Invalid point value: FMARn.PairArr.CrvPts.AO249' - } - assert messages == {} - - def test_missing_mandatory_step(self, run_master, agent, reset): - """Test send a function missing its mandatory step, confirm getting exception error.""" - send_function = self.convert_json_file_to_dict('connect_and_disconnect.json') - - # Remove mandatory step - del send_function['DCTE.RvrtTms.AO17'] - - exceptions = self.send_points(run_master, send_function) - assert exceptions == { - 'key': 'FunctionTestException', - 'error': "Validation Error: Function Test missing mandatory steps: ['DCTE.RvrtTms.AO17']" - } - assert messages == {} - - def test_missing_point_definition(self, run_master, agent, reset): - """Test send a function with a point not defined in point definitions, confirm getting exception error.""" - send_function = self.convert_json_file_to_dict('connect_and_disconnect.json') - - # Add a point for testing - send_function['test point'] = 5 - - exceptions = self.send_points(run_master, send_function) - assert exceptions == { - 'key': 'FunctionTestException', - 'error': 'Validation Error: Not all points resolve' - } - assert messages == {} - - def test_wrong_step_order(self, run_master, agent, reset): - """Test send a function in wrong step order, confirm getting exception error.""" - connect_and_disconnect_dict = { - 'function_id': 'connect_and_disconnect', - 'name': 'Connect and Disconnect', - 'DCTE.RvrtTms.AO17': 12, # In wrong order: suppose to be step 2 instead of step 1 - 'DCTE.WinTms.AO16': 10, # In wrong order: suppose to be step 1 instead of step 2 - 'CSWI.Pos.BO5': True - } - - exceptions = self.send_points(run_master, connect_and_disconnect_dict, send_in_step_order=False) - assert exceptions == { - 'key': 'MesaMasterTestException', - 'error': 'Step not in order: 1' - } - assert messages == {} - - # ********** - # ********** INPUT TESTS (send data from ControlAgent to Agent to Master) ************ - # ********** - - def test_set_point(self, run_master, agent, reset): - """Test set an input point and confirm getting the same value for that point.""" - point_name = 'DCTE.WinTms.AI55' - self.set_point(agent, point_name, 45) - received_val = self.get_value_from_master(run_master, point_name) - assert received_val == 45, 'Expected {} = {}, got {}'.format(point_name, 45, received_val) - - def test_set_invalid_point(self, agent, reset): - """Test set an invalid input point and confirm getting exception error.""" - point_name = 'Invalid Point' - try: - self.set_point(agent, point_name, 45) - assert False, 'Input point with invalid name failed to cause an exception' - except Exception as err: - assert str(err) == "dnp3.points.DNP3Exception('No point named {}')".format(point_name) - - def test_set_invalid_point_value(self, agent, reset): - """Test set an invalid input point and confirm getting exception error.""" - point_name = 'DCTE.WinTms.AI55' - try: - self.set_point(agent, point_name, True) - assert False, 'Input point with invalid value failed to cause an exception' - except Exception as err: - assert str(err) == "dnp3.points.DNP3Exception(\"Received value for PointDefinition " \ - "{} (event_class=2, index=55, type=AI).\")".format(point_name) - - def test_set_points(self, run_master, agent, reset): - """Test set a set of points and confirm getting the correct values for all point that are set.""" - - set_points_dict = { - 'AI0': 0, - 'AI1': 1, - 'DGEN.VMinRtg.AI2': 2, - 'DGEN.VMaxRtg.AI3': 3, - 'DGEN.WMaxRtg.AI4': 4, - 'DSTO.ChaWMaxRtg.AI5': 5, - 'DGEN.WOvPFRtg.AI6': 6, - 'DSTO.ChaWOvPFRtg.AI7': 7, - 'DGEN.OvPFRtg.AI8': 8, - 'DGEN.WUnPFRtg.AI9': 9, - 'DHVT.ModEna.BI64': True - } - - self.set_points(agent, set_points_dict) - - for point_name in set_points_dict.keys(): - assert self.get_value_from_master(run_master, point_name) == set_points_dict[point_name] - - def test_set_points_array(self, run_master, agent, reset): - """Test set a set of points of an array and confirm getting the correct values for all point that are set.""" - - self.set_points(agent, { - 'FMARn.PairArr.CrvPts.AI333': [ - {'FMARn.PairArr.CrvPts.AI333.xVal': 1, - 'FMARn.PairArr.CrvPts.AI333.yVal': 2}, - {'FMARn.PairArr.CrvPts.AI333.xVal': 3, - 'FMARn.PairArr.CrvPts.AI333.yVal': 4}, - {'FMARn.PairArr.CrvPts.AI333.xVal': 5, - 'FMARn.PairArr.CrvPts.AI333.yVal': 6} - ] - }) - - pdef = pdefs.point_named('FMARn.PairArr.CrvPts.AI333') - group = input_group_map[pdef.group] - - assert run_master.soe_handler.result[group][333] == 1.0 - assert run_master.soe_handler.result[group][334] == 2.0 - assert run_master.soe_handler.result[group][335] == 3.0 - assert run_master.soe_handler.result[group][336] == 4.0 - assert run_master.soe_handler.result[group][337] == 5.0 - assert run_master.soe_handler.result[group][338] == 6.0 - - def test_wrong_database_size(self, run_master, agent, reset): - """Test set point for an index out of database size range, confirm receiving None for that point.""" - - try: - # This Input Test Point index is 800, but database size is only 700 - self.set_point(agent, 'TestPoint.BI900', True) - assert False, 'Wrong database size failed to cause an exception' - except Exception as err: - assert str(err) == "dnp3.points.DNP3Exception('Attempt to set a value for index 900 " \ - "which exceeds database size 800')" diff --git a/services/core/DNP3Agent/tests/test_points.py b/services/core/DNP3Agent/tests/test_points.py deleted file mode 100644 index b8fbf8023f..0000000000 --- a/services/core/DNP3Agent/tests/test_points.py +++ /dev/null @@ -1,177 +0,0 @@ -import pytest -try: - import dnp3 -except ImportError: - pytest.skip("pydnp3 not found!", allow_module_level=True) - -import copy - -from dnp3.points import PointDefinition, ArrayHeadPointDefinition, PointDefinitions, PointValue - -from test_mesa_agent import POINT_DEFINITIONS_PATH, FUNCTION_DEFINITIONS_PATH - - -AO_4 = { - 'index': 4, - 'description': 'Power Factor Sign convention', - 'data_type': 'AO', - 'common_data_class': 'ENG', - 'maximum': 2, - 'ln_class': 'MMXU', - 'units': 'None', - 'minimum': 1, - 'data_object': 'PFSign', - 'allowed_values': { - '1': 'IEC active power', - '2': 'IEEE lead/lag' - }, - 'type': 'enumerated', - 'name': 'MMXU.PFSign.AO4' -} - -AO_244 = { - 'index': 244, - 'description': 'Curve Edit Selector. Writing to this point selects ' - 'which of the curves can currently be viewed and changed.', - 'data_type': 'AO', - 'common_data_class': 'ORG', - 'ln_class': 'DGSM', - 'minimum': 1, - 'data_object': 'InCrv', - 'name': 'DGSMn.InCrv.AO244', - 'type': 'selector_block', - 'selector_block_start': 244, - 'selector_block_end': 448 - } - - -class TestPointDefinition: - - @property - def point_json(self): - return copy.deepcopy(AO_4) - - @staticmethod - def validate_point_definition(point_json): - exception = {} - try: - PointDefinition(point_json) - except Exception as err: - exception['key'] = type(err).__name__ - exception['error'] = str(err) - return exception - - def test_valid_point_definition(self): - exception = self.validate_point_definition(self.point_json) - assert exception == {} - - def test_missing_point_name(self): - """Test raising exception if point definition missing point name""" - point_json = self.point_json - point_json.pop('name') - exception = self.validate_point_definition(point_json) - assert exception == { - 'key': 'ValueError', - 'error': 'Missing point name' - } - - def test_missing_index(self): - """Test raising exception if point definition missing point index""" - point_json = self.point_json - point_json.pop('index') - exception = self.validate_point_definition(point_json) - assert exception == { - 'key': 'ValueError', - 'error': 'Missing index for point {}'.format(self.point_json['name']) - } - - def test_missing_data_type(self): - """Test raising exception if point definition missing data_type""" - point_json = self.point_json - point_json.pop('data_type') - exception = self.validate_point_definition(point_json) - assert exception == { - 'key': 'ValueError', - 'error': 'Missing data type for point {}'.format(self.point_json['name']) - } - - def test_invalid_event_class(self): - """Test raising exception if event_class is not 0, 1, 2, or 3""" - point_json = self.point_json - point_json.update({ - 'event_class': 4 - }) - exception = self.validate_point_definition(point_json) - assert exception == { - 'key': 'ValueError', - 'error': 'Invalid event class 4 for point {}'.format(self.point_json['name']) - } - - def test_invalid_type(self): - """Test raising exception if type is not array, selector_block, or enumerated""" - point_json = self.point_json - point_json.update({ - 'type': 'regular' - }) - exception = self.validate_point_definition(point_json) - assert exception == { - 'key': 'ValueError', - 'error': 'Invalid type regular for point {}'.format(self.point_json['name']) - } - - def test_missing_response(self): - """Test raising exception if the point action is publish_and_respond but missing response field""" - point_json = self.point_json - point_json.update({ - 'action': 'publish_and_respond' - }) - exception = self.validate_point_definition(point_json) - assert exception == { - 'key': 'ValueError', - 'error': 'Missing response point name for point {}'.format(self.point_json['name']) - } - - def test_missing_allowed_values(self): - """Test raising exception if the enumerated type missing allowed_values map""" - point_json = self.point_json - point_json.pop('allowed_values') - exception = self.validate_point_definition(point_json) - assert exception == { - 'key': 'ValueError', - 'error': 'Missing allowed values mapping for point {}'.format(self.point_json['name']) - } - - def test_invalid_defined_selector_block_start(self): - """Test raising exception if selector_block_start defined for non-selector-block point""" - point_json = self.point_json - point_json.update({ - 'selector_block_start': 244 - }) - exception = self.validate_point_definition(point_json) - assert exception == { - 'key': 'ValueError', - 'error': 'selector_block_start defined for non-selector-block point {}'.format(self.point_json['name']) - } - - def test_invalid_defined_selector_block_end(self): - """Test raising exception if selector_block_end defined for non-selector-block point""" - point_json = self.point_json - point_json.update({ - 'selector_block_end': 448 - }) - exception = self.validate_point_definition(point_json) - assert exception == { - 'key': 'ValueError', - 'error': 'selector_block_end defined for non-selector-block point {}'.format(self.point_json['name']) - } - - -class TestPointDefinitions: - """Regression tests for the Mesa Agent.""" - - def test_load_points_from_json_file(self): - try: - PointDefinitions(point_definitions_path=POINT_DEFINITIONS_PATH) - assert True - except ValueError: - assert False \ No newline at end of file diff --git a/services/core/DNP3Agent/tests/unit_test_point_definitions.py b/services/core/DNP3Agent/tests/unit_test_point_definitions.py deleted file mode 100644 index 896dffe088..0000000000 --- a/services/core/DNP3Agent/tests/unit_test_point_definitions.py +++ /dev/null @@ -1,212 +0,0 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, 8minutenergy / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Neither 8minutenergy nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by 8minutenergy or Kisensum. -# }}} - -import pytest -try: - import dnp3 -except ImportError: - pytest.skip("pydnp3 not found!", allow_module_level=True) - -from dnp3.points import ArrayHeadPointDefinition, PointDefinitions, PointValue -from dnp3.mesa.agent import MesaAgent -from dnp3.mesa.functions import FunctionDefinitions - -from test_mesa_agent import POINT_DEFINITIONS_PATH, FUNCTION_DEFINITIONS_PATH - - -def test_point_definition_load(): - point_defs = PointDefinitions(point_definitions_path=POINT_DEFINITIONS_PATH) - import pprint - pprint.pprint(point_defs._points) - pprint.pprint(point_defs._point_name_dict) - print("_point_variations_dict") - pprint.pprint(point_defs._point_variation_dict) - - -def test_point_definition(): - - test_dict = { - "name": "CurveStart-X", - "type": "array", # Start of the curve's X/Y array - "array_times_repeated": 100, - "group": 40, - "variation": 1, - "index": 207, - "description": "Starting index for a curve of up to 99 X/Y points", - "array_points": [ - { - "name": "Curve-X" - }, - { - "name": "Curve-Y" - } - ] - } - test_def = ArrayHeadPointDefinition(test_dict) - print(test_def) - - -def send_points(mesa_agent, some_points): - - for name, value, index in some_points: - pdef = mesa_agent.point_definitions.get_point_named(name,index) - point_value = PointValue('Operate', - None, - value, - pdef, - pdef.index, - None) # What is op_type used for? - - print(point_value) - mesa_agent._process_point_value(point_value) - - -def test_mesa_agent(): - mesa_agent = MesaAgent(point_topic='points_foobar', local_ip='127.0.0.1', port=8999, outstation_config={}, - function_topic='functions_foobar', outstation_status_topic='', - local_point_definitions_path=POINT_DEFINITIONS_PATH, - local_function_definitions_path=FUNCTION_DEFINITIONS_PATH) - - mesa_agent._configure('', '', {}) - point_definitions = mesa_agent.point_definitions - supported_pdef = point_definitions.get_point_named("Supports Charge/Discharge Mode") - mesa_agent.update_input_point(supported_pdef, True) - - test_points = ( - # ("DCHD.WinTms (out)", 1.0), - # ("DCHD.RmpTms (out)", 2.0), - # ("DCHD.RevtTms (out)", 3.0), - ("CurveStart-X", 1.0, None), - ("CurveStart-X", 2.0, 208), - - ) - send_points(mesa_agent, test_points) - - -def test_mesa_agent_2(): - mesa_agent = MesaAgent(point_topic='points_foobar', local_ip='127.0.0.1', port=8999, outstation_config={}, - function_topic='functions_foobar', outstation_status_topic='', - local_point_definitions_path=POINT_DEFINITIONS_PATH, - local_function_definitions_path=FUNCTION_DEFINITIONS_PATH) - - mesa_agent._configure('', '', {}) - point_definitions = mesa_agent.point_definitions - supported_pdef = point_definitions.get_point_named("Supports Charge/Discharge Mode") - mesa_agent.update_input_point(supported_pdef, True) - - test_points = ( - ("DCHD.WinTms (out)", 1.0, None), - #("DCHD.RmpTms (out)", 2.0, None), - ("DCHD.RevtTms (out)", 3.0, None), - - ) - send_points(mesa_agent, test_points) - - -def test_function_definitions(): - point_definitions = PointDefinitions(point_definitions_path=POINT_DEFINITIONS_PATH) - fdefs = FunctionDefinitions(point_definitions, function_definitions_path=FUNCTION_DEFINITIONS_PATH) - - fd = fdefs.function_for_id("curve") - print(fd) - - pdef = point_definitions.get_point_named("DCHD.WinTms (out)") - print(pdef) - print(fdefs.step_definition_for_point(pdef)) - - -def test_selector_block(): - """ - Test send a Curve function / selector block (including an array of points) to MesaAgent. - Get MesaAgent's selector block and confirm that it has the correct contents. - Do this for a variety of Edit Selectors and array contents. - """ - - def process_block_points(agt, block_points, edit_selector): - """Send each point value in block_points to the MesaAgent.""" - # print('Processing {}'.format(block_points)) - for name, value, index in block_points: - point_definitions = agt.point_definitions - pdef = point_definitions.get_point_named(name, index) - point_value = PointValue('Operate', None, value, pdef, pdef.index, None) - agt._process_point_value(point_value) - returned_block = mesa_agent.get_selector_block('Curve Edit Selector', edit_selector) - # print('get_selector_block returned {}'.format(returned_block)) - return returned_block - - mesa_agent = MesaAgent(point_topic='points_foobar', local_ip='127.0.0.1', port=8999, outstation_config={}, - function_topic='functions_foobar', outstation_status_topic='', - local_point_definitions_path=POINT_DEFINITIONS_PATH, - local_function_definitions_path=FUNCTION_DEFINITIONS_PATH) - mesa_agent._configure('', '', {}) - - block_1_points = [('Curve Edit Selector', 1, None), # index 191 - Function and SelectorBlock start - ('CurveStart-X', 1.0, None), # Point #1-X: index 207 - Array start - ('CurveStart-X', 2.0, 208), # Point #1-Y - ('Curve Number of Points', 1, None)] # index 196 - Curve function end - block_2_points = [('Curve Edit Selector', 2, None), # index 191 - Function and SelectorBlock start - ('CurveStart-X', 1.0, None), # Point #1-X: index 207 - Array start - ('CurveStart-X', 2.0, 208), # Point #1-Y - ('CurveStart-X', 3.0, 209), # Point #2-X - ('CurveStart-X', 4.0, 210), # Point #2-Y - ('Curve Number of Points', 2, None)] # index 196 - Curve function end - block_2a_points = [('Curve Edit Selector', 2, None), # index 191 - Function and SelectorBlock start - ('CurveStart-X', 1.0, None), # Point #1-X: index 207 - Array start - ('CurveStart-X', 2.0, 208), # Point #1-Y - ('CurveStart-X', 5.0, 211), # Point #3-X - ('CurveStart-X', 6.0, 212), # Point #3-Y - ('Curve Number of Points', 3, None)] # index 196 - Curve function end - - # Send block #1. Confirm that its array has a point with Y=2.0. - block = process_block_points(mesa_agent, block_1_points, 1) - assert block['CurveStart-X'][0]['Curve-Y'] == 2.0 - - # Send block #2. Confirm that its array has a point #2 with Y=4.0. - block = process_block_points(mesa_agent, block_2_points, 2) - assert block['CurveStart-X'][1]['Curve-Y'] == 4.0 - - # Send an updated block #2 with no point #2 and a new point #3. - block = process_block_points(mesa_agent, block_2a_points, 2) - # Confirm that its array still has a point #2 with Y=4.0, even though it wasn't just sent. - assert block['CurveStart-X'][1]['Curve-Y'] == 4.0 - # Confirm that its array now has a point #3 with Y=6.0. - assert block['CurveStart-X'][2]['Curve-Y'] == 6.0 - - # Re-send block #1. Confirm that selector block initialization reset the point cache: the array has no second point. - block = process_block_points(mesa_agent, block_1_points, 1) - assert len(block['CurveStart-X']) == 1 - - -if __name__ == "__main__": - # test_mesa_agent() - # test_mesa_agent_2() - # test_function_definitions() - # test_point_definition() - test_point_definition_load() - # test_selector_block() diff --git a/services/core/DNP3OutstationAgent/README.md b/services/core/DNP3OutstationAgent/README.md new file mode 100644 index 0000000000..b25354d874 --- /dev/null +++ b/services/core/DNP3OutstationAgent/README.md @@ -0,0 +1,328 @@ +# DNP3 Outstation Agent + +Distributed Network Protocol (DNP or DNP3) has achieved a large-scale acceptance since its introduction in 1993. This +protocol is an immediately deployable solution for monitoring remote sites because it was developed for communication of +critical infrastructure status, allowing for reliable remote control. + +GE-Harris Canada (formerly Westronic, Inc.) is generally credited with the seminal work on the protocol. This protocol +is, however, currently implemented by an extensive range of manufacturers in a variety of industrial applications, such +as electric utilities. + +DNP3 is composed of three layers of the OSI seven-layer functions model. These layers are application layer, data link +layer, and transport layer. Also, DNP3 can be transmitted over a serial bus connection or over a TCP/IP network. + +# Prerequisites + +* Python 3.8 + + +# Installation + +1. Install volttron and start the platform. + + Refer to the [VOLTTRON Quick Start](https://volttron.readthedocs.io/en/main/tutorials/quick-start.html) to install + the VOLTTRON platform. + + ```shell + ... + # Activate the virtual enviornment + $ source env/bin/activate + + # Start the platform + (volttron) $ ./start-volttron + + # Check (installed) agent status + (volttron) $ vctl status + UUID AGENT IDENTITY TAG STATUS HEALTH + 75 listeneragent-3.3 listeneragent-3.3_1 listener + 2f platform_driveragent-4.0 platform.driver platform_driver + ``` + +1. (If not satisfied yet,) install [dnp3-python](https://pypi.org/project/dnp3-python/) dependency. + + ```shell + (volttron) $ pip install dnp3-python==0.2.3b3 + ``` + +1. Install and start the DNP3 Outstation Agent. + + Install the DNP3 Outstation agent with the following command: + + ```shell + (volttron) $ vctl install \ + --agent-config \ + --tag \ + --vip-identity \ + -f \ + --start + ``` + + Assuming at the package root path, installing a dnp3-agent with [example-config.json](example-config.json), called " + dnp3-outstation-agent". + + ```shell + (volttron) $ vctl install ./services/core/DNP3OutstationAgent/ \ + --agent-config services/core/DNP3OutstationAgent/example-config.json \ + --tag dnp3-outstation-agent \ + --vip-identity dnp3-outstation-agent \ + -f \ + --start + + # >> + Agent 2e37a3bc-4438-4d52-8e05-cb6703cf3760 installed and started [11074] + ``` + + Please see more details about agent installation with `vctl install -h`. + +1. View the status of the installed agent (and notice a new dnp3 outstation agent is installed and running.) + + ```shell + (volttron) $ vctl status + UUID AGENT IDENTITY TAG STATUS HEALTH + 2e dnp3_outstation_agentagent-0.2.0 dnp3-outstation-agent dnp3-outstation-agent running [11074] GOOD + 75 listeneragent-3.3 listeneragent-3.3_1 listener + 2f platform_driveragent-4.0 platform.driver platform_driver + ``` + +1. Verification + + The dnp3 outstation agent acts as a server, and we will demonstrate a typical use case in the "Demonstration" + session. + +# Agent Configuration + +The required parameters for this agent are "outstation_ip", "port", "master_id", and "outstation_id". +Below is an example configuration can be found at [example-config.json](example-config.json). + +``` + { + 'outstation_ip': '0.0.0.0', + 'port': 20000, + 'master_id': 2, + 'outstation_id': 1 + } +``` + +Note: as part of the Volttron configuration framework, this file will be added to +the `$VOLTTRON_HOME/agents////` as `config`, +e.g. `~/.volttron/agents/94e54843-4bd4-45d7-9a92-3d18588b5682/dnp3_outstation_agentagent-0.2.0/dnp3_outstation_agentagent-0.2.0.dist-info/config` + +# Demonstration + +If you don't have a dedicated DNP3 Master to test the DNP3 outstation agent against, you can setup a local DNP3 Master +instead. This DNP3 Master will +be hosted at localhost on a specific port (port 20000 by default, i.e. 127.0.0.1:20000). +This Master will communicate with the DNP3 outstation agent. + +To setup a local master, we can utilize the dnp3demo module from the dnp3-python dependency. For more information about +the dnp3demo module, please refer +to [dnp3demo-Module.md](https://github.com/VOLTTRON/dnp3-python/blob/develop/docs/dnp3demo-Module.md) + +## Setup DNP3 Master + +1. Verify [dnp3-python](https://pypi.org/project/dnp3-python/) is installed properly: + + ```shell + (volttron) $ pip list | grep dnp3 + dnp3-python 0.2.3b2 + + (volttron) $ dnp3demo + ms(1676667858612) INFO manager - Starting thread (0) + ms(1676667858612) WARN server - Address already in use + 2023-02-17 15:04:18,612 dnp3demo.data_retrieval_demo DEBUG Initialization complete. OutStation in command loop. + ms(1676667858613) INFO manager - Starting thread (0) + channel state change: OPENING + ms(1676667858613) INFO tcpclient - Connecting to: 127.0.0.1 + 2023-02-17 15:04:18,613 dnp3demo.data_retrieval_demo DEBUG Initialization complete. Master Station in command loop. + ms(1676667858613) INFO tcpclient - Connected to: 127.0.0.1 + channel state change: OPEN + 2023-02-17 15:04:19.615457 ============count 1 + ====== Outstation update index 0 with 8.465443888876885 + ====== Outstation update index 1 with 17.77180643225464 + ====== Outstation update index 2 with 27.730343174887107 + ====== Outstation update index 0 with False + ====== Outstation update index 1 with True + + ... + + 2023-02-17 15:04:22,839 dnp3demo.data_retrieval_demo DEBUG Exiting. + channel state change: CLOSED + channel state change: SHUTDOWN + ms(1676667864841) INFO manager - Exiting thread (0) + ms(1676667870850) INFO manager - Exiting thread (0) + ``` + +1. Run a DNP3 Master at local (with the default parameters) + + Assuming the DNP3 outstation agent is running, run the following commands and expect the similar output. + ```shell + (volttron) $ dnp3demo master + dnp3demo.run_master {'command': 'master', 'master_ip': '0.0.0.0', 'outstation_ip': '127.0.0.1', 'port': 20000, 'master_id': 2, 'outstation_id': 1} + ms(1676668214630) INFO manager - Starting thread (0) + 2023-02-17 15:10:14,630 control_workflow_demo INFO Communication Config + 2023-02-17 15:10:14,630 control_workflow_demo INFO Communication Config + 2023-02-17 15:10:14,630 control_workflow_demo INFO Communication Config + channel state change: OPENING + ms(1676668214630) INFO tcpclient - Connecting to: 127.0.0.1 + ms(1676668214630) INFO tcpclient - Connected to: 127.0.0.1 + channel state change: OPEN + 2023-02-17 15:10:14,630 control_workflow_demo DEBUG Initialization complete. Master Station in command loop. + 2023-02-17 15:10:14,630 control_workflow_demo DEBUG Initialization complete. Master Station in command loop. + 2023-02-17 15:10:14,630 control_workflow_demo DEBUG Initialization complete. Master Station in command loop. + ==== Master Operation MENU ================================== + - set analog-output point value (for remote control) + - set binary-output point value (for remote control) +
- display/polling (outstation) database + - display configuration + ================================================================= + + ======== Your Input Here: ==(master)====== + + ``` + + Note: if the dnp3 agent is not running, you might observe the following output instead + ``` + Start retry... + Communication error. + Communication Config {'masterstation_ip_str': '0.0.0.0', 'outstation_ip_str': '127.0.0.1', 'port': 20000, 'masterstation_id_int': 2, 'outstation_id_int': 1} + ... + ``` + + This Master station runs at port 20000 by default. Please see `dnp3demo master -h` for configuration options. + Note: If using customized master parameter, please make sure the DNP3 Outstation Agent is configured accordingly. + Please refer to [DNP3-Primer.md](https://github.com/VOLTTRON/dnp3-python/blob/develop/docs/DNP3-Primer.md) for DNP3 + protocol fundamentals including connection settings. + +## Basic operation demo + +The dnp3demo master submodule is an interactive CLI tool to communicate with an outstation. The available options are +shown in the "Master Operation MENU" and should be self-explanatory. Here we can demonstrate
and commands. + +1.
- display/polling (outstation) database + + ```shell + ======== Your Input Here: ==(master)====== + dd + You chose < dd > - display database + {'Analog': {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0}, 'AnalogOutputStatus': {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0}, 'Binary': {0: False, 1: False, 2: False, 3: False, 4: False, 5: False, 6: False, 7: False, 8: False, 9: False}, 'BinaryOutputStatus': {0: False, 1: False, 2: False, 3: False, 4: False, 5: False, 6: False, 7: False, 8: False, 9: False}} + ==== Master Operation MENU ================================== + - set analog-output point value (for remote control) + - set binary-output point value (for remote control) +
- display/polling (outstation) database + - display configuration + ================================================================= + + ``` + + Note that an outstation is initialed with "0.0" for Analog-type points, and "False" for Binary-type points, hence the + output displayed above. + +1. - set analog-output point value (for remote control) + + ```shell + ======== Your Input Here: ==(master)====== + ao + You chose - set analog-output point value + Type in and . Separate with space, then hit ENTER. + Type 'q', 'quit', 'exit' to main menu. + + ======== Your Input Here: ==(master)====== + 0.1233 0 + SUCCESS {'AnalogOutputStatus': {0: 0.1233, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0}} + You chose - set analog-output point value + Type in and . Separate with space, then hit ENTER. + Type 'q', 'quit', 'exit' to main menu. + + ======== Your Input Here: ==(master)====== + 1.3223 1 + SUCCESS {'AnalogOutputStatus': {0: 0.1233, 1: 1.3223, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0}} + You chose - set analog-output point value + Type in and . Separate with space, then hit ENTER. + Type 'q', 'quit', 'exit' to main menu. + + ======== Your Input Here: ==(master)====== + q + ==== Master Operation MENU ================================== + - set analog-output point value (for remote control) + - set binary-output point value (for remote control) +
- display/polling (outstation) database + - display configuration + ================================================================= + + ======== Your Input Here: ==(master)====== + dd + You chose < dd > - display database + {'Analog': {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0}, 'AnalogOutputStatus': {0: 0.1233, 1: 1.3223, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0, 6: 0.0, 7: 0.0, 8: 0.0, 9: 0.0}, 'Binary': {0: False, 1: False, 2: False, 3: False, 4: False, 5: False, 6: False, 7: False, 8: False, 9: False}, 'BinaryOutputStatus': {0: False, 1: False, 2: False, 3: False, 4: False, 5: False, 6: False, 7: False, 8: False, 9: False}} + + ``` + + Explain of the operation and expected output + * We type "ao" (stands for "analog output") to enter the set point dialog. + * We set AnalogOut index0==0.1233, (the prompt indicates the operation is successful.) + * Then, we set AnalogOut index1==1.3223, (again, the prompt indicates the operation is successful.) + * We type "q" (stands for "quit") to exit the set point dialog. + * We use "dd" command and verified that AnalogOutput values are consistent to what we set ealier. + +1. Bonus script for running DNP3 outstation agent interactively + + Similar to the interactive dnp3demo master submodule, we can run the dnp3 outstation agent interactively from the + command line using [run_dnp3_outstation_agent_script.py](demo-scripts/run_dnp3_outstation_agent_script.py). + + ```shell + (volttron) $ python services/core/DNP3OutstationAgent/demo-scripts/run_dnp3_outstation_agent_script.py + ... + 2023-02-17 15:58:04,123 volttron.platform.vip.agent.core INFO: Connected to platform: router: a2ae7a58-6ce7-4386-b5eb-71e386075c15 version: 1.0 identity: e91d54f6-d4ff-4fe5-afcb-cf8f360e84af + 2023-02-17 15:58:04,123 volttron.platform.vip.agent.core DEBUG: Running onstart methods. + 2023-02-17 15:58:07,137 volttron.platform.vip.agent.subsystems.auth WARNING: Auth entry not found for e91d54f6-d4ff-4fe5-afcb-cf8f360e84af: rpc_method_authorizations not updated. If this agent does have an auth entry, verify that the 'identity' field has been included in the auth entry. This should be set to the identity of the agent + ========================= MENU ================================== + - set analog-input point value + - set analog-output point value + - set binary-input point value + - set binary-output point value + +
- display database + - display (outstation) info + - config then restart outstation + + ``` + + dd command + ```shell + ======== Your Input Here: ==(DNP3 OutStation Agent)====== + dd + You chose
- display database + {'Analog': {'0': None, '1': None, '2': None, '3': None, '4': None, '5': None, '6': None, '7': None, '8': None, '9': None}, 'AnalogOutputStatus': {'0': 0.1233, '1': 1.3223, '2': None, '3': None, '4': None, '5': None, '6': None, '7': None, '8': None, '9': None}, 'Binary': {'0': None, '1': None, '2': None, '3': None, '4': None, '5': None, '6': None, '7': None, '8': None, '9': None}, 'BinaryOutputStatus': {'0': None, '1': None, '2': None, '3': None, '4': None, '5': None, '6': None, '7': None, '8': None, '9': None}} + + ``` + + Note: [run_dnp3_outstation_agent_script.py](demo-scripts/run_dnp3_outstation_agent_script.py) script is a wrapper on + the dnp3demo outstation submodle. For details about the interactive dnp3 station operations, please refer + to [dnp3demo-Module.md](https://github.com/VOLTTRON/dnp3-python/blob/develop/docs/dnp3demo-Module.md) + +# Run Tests + +1. Install volttron testing dependencies + ```shell + (volttron) $ python bootstrap.py --testing + UPDATE: ['testing'] + Installing required packages + + pip install --upgrade --no-deps wheel==0.30 + Requirement already satisfied: wheel==0.30 in ./env/lib/python3.10/site-packages (0.30.0) + + pip install --upgrade --install-option --zmq=bundled --no-deps pyzmq==22.2.1 + WARNING: Disabling all use of wheels due to the use of --build-option / --global-option / --install-option. + ... + ``` + +1. Run pytest + ```shell + (volttron) $ pytest services/core/DNP3OutstationAgent/tests/. + ===================================================================================================== test session starts ===================================================================================================== + platform linux -- Python 3.10.6, pytest-7.1.2, pluggy-1.0.0 -- /home/kefei/project/volttron/env/bin/python + cachedir: .pytest_cache + rootdir: /home/kefei/project/volttron, configfile: pytest.ini + plugins: rerunfailures-10.2, asyncio-0.19.0, timeout-2.1.0 + asyncio: mode=auto + timeout: 300.0s + timeout method: signal + timeout func_only: False + collected 40 items + ``` diff --git a/services/core/DNP3OutstationAgent/conftest.py b/services/core/DNP3OutstationAgent/conftest.py new file mode 100644 index 0000000000..8559470457 --- /dev/null +++ b/services/core/DNP3OutstationAgent/conftest.py @@ -0,0 +1,6 @@ +import sys + +from volttrontesting.fixtures.volttron_platform_fixtures import * + +# Add system path of the agent's directory +sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) \ No newline at end of file diff --git a/services/core/DNP3OutstationAgent/demo-scripts/installation-script-notes.txt b/services/core/DNP3OutstationAgent/demo-scripts/installation-script-notes.txt new file mode 100644 index 0000000000..232c7b0e36 --- /dev/null +++ b/services/core/DNP3OutstationAgent/demo-scripts/installation-script-notes.txt @@ -0,0 +1,5 @@ +python scripts/install-agent.py -s services/core/DNP3OutstationAgent/ \ + -c services/core/DNP3OutstationAgent/config \ + -t dnp3-outstation-agent \ + -i dnp3-outstation-agent \ + -f diff --git a/services/core/DNP3OutstationAgent/demo-scripts/rpc_example.py b/services/core/DNP3OutstationAgent/demo-scripts/rpc_example.py new file mode 100644 index 0000000000..6ece1cd166 --- /dev/null +++ b/services/core/DNP3OutstationAgent/demo-scripts/rpc_example.py @@ -0,0 +1,80 @@ +""" +A demo to test dnp3-driver get_point method using rpc call. +Pre-requisite: +- install platform-driver +- configure dnp3-driver +- a dnp3 outstation/server is up and running +- platform-driver is up and running +""" + +from volttron.platform.vip.agent.utils import build_agent +from time import sleep +import datetime + + +def main(): + a = build_agent() + + # peer = "test-agent" + # peer_method = "outstation_get_config" + # + # rs = a.vip.rpc.call(peer, peer_method, ).get(timeout=10) + # print(datetime.datetime.now(), "rs: ", rs) + + peer = "dnp3-agent" + + peer_method = "get_volttron_config" + rs = a.vip.rpc.call(peer, peer_method).get(timeout=10) + print(datetime.datetime.now(), "rs: ", rs) + + # peer_method = "set_volttron_config" + # rs = a.vip.rpc.call(peer, peer_method, port=100, unused_key="unused").get(timeout=10) + # print(datetime.datetime.now(), "rs: ", rs) + # + # peer_method = "demo_config_store" + # rs = a.vip.rpc.call(peer, peer_method).get(timeout=10) + # print(datetime.datetime.now(), "rs: ", rs) + + peer_method = "set_volttron_config" + rs = a.vip.rpc.call(peer, peer_method, port=31000).get(timeout=10) + print(datetime.datetime.now(), "rs: ", rs) + + # peer_method = "outstation_get_is_connected" + # rs = a.vip.rpc.call(peer, peer_method).get(timeout=10) + # print(datetime.datetime.now(), "rs: ", rs) + + peer_method = "outstation_reset" + rs = a.vip.rpc.call(peer, peer_method).get(timeout=10) + print(datetime.datetime.now(), "rs: ", rs) + + + + # while True: + # sleep(5) + # print("============") + # try: + # peer = "test-agent" + # peer_method = "outstation_display_db" + # + # rs = a.vip.rpc.call(peer, peer_method).get(timeout=10) + # print(datetime.datetime.now(), "rs: ", rs) + # + # # rs = a.vip.rpc.call(peer, peer_method, arg1="173", arg2="arg2222", + # # something="something-else" + # # ).get(timeout=10) + # + # # rs = a.vip.rpc.call(peer, peer_method, "173", "arg2222", + # # "something-else" + # # ).get(timeout=10) + # # print(datetime.datetime.now(), "rs: ", rs) + # # reg_pt_name = "AnalogInput_index1" + # # rs = a.vip.rpc.call("platform.driver", rpc_method, + # # device_name, + # # reg_pt_name).get(timeout=10) + # # print(datetime.datetime.now(), "point_name: ", reg_pt_name, "value: ", rs) + # except Exception as e: + # print(e) + + +if __name__ == "__main__": + main() diff --git a/services/core/DNP3OutstationAgent/demo-scripts/run_dnp3_outstation_agent_script.py b/services/core/DNP3OutstationAgent/demo-scripts/run_dnp3_outstation_agent_script.py new file mode 100644 index 0000000000..c061c9a666 --- /dev/null +++ b/services/core/DNP3OutstationAgent/demo-scripts/run_dnp3_outstation_agent_script.py @@ -0,0 +1,261 @@ +import logging +import sys +import argparse + +from pydnp3 import opendnp3 +# from dnp3_python.dnp3station.outstation import MyOutStation + +from time import sleep +from volttron.platform.vip.agent.utils import build_agent +from services.core.DNP3OutstationAgent.dnp3_outstation_agent.agent import Dnp3Agent as Dnp3OutstationAgent # agent +from volttron.platform.vip.agent import Agent + +import logging +import sys +import argparse + +# from pydnp3 import opendnp3 +# from dnp3_python.dnp3station.outstation_new import MyOutStationNew + +from time import sleep + +# from volttron.client.vip.agent import build_agent +# from dnp3_outstation.agent import Dnp3OutstationAgent +# from volttron.client.vip.agent import Agent + +DNP3_AGENT_ID = "dnp3_outstation" + +stdout_stream = logging.StreamHandler(sys.stdout) +stdout_stream.setFormatter(logging.Formatter('%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s')) + +_log = logging.getLogger(__name__) +# _log = logging.getLogger("control_workflow_demo") +_log.addHandler(stdout_stream) +_log.setLevel(logging.INFO) + + +def input_prompt(display_str=None) -> str: + if display_str is None: + display_str = f""" +======== Your Input Here: ==(DNP3 OutStation Agent)====== +""" + return input(display_str) + + +def setup_args(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: + parser.add_argument("-aid", "--agent-identity", action="store", default=DNP3_AGENT_ID, type=str, + metavar="", + help=f"specify agent identity (parsed as peer-name for rpc call), default '{DNP3_AGENT_ID}'.") + + return parser + + +def print_menu(): + welcome_str = rf""" +========================= MENU ================================== + - set analog-input point value + - set analog-output point value + - set binary-input point value + - set binary-output point value + +
- display database + - display (outstation) info + - config then restart outstation +================================================================= +""" + print(welcome_str) + + +def check_agent_id_existence(agent_id: str, vip_agent: Agent): + rs = vip_agent.vip.peerlist.list().get(5) + if agent_id not in rs: + raise ValueError(f"There is no agent named `{agent_id}` available on the message bus." + f"Available peers are {rs}") + # _log.warning(f"There is no agent named `{agent_id}` available on the message bus." + # f"Available peers are {rs}") + + +def main(parser=None, *args, **kwargs): + if parser is None: + # Initialize parser + parser = argparse.ArgumentParser( + prog="dnp3-outstation", + description=f"Run a dnp3 outstation agent. Specify agent identity, by default `{DNP3_AGENT_ID}`", + # epilog="Thanks for using %(prog)s! :)", + ) + parser = setup_args(parser) + + # Read arguments from command line + args = parser.parse_args() + # create volttron vip agent to evoke dnp3-agent rpc calls + a = build_agent() + peer = args.agent_identity # note: default {DNP3_AGENT_ID} or "test-agent" + # print(f"========= peer {peer}") + check_agent_id_existence(peer, a) + + def get_db_helper(): + _peer_method = Dnp3OutstationAgent.display_outstation_db.__name__ + _db_print = a.vip.rpc.call(peer, _peer_method).get(timeout=10) + return _db_print + + def get_config_helper(): + _peer_method = Dnp3OutstationAgent.get_outstation_config.__name__ + _config_print = a.vip.rpc.call(peer, _peer_method).get(timeout=10) + _config_print.update({"peer": peer}) + return _config_print + + sleep(2) + # Note: if without sleep(2) there will be a glitch when first send_select_and_operate_command + # (i.e., all the values are zero, [(0, 0.0), (1, 0.0), (2, 0.0), (3, 0.0)])) + # since it would not update immediately + + count = 0 + while count < 1000: + # sleep(1) # Note: hard-coded, master station query every 1 sec. + count += 1 + # print(f"=========== Count {count}") + peer_method = Dnp3OutstationAgent.is_outstation_connected + if a.vip.rpc.call(peer, peer_method.__name__, ).get(timeout=10): + # print("Communication Config", master_application.get_config()) + print_menu() + else: + print_menu() + print("!!!!!!!!! WARNING: The outstation is NOT connected !!!!!!!!!") + print(get_config_helper()) + # else: + # print("Communication error.") + # # print("Communication Config", outstation_application.get_config()) + # print(get_config_helper()) + # print("Start retry...") + # sleep(2) + # continue + + # print_menu() + option = input_prompt() # Note: one of ["ai", "ao", "bi", "bo", "dd", "dc"] + while True: + if option == "ai": + print("You chose - set analog-input point value") + print("Type in and . Separate with space, then hit ENTER. e.g., `1.4321, 1`.") + print("Type 'q', 'quit', 'exit' to main menu.") + input_str = input_prompt() + if input_str in ["q", "quit", "exit"]: + break + try: + p_val = float(input_str.split(" ")[0]) + index = int(input_str.split(" ")[1]) + # outstation_application.apply_update(opendnp3.Analog(value=p_val), index) + # result = {"Analog": outstation_application.db_handler.db.get("Analog")} + method = Dnp3OutstationAgent.apply_update_analog_input + peer_method = method.__name__ # i.e., "apply_update_analog_input" + response = a.vip.rpc.call(peer, peer_method, p_val, index).get(timeout=10) + result = {"Analog": get_db_helper().get("Analog")} + print(result) + sleep(2) + except Exception as e: + print(f"your input string '{input_str}'") + print(e) + elif option == "ao": + print("You chose - set analog-output point value") + print("Type in and . Separate with space, then hit ENTER. e.g., `0.1234, 0`.") + print("Type 'q', 'quit', 'exit' to main menu.") + input_str = input_prompt() + if input_str in ["q", "quit", "exit"]: + break + try: + p_val = float(input_str.split(" ")[0]) + index = int(input_str.split(" ")[1]) + method = Dnp3OutstationAgent.apply_update_analog_output + peer_method = method.__name__ # i.e., "apply_update_analog_input" + response = a.vip.rpc.call(peer, peer_method, p_val, index).get(timeout=10) + result = {"AnalogOutputStatus": get_db_helper().get("AnalogOutputStatus")} + print(result) + sleep(2) + except Exception as e: + print(f"your input string '{input_str}'") + print(e) + elif option == "bi": + print("You chose - set binary-input point value") + print("Type in <[1/0]> and . Separate with space, then hit ENTER. e.g., `1, 0`.") + input_str = input_prompt() + if input_str in ["q", "quit", "exit"]: + break + try: + p_val_input = input_str.split(" ")[0] + if p_val_input not in ["0", "1"]: + raise ValueError("binary-output value only takes '0' or '1'.") + else: + p_val = True if p_val_input == "1" else False + index = int(input_str.split(" ")[1]) + method = Dnp3OutstationAgent.apply_update_binary_input + peer_method = method.__name__ + response = a.vip.rpc.call(peer, peer_method, p_val, index).get(timeout=10) + result = {"Binary": get_db_helper().get("Binary")} + print(result) + sleep(2) + except Exception as e: + print(f"your input string '{input_str}'") + print(e) + elif option == "bo": + print("You chose - set binary-output point value") + print("Type in <[1/0]> and . Separate with space, then hit ENTER. e.g., `1, 0`.") + input_str = input_prompt() + if input_str in ["q", "quit", "exit"]: + break + try: + p_val_input = input_str.split(" ")[0] + if p_val_input not in ["0", "1"]: + raise ValueError("binary-output value only takes '0' or '1'.") + else: + p_val = True if p_val_input == "1" else False + index = int(input_str.split(" ")[1]) + method = Dnp3OutstationAgent.apply_update_binary_output + peer_method = method.__name__ + response = a.vip.rpc.call(peer, peer_method, p_val, index).get(timeout=10) + result = {"BinaryOutputStatus": get_db_helper().get("BinaryOutputStatus")} + print(result) + sleep(2) + except Exception as e: + print(f"your input string '{input_str}'") + print(e) + elif option == "dd": + print("You chose
- display database") + print(get_db_helper()) + sleep(2) + break + elif option == "di": + print("You chose - display (outstation) info") + print(get_config_helper()) + sleep(3) + break + elif option == "cr": + print("You chose - config then restart outstation") + print(f"current self.volttron_config is {get_config_helper()}") + print( + "Type in , then hit ENTER. e.g., `20000`." + "(Note: In this script, only support port configuration.)") + # input_str = input_prompt() + input_str = input() + try: + # set_volttron_config + port_val = int(input_str) + method = Dnp3OutstationAgent.update_outstation + peer_method = method.__name__ + response = a.vip.rpc.call(peer, peer_method, port=port_val).get(timeout=10) + print("SUCCESS.", get_config_helper()) + sleep(2) + except Exception as e: + print(f"your input string '{input_str}'") + print(e) + break + else: + print(f"ERROR- your input `{option}` is not one of the following.") + sleep(1) + break + + _log.debug('Exiting.') + # outstation_application.shutdown() + # outstation_application.shutdown() + + +if __name__ == '__main__': + main() diff --git a/services/core/DNP3Agent/dnp3/mesa/__init__.py b/services/core/DNP3OutstationAgent/dnp3_outstation_agent/__init__.py similarity index 100% rename from services/core/DNP3Agent/dnp3/mesa/__init__.py rename to services/core/DNP3OutstationAgent/dnp3_outstation_agent/__init__.py diff --git a/services/core/DNP3OutstationAgent/dnp3_outstation_agent/agent.py b/services/core/DNP3OutstationAgent/dnp3_outstation_agent/agent.py new file mode 100644 index 0000000000..9afb392e23 --- /dev/null +++ b/services/core/DNP3OutstationAgent/dnp3_outstation_agent/agent.py @@ -0,0 +1,567 @@ +""" +Agent documentation goes here. +""" + +__docformat__ = 'reStructuredText' + +import logging +import sys +from volttron.platform.agent import utils +from volttron.platform.vip.agent import Agent, Core, RPC + +# from dnp3_python.dnp3station.outstation import MyOutStation as MyOutStationNew +from dnp3_python.dnp3station.outstation_new import MyOutStationNew +from pydnp3 import opendnp3 +from typing import Dict + + + +_log = logging.getLogger("Dnp3-agent") +utils.setup_logging() +__version__ = "0.2.0" + +_log.level=logging.DEBUG +_log.addHandler(logging.StreamHandler(sys.stdout)) # Note: redirect stdout from dnp3 lib + + +# def agent_main(config_path, **kwargs): +# """ +# Parses the Agent configuration and returns an instance of +# the agent created using that configuration. +# +# Note: config_path is by convention under .volttron home path, called config, e.g. +# /home/kefei/.volttron/agents/6745e0ef-b500-495a-a6e8-120ec0ead4fd/testeragent-0.5/testeragent-0.5.dist-info/config +# +# :param config_path: Path to a configuration file. +# :type config_path: str +# :returns: Tester +# :rtype: Dnp3Agent +# """ +# # _log.info(f"======config_path {config_path}") +# # Note: config_path is by convention under .volttron home path, called config, e.g. +# # /home/kefei/.volttron/agents/6745e0ef-b500-495a-a6e8-120ec0ead4fd/testeragent-0.5/testeragent-0.5.dist-info/config +# # Note: the config file is attached when running `python scripts/install-agent.py -c TestAgent/config` +# # NOte: the config file attached in this way will not appear in the config store. +# # (Need to explicitly using `vctl config store`) +# try: +# config: dict = utils.load_config(config_path) +# except Exception as e: +# _log.info(e) +# config = {} +# +# if not config: +# _log.info("Using Agent defaults for starting configuration.") +# +# setting1 = int(config.get('setting1', 1)) +# setting2 = config.get('setting2', "some/random/topic") +# +# return Dnp3Agent(config, **kwargs) + + +class Dnp3Agent(Agent): + """This is class is a subclass of the Volttron Agent; + This agent is an implementation of a DNP3 outstation; + The agent overrides @Core.receiver methods to modify agent life cycle behavior; + The agent exposes @RPC.export as public interface utilizing RPC calls. + """ + + def __init__(self, config_path: str, **kwargs) -> None: + super(Dnp3Agent, self).__init__(**kwargs) + + # default_config, mainly for developing and testing purposes. + default_config: dict = {'outstation_ip': '0.0.0.0', 'port': 20000, 'master_id': 2, 'outstation_id': 1} + # agent configuration using volttron config framework + # self._dnp3_outstation_config = default_config + config_from_path = self._parse_config(config_path) + + # TODO: improve this logic by refactoring out the MyOutstationNew init, + # and add config from "config store" + try: + _log.info("Using config_from_path {config_from_path}") + self._dnp3_outstation_config = config_from_path + self.outstation_application = MyOutStationNew(**self._dnp3_outstation_config) + except Exception as e: + _log.error(e) + _log.info(f"Failed to use config_from_path {config_from_path}" + f"Using default_config {default_config}") + self._dnp3_outstation_config = default_config + self.outstation_application = MyOutStationNew(**self._dnp3_outstation_config) + + # SubSystem/ConfigStore + self.vip.config.set_default("config", default_config) + self.vip.config.subscribe( + self._config_callback_dummy, # TODO: cleanup: used to be _configure_ven_client + actions=["NEW", "UPDATE"], + pattern="config", + ) # TODO: understand what vip.config.subscribe does + + @property + def dnp3_outstation_config(self): + return self._dnp3_outstation_config + + @dnp3_outstation_config.setter + def dnp3_outstation_config(self, config: dict): + # TODO: add validation + self._dnp3_outstation_config = config + + def _config_callback_dummy(self, config_name: str, action: str, + contents: Dict) -> None: + pass + + @Core.receiver("onstart") + def onstart(self, sender, **kwargs): + """ + This is method is called once the Agent has successfully connected to the platform. + This is a good place to setup subscriptions if they are not dynamic or + do any other startup activities that require a connection to the message bus. + Called after any configurations methods that are called at startup. + Usually not needed if using the configuration store. + """ + + # for dnp3 outstation + self.outstation_application.start() + + # Example publish to pubsub + # self.vip.pubsub.publish('pubsub', "some/random/topic", message="HI!") + # + # # Example RPC call + # # self.vip.rpc.call("some_agent", "some_method", arg1, arg2) + # pass + # self._create_subscriptions(self.setting2) + + # ***************** Helper methods ******************** + def _parse_config(self, config_path: str) -> Dict: + """Parses the agent's configuration file. + + :param config_path: The path to the configuration file + :return: The configuration + """ + # TODO: added capability to configuration based on tabular config file (e.g., csv) + try: + config = utils.load_config(config_path) + except NameError as err: + _log.exception(err) + raise err + except Exception as err: + _log.error("Error loading configuration: {}".format(err)) + config = {} + # print(f"============= def _parse_config config {config}") + if not config: + raise Exception("Configuration cannot be empty.") + return config + + @RPC.export + def rpc_dummy(self) -> str: + """ + For testing rpc call + """ + return "This is a dummy rpc call" + + @RPC.export + def reset_outstation(self): + """update`self._dnp3_outstation_config`, then init a new outstation. + For post-configuration and immediately take effect. + Note: will start a new outstation instance and the old database data will lose""" + # self.dnp3_outstation_config(**kwargs) + # TODO: this method might be refactored as internal helper method for `update_outstation` + try: + self.outstation_application.shutdown() + outstation_app_new = MyOutStationNew(**self.dnp3_outstation_config) + self.outstation_application = outstation_app_new + self.outstation_application.start() + _log.info(f"Outstation has restarted") + except Exception as e: + _log.error(e) + + @RPC.export + def display_outstation_db(self) -> dict: + """expose db""" + return self.outstation_application.db_handler.db + + @RPC.export + def get_outstation_config(self) -> dict: + """expose get_config""" + return self.outstation_application.get_config() + + @RPC.export + def is_outstation_connected(self) -> bool: + """expose is_connected, note: status, property""" + return self.outstation_application.is_connected + + @RPC.export + def apply_update_analog_input(self, val: float, index: int) -> dict: + """public interface to update analog-input point value + val: float + index: int, point index + """ + if not isinstance(val, float): + raise f"val of type(val) should be float" + self.outstation_application.apply_update(opendnp3.Analog(value=val), index) + _log.debug(f"Updated outstation analog-input index: {index}, val: {val}") + + return self.outstation_application.db_handler.db + + @RPC.export + def apply_update_analog_output(self, val: float, index: int) -> dict: + """public interface to update analog-output point value + val: float + index: int, point index + """ + + if not isinstance(val, float): + raise f"val of type(val) should be float" + self.outstation_application.apply_update(opendnp3.AnalogOutputStatus(value=val), index) + _log.debug(f"Updated outstation analog-output index: {index}, val: {val}") + + return self.outstation_application.db_handler.db + + @RPC.export + def apply_update_binary_input(self, val: bool, index: int): + """public interface to update binary-input point value + val: bool + index: int, point index + """ + if not isinstance(val, bool): + raise f"val of type(val) should be bool" + self.outstation_application.apply_update(opendnp3.Binary(value=val), index) + _log.debug(f"Updated outstation binary-input index: {index}, val: {val}") + + return self.outstation_application.db_handler.db + + @RPC.export + def apply_update_binary_output(self, val: bool, index: int): + """public interface to update binary-output point value + val: bool + index: int, point index + """ + if not isinstance(val, bool): + raise f"val of type(val) should be bool" + self.outstation_application.apply_update(opendnp3.BinaryOutputStatus(value=val), index) + _log.debug(f"Updated outstation binary-output index: {index}, val: {val}") + + return self.outstation_application.db_handler.db + + @RPC.export + def update_outstation(self, + outstation_ip: str = None, + port: int = None, + master_id: int = None, + outstation_id: int = None, + **kwargs): + """ + Update dnp3 outstation config and restart the application to take effect. By default, + {'outstation_ip': '0.0.0.0', 'port': 20000, 'master_id': 2, 'outstation_id': 1} + """ + config = self._dnp3_outstation_config.copy() + for kwarg in [{"outstation_ip": outstation_ip}, + {"port": port}, + {"master_id": master_id}, {"outstation_id": outstation_id}]: + if list(kwarg.values())[0] is not None: + config.update(kwarg) + self._dnp3_outstation_config = config + self.reset_outstation() + +# class Dnp3Agent(Agent): +# """ +# Dnp3 agent mainly to represent a dnp3 outstation +# """ +# +# def __init__(self, setting1={}, setting2="some/random/topic", **kwargs): +# # TODO: clean-up the bizarre signature. Note: may need to reinstall the agent for testing. +# super(Dnp3Agent, self).__init__(**kwargs) +# _log.debug("vip_identity: " + self.core.identity) # Note: consistent with IDENTITY in `vctl status` +# +# +# # self.setting1 = setting1 +# # self.setting2 = setting2 +# config_when_installed = setting1 +# # TODO: new-feature: load_config from config store +# # config_at_configstore = +# +# self.default_config = {'outstation_ip': '0.0.0.0', 'port': 20000, +# 'master_id': 2, 'outstation_id': 1} +# # agent configuration using volttron config framework +# # get_volttron_cofig, set_volltron_config +# self._volttron_config: dict +# +# # for dnp3 features +# try: +# self.outstation_application = MyOutStation(**config_when_installed) +# _log.info(f"init dnp3 outstation with {config_when_installed}") +# self._volttron_config = config_when_installed +# except Exception as e: +# _log.error(e) +# self.outstation_application = MyOutStation(**self.default_config) +# _log.info(f"init dnp3 outstation with {self.default_config}") +# self._volttron_config = self.default_config +# # self.outstation_application.start() # moved to onstart +# +# # Set a default configuration to ensure that self.configure is called immediately to setup +# # the agent. +# self.vip.config.set_default(config_name="default-config", contents=self.default_config) +# self.vip.config.set_default(config_name="_volttron_config", contents=self._volttron_config) +# # Hook self.configure up to changes to the configuration file "config". +# self.vip.config.subscribe(self.configure, actions=["NEW", "UPDATE"], pattern="config") +# +# def _get_volttron_config(self): +# return self._volttron_config +# +# def _set_volttron_config(self, **kwargs): +# """set self._volttron_config using **kwargs. +# EXAMPLE +# self.default_config = {'outstation_ip': '0.0.0.0', 'port': 21000, +# 'master_id': 2, 'outstation_id': 1} +# set_volttron_config(port=30000, unused_key="unused") +# # outcome +# self.default_config = {'outstation_ip': '0.0.0.0', 'port': 30000, +# 'master_id': 2, 'outstation_id': 1, +# 'unused_key': 'unused'} +# """ +# self._volttron_config.update(kwargs) +# _log.info(f"Updated self._volttron_config to {self._volttron_config}") +# return {"_volttron_config": self._get_volttron_config()} +# +# @RPC.export +# def outstation_reset(self, **kwargs): +# """update`self._volttron_config`, then init a new outstation. +# +# For post-configuration and immediately take effect. +# Note: will start a new outstation instance and the old database data will lose""" +# self._set_volttron_config(**kwargs) +# try: +# outstation_app_new = MyOutStation(**self._volttron_config) +# self.outstation_application.shutdown() +# self.outstation_application = outstation_app_new +# self.outstation_application.start() +# except Exception as e: +# _log.error(e) +# +# @RPC.export +# def outstation_get_db(self): +# """expose db""" +# return self.outstation_application.db_handler.db +# +# @RPC.export +# def outstation_get_config(self): +# """expose get_config""" +# return self.outstation_application.get_config() +# +# @RPC.export +# def outstation_get_is_connected(self): +# """expose is_connected, note: status, property""" +# return self.outstation_application.is_connected +# +# @RPC.export +# def outstation_apply_update_analog_input(self, val, index): +# """public interface to update analog-input point value +# +# val: float +# index: int, point index +# """ +# if not isinstance(val, float): +# raise f"val of type(val) should be float" +# self.outstation_application.apply_update(opendnp3.Analog(value=val), index) +# _log.debug(f"Updated outstation analog-input index: {index}, val: {val}") +# +# return self.outstation_application.db_handler.db +# +# @RPC.export +# def outstation_apply_update_analog_output(self, val, index): +# """public interface to update analog-output point value +# +# val: float +# index: int, point index +# """ +# +# if not isinstance(val, float): +# raise f"val of type(val) should be float" +# self.outstation_application.apply_update(opendnp3.AnalogOutputStatus(value=val), index) +# _log.debug(f"Updated outstation analog-output index: {index}, val: {val}") +# +# return self.outstation_application.db_handler.db +# +# @RPC.export +# def outstation_apply_update_binary_input(self, val, index): +# """public interface to update binary-input point value +# +# val: bool +# index: int, point index +# """ +# if not isinstance(val, bool): +# raise f"val of type(val) should be bool" +# self.outstation_application.apply_update(opendnp3.Binary(value=val), index) +# _log.debug(f"Updated outstation binary-input index: {index}, val: {val}") +# +# return self.outstation_application.db_handler.db +# +# @RPC.export +# def outstation_apply_update_binary_output(self, val, index): +# """public interface to update binary-output point value +# +# val: bool +# index: int, point index +# """ +# if not isinstance(val, bool): +# raise f"val of type(val) should be bool" +# self.outstation_application.apply_update(opendnp3.BinaryOutputStatus(value=val), index) +# _log.debug(f"Updated outstation binary-output index: {index}, val: {val}") +# +# return self.outstation_application.db_handler.db +# +# @RPC.export +# def outstation_display_db(self): +# return self.outstation_application.db_handler.db +# +# def configure(self, config_name, action, contents): +# """ +# # TODO: clean-up this bizarre method +# """ +# config = self.default_config.copy() +# config.update(contents) +# +# _log.debug("Configuring Agent") +# +# try: +# setting1 = int(config["setting1"]) +# setting2 = str(config["setting2"]) +# except ValueError as e: +# _log.error("ERROR PROCESSING CONFIGURATION: {}".format(e)) +# return +# +# self.setting1 = setting1 +# self.setting2 = setting2 +# +# self._create_subscriptions(self.setting2) +# +# def _create_subscriptions(self, topic): +# """ +# Unsubscribe from all pub/sub topics and create a subscription to a topic in the configuration which triggers +# the _handle_publish callback +# """ +# self.vip.pubsub.unsubscribe("pubsub", None, None) +# +# topic = "some/topic" +# self.vip.pubsub.subscribe(peer='pubsub', +# prefix=topic, +# callback=self._handle_publish) +# +# def _handle_publish(self, peer, sender, bus, topic, headers, message): +# """ +# Callback triggered by the subscription setup using the topic from the agent's config file +# """ +# _log.debug(f" ++++++handleer++++++++++++++++++++++++++" +# f"peer {peer}, sender {sender}, bus {bus}, topic {topic}, " +# f"headers {headers}, message {message}") +# +# @Core.receiver("onstart") +# def onstart(self, sender, **kwargs): +# """ +# This is method is called once the Agent has successfully connected to the platform. +# This is a good place to setup subscriptions if they are not dynamic or +# do any other startup activities that require a connection to the message bus. +# Called after any configurations methods that are called at startup. +# +# Usually not needed if using the configuration store. +# """ +# +# # for dnp3 outstation +# self.outstation_application.start() +# +# # Example publish to pubsub +# # self.vip.pubsub.publish('pubsub', "some/random/topic", message="HI!") +# # +# # # Example RPC call +# # # self.vip.rpc.call("some_agent", "some_method", arg1, arg2) +# # pass +# # self._create_subscriptions(self.setting2) +# +# +# @Core.receiver("onstop") +# def onstop(self, sender, **kwargs): +# """ +# This method is called when the Agent is about to shutdown, but before it disconnects from +# the message bus. +# """ +# pass +# self.outstation_application.shutdown() +# +# # @RPC.export +# # def rpc_demo_load_config(self): +# # """ +# # RPC method +# # +# # May be called from another agent via self.core.rpc.call +# # """ +# # try: +# # config = utils.load_config("/home/kefei/project-local/volttron/TestAgent/config") +# # except Exception: +# # config = {} +# # return config +# +# # @RPC.export +# # def rpc_demo_config_list_set_get(self): +# # """ +# # RPC method +# # +# # May be called from another agent via self.core.rpc.call +# # """ +# # default_config = {"setting1": "setting1-xxxxxxxxx", +# # "setting2": "setting2-xxxxxxxxx"} +# # +# # # Set a default configuration to ensure that self.configure is called immediately to setup +# # # the agent. +# # # self.vip.config.set_default("config", default_config) # set_default can only be used before onstart +# # self.vip.config.set(config_name="config_2", contents=default_config, +# # trigger_callback=False, send_update=True) +# # get_result = [ +# # self.vip.config.get(config) for config in self.vip.config.list() +# # ] +# # return self.vip.config.list(), get_result +# +# # @RPC.export +# # def rpc_demo_config_set_default(self): +# # """ +# # RPC method +# # +# # May be called from another agent via self.core.rpc.call +# # """ +# # default_config = {"setting1": "setting1-xxxxxxxxx", +# # "setting2": "setting2-xxxxxxxxx"} +# # +# # # Set a default configuration to ensure that self.configure is called immediately to setup +# # # the agent. +# # self.vip.config.set_default("config", default_config) +# # return self.vip.config.list() +# # # # Hook self.configure up to changes to the configuration file "config". +# # # self.vip.config.subscribe(self.configure, actions=["NEW", "UPDATE"], pattern="config") +# +# # @RPC.export +# # def rpc_demo_pubsub(self): +# # """ +# # RPC method +# # +# # May be called from another agent via self.core.rpc.call +# # """ +# # +# # # pubsub_list = self.vip.pubsub.list('pubsub', 'some/') +# # # list(self, peer, prefix='', bus='', subscribed=True, reverse=False, all_platforms=False) +# # # # return pubsub_list +# # self.vip.pubsub.publish('pubsub', 'some/topic/', message="+++++++++++++++++++++++++ something something") +# # # self.vip.pubsub.subscribe('pubsub', 'some/topic/', callable=self._handle_publish) +# # # return pubsub_list +# # # # Hook self.configure up to changes to the configuration file "config". +# # # self.vip.config.subscribe(self.configure, actions=["NEW", "UPDATE"], pattern="config") + + +def main(): + """Main method called to start the agent.""" + utils.vip_main(Dnp3Agent, + version=__version__) + + +if __name__ == '__main__': + # Entry point for script + try: + sys.exit(main()) + except KeyboardInterrupt: + pass diff --git a/services/core/DNP3OutstationAgent/example-config.json b/services/core/DNP3OutstationAgent/example-config.json new file mode 100644 index 0000000000..567306a8e3 --- /dev/null +++ b/services/core/DNP3OutstationAgent/example-config.json @@ -0,0 +1,4 @@ +{'outstation_ip': '0.0.0.0', +'port': 20000, +'master_id': 2, +'outstation_id': 1} diff --git a/services/core/DNP3OutstationAgent/requirements.txt b/services/core/DNP3OutstationAgent/requirements.txt new file mode 100644 index 0000000000..2a3adaaaae --- /dev/null +++ b/services/core/DNP3OutstationAgent/requirements.txt @@ -0,0 +1 @@ +dnp3-python==0.2.3b3 diff --git a/services/core/DNP3OutstationAgent/setup.py b/services/core/DNP3OutstationAgent/setup.py new file mode 100644 index 0000000000..b96cd42d10 --- /dev/null +++ b/services/core/DNP3OutstationAgent/setup.py @@ -0,0 +1,29 @@ +from setuptools import setup, find_packages + +MAIN_MODULE = 'agent' + +# Find the agent package that contains the main module +packages = find_packages('.') +agent_package = 'dnp3_outstation_agent' + +# Find the version number from the main module +agent_module = agent_package + '.' + MAIN_MODULE +_temp = __import__(agent_module, globals(), locals(), ['__version__'], 0) +__version__ = _temp.__version__ + +# Setup +setup( + name=agent_package + 'agent', + version=__version__, + author="VOLTTRON team", + author_email="volttron@pnl.gov", + url="http:something", + description="Dnp3 agent as an outstation", + install_requires=['volttron'], + packages=packages, + entry_points={ + 'setuptools.installation': [ + 'eggsecutable = ' + agent_module + ':main', + ] + } +) diff --git a/services/core/DNP3OutstationAgent/tests/test_dnp3_agent.py b/services/core/DNP3OutstationAgent/tests/test_dnp3_agent.py new file mode 100644 index 0000000000..f7b4fa266b --- /dev/null +++ b/services/core/DNP3OutstationAgent/tests/test_dnp3_agent.py @@ -0,0 +1,244 @@ +""" +This test suits focus on the exposed RPC calls. +It utilizes a vip agent to evoke the RPC calls. +The volltron instance and dnp3-agent is start manually. +Note: several fixtures are used + volttron_platform_wrapper + vip_agent + dnp3_outstation_agent +""" +import pathlib + +import gevent +import pytest +import os +import datetime +# from dnp3_outstation.agent import Dnp3OutstationAgent +from services.core.DNP3OutstationAgent.dnp3_outstation_agent.agent import Dnp3Agent as Dnp3OutstationAgent +from dnp3_python.dnp3station.outstation_new import MyOutStationNew +import random +import subprocess +import logging + +logging_logger = logging.getLogger(__name__) + +dnp3_vip_identity = "dnp3_outstation" + + +# @pytest.fixture(scope="module") +# def volttron_home(): +# """ +# VOLTTRON_HOME environment variable suggested to setup at pytest.ini [env] +# """ +# volttron_home: str = os.getenv("VOLTTRON_HOME") +# assert volttron_home +# return volttron_home +# +# +# def test_volttron_home_fixture(volttron_home): +# assert volttron_home +# print(volttron_home) + + +def test_testing_file_path(): + parent_path = os.getcwd() + dnp3_agent_config_path = os.path.join(parent_path, "dnp3-outstation-config.json") + # print(dnp3_agent_config_path) + logging_logger.info(f"test_testing_file_path {dnp3_agent_config_path}") + + +def test_volttron_instance_fixture(volttron_instance): + print(volttron_instance) + logging_logger.info(f"=========== volttron_instance_new.volttron_home: {volttron_instance.volttron_home}") + logging_logger.info(f"=========== volttron_instance_new.skip_cleanup: {volttron_instance.skip_cleanup}") + logging_logger.info(f"=========== volttron_instance_new.vip_address: {volttron_instance.vip_address}") + + +@pytest.fixture(scope="module") +def vip_agent(volttron_instance): + # build a vip agent + a = volttron_instance.build_agent() + print(a) + return a + + +def test_vip_agent_fixture(vip_agent): + print(vip_agent) + logging_logger.info(f"=========== vip_agent: {vip_agent}") + logging_logger.info(f"=========== vip_agent.core.identity: {vip_agent.core.identity}") + logging_logger.info(f"=========== vip_agent.vip.peerlist().get(): {vip_agent.vip.peerlist().get()}") + + +@pytest.fixture(scope="module") +def dnp3_outstation_agent(volttron_instance) -> dict: + """ + Install and start a dnp3-outstation-agent, return its vip-identity + """ + # install a dnp3-outstation-agent + # TODO: improve the following hacky path resolver + parent_path = pathlib.Path(__file__) + dnp3_outstation_package_path = pathlib.Path(parent_path).parent.parent + dnp3_agent_config_path = str(os.path.join(parent_path, "dnp3-outstation-config.json")) + config = { + "outstation_ip": "0.0.0.0", + "master_id": 2, + "outstation_id": 1, + "port": 20000 + } + agent_vip_id = dnp3_vip_identity + uuid = volttron_instance.install_agent( + agent_dir=dnp3_outstation_package_path, + # agent_dir="volttron-dnp3-outastion", + config_file=config, + start=False, # Note: for some reason, need to set to False, then start + vip_identity=agent_vip_id) + # start agent with retry + # pid = retry_call(volttron_instance.start_agent, f_kwargs=dict(agent_uuid=uuid), max_retries=5, delay_s=2, + # wait_before_call_s=2) + + # # check if running with retry + # retry_call(volttron_instance.is_agent_running, f_kwargs=dict(agent_uuid=uuid), max_retries=5, delay_s=2, + # wait_before_call_s=2) + gevent.sleep(5) + pid = volttron_instance.start_agent(uuid) + gevent.sleep(5) + logging_logger.info( + f"=========== volttron_instance.is_agent_running(uuid): {volttron_instance.is_agent_running(uuid)}") + # TODO: get retry_call back + return {"uuid": uuid, "pid": pid} + + +def test_install_dnp3_outstation_agent_fixture(dnp3_outstation_agent, vip_agent, volttron_instance): + puid = dnp3_outstation_agent + print(puid) + logging_logger.info(f"=========== dnp3_outstation_agent ids: {dnp3_outstation_agent}") + logging_logger.info(f"=========== vip_agent.vip.peerlist().get(): {vip_agent.vip.peerlist().get()}") + logging_logger.info(f"=========== volttron_instance_new.is_agent_running(puid): " + f"{volttron_instance.is_agent_running(dnp3_outstation_agent['uuid'])}") + + +def test_dummy(vip_agent, dnp3_outstation_agent): + peer = dnp3_vip_identity + method = Dnp3OutstationAgent.rpc_dummy + peer_method = method.__name__ # "rpc_dummy" + rs = vip_agent.vip.rpc.call(peer, peer_method).get(timeout=5) + print(datetime.datetime.now(), "rs: ", rs) + + +def test_outstation_reset(vip_agent, dnp3_outstation_agent): + + peer = dnp3_vip_identity + method = Dnp3OutstationAgent.reset_outstation + peer_method = method.__name__ # "reset_outstation" + # note: reset_outstation returns None, check if raise or time out instead + try: + rs = vip_agent.vip.rpc.call(peer, peer_method).get(timeout=5) + print(datetime.datetime.now(), "rs: ", rs) + except BaseException as e: + assert False + + +def test_outstation_get_db(vip_agent, dnp3_outstation_agent): + peer = dnp3_vip_identity + method = Dnp3OutstationAgent.display_outstation_db + peer_method = method.__name__ # "display_outstation_db" + rs = vip_agent.vip.rpc.call(peer, peer_method).get(timeout=5) + print(datetime.datetime.now(), "rs: ", rs) + assert rs == { + 'Analog': {'0': None, '1': None, '2': None, '3': None, '4': None, '5': None, '6': None, '7': None, '8': None, + '9': None}, + 'AnalogOutputStatus': {'0': None, '1': None, '2': None, '3': None, '4': None, '5': None, '6': None, '7': None, + '8': None, '9': None}, + 'Binary': {'0': None, '1': None, '2': None, '3': None, '4': None, '5': None, '6': None, '7': None, '8': None, + '9': None}, + 'BinaryOutputStatus': {'0': None, '1': None, '2': None, '3': None, '4': None, '5': None, '6': None, '7': None, + '8': None, '9': None}} + + +def test_outstation_get_config(vip_agent, dnp3_outstation_agent): + peer = dnp3_vip_identity + method = Dnp3OutstationAgent.get_outstation_config + peer_method = method.__name__ # "get_outstation_config" + rs = vip_agent.vip.rpc.call(peer, peer_method).get(timeout=5) + print(datetime.datetime.now(), "rs: ", rs) + assert rs == {'outstation_ip_str': '0.0.0.0', 'port': 20000, 'masterstation_id_int': 2, 'outstation_id_int': 1} + + +def test_outstation_is_connected(vip_agent, dnp3_outstation_agent): + peer = dnp3_vip_identity + method = Dnp3OutstationAgent.is_outstation_connected + peer_method = method.__name__ # "is_outstation_connected" + rs = vip_agent.vip.rpc.call(peer, peer_method).get(timeout=5) + print(datetime.datetime.now(), "rs: ", rs) + assert rs in [True, False] + + +def test_outstation_apply_update_analog_input(vip_agent, dnp3_outstation_agent): + peer = dnp3_vip_identity + method = Dnp3OutstationAgent.apply_update_analog_input + peer_method = method.__name__ # "apply_update_analog_input" + val, index = random.random(), random.choice(range(5)) + print(f"val: {val}, index: {index}") + rs = vip_agent.vip.rpc.call(peer, peer_method, val, index).get(timeout=5) + print(datetime.datetime.now(), "rs: ", rs) + + # verify + val_new = rs.get("Analog").get(str(index)) + assert val_new == val + + +def test_outstation_apply_update_analog_output(vip_agent, dnp3_outstation_agent): + peer = dnp3_vip_identity + method = Dnp3OutstationAgent.apply_update_analog_output + peer_method = method.__name__ # "apply_update_analog_output" + val, index = random.random(), random.choice(range(5)) + print(f"val: {val}, index: {index}") + rs = vip_agent.vip.rpc.call(peer, peer_method, val, index).get(timeout=5) + print(datetime.datetime.now(), "rs: ", rs) + + # verify + val_new = rs.get("AnalogOutputStatus").get(str(index)) + assert val_new == val + + +def test_outstation_apply_update_binary_input(vip_agent, dnp3_outstation_agent): + peer = dnp3_vip_identity + method = Dnp3OutstationAgent.apply_update_binary_input + peer_method = method.__name__ # "apply_update_binary_input" + val, index = random.choice([True, False]), random.choice(range(5)) + print(f"val: {val}, index: {index}") + rs = vip_agent.vip.rpc.call(peer, peer_method, val, index).get(timeout=5) + print(datetime.datetime.now(), "rs: ", rs) + + # verify + val_new = rs.get("Binary").get(str(index)) + assert val_new == val + + +def test_outstation_apply_update_binary_output(vip_agent, dnp3_outstation_agent): + peer = dnp3_vip_identity + method = Dnp3OutstationAgent.apply_update_binary_output + peer_method = method.__name__ # "apply_update_binary_output" + val, index = random.choice([True, False]), random.choice(range(5)) + print(f"val: {val}, index: {index}") + rs = vip_agent.vip.rpc.call(peer, peer_method, val, index).get(timeout=5) + print(datetime.datetime.now(), "rs: ", rs) + + # verify + val_new = rs.get("BinaryOutputStatus").get(str(index)) + assert val_new == val + + +def test_outstation_update_config_with_restart(vip_agent, dnp3_outstation_agent): + peer = dnp3_vip_identity + method = Dnp3OutstationAgent.update_outstation + peer_method = method.__name__ # "update_outstation" + port_to_set = 20001 + rs = vip_agent.vip.rpc.call(peer, peer_method, port=port_to_set).get(timeout=5) + print(datetime.datetime.now(), "rs: ", rs) + + # verify + rs = vip_agent.vip.rpc.call(peer, "get_outstation_config").get(timeout=5) + port_new = rs.get("port") + # print(f"========= port_new {port_new}") + assert port_new == port_to_set diff --git a/services/core/DataMover/tests/test_datamover.py b/services/core/DataMover/tests/test_datamover.py index 581a1094bd..d25945a44b 100644 --- a/services/core/DataMover/tests/test_datamover.py +++ b/services/core/DataMover/tests/test_datamover.py @@ -204,10 +204,10 @@ def test_devices_topic(publish_agent, query_agent): count=20, order="LAST_TO_FIRST").get(timeout=10) - assert (len(result['values']) == 1) + assert len(result['values']) == 1 (time1_date, time1_time) = time1.split("T") assert (result['values'][0][0] == time1_date + 'T' + time1_time + '+00:00') - assert (result['values'][0][1] == approx(oat_reading)) + assert result['values'][0][1] == approx(oat_reading) assert set(result['metadata'].items()) == set(float_meta.items()) @@ -367,12 +367,12 @@ def test_analysis_topic(publish_agent, query_agent): start=now, order="LAST_TO_FIRST").get(timeout=10) print('Query Result', result) - assert (len(result['values']) == 1) + assert len(result['values']) == 1 (now_date, now_time) = now.split("T") if now_time[-1:] == 'Z': now_time = now_time[:-1] - assert (result['values'][0][0] == now_date + 'T' + now_time + '+00:00') - assert (result['values'][0][1] == approx(mixed_reading)) + assert result['values'][0][0] == now_date + 'T' + now_time + '+00:00' + assert result['values'][0][1] == approx(mixed_reading) @pytest.mark.historian @@ -430,8 +430,8 @@ def test_analysis_topic_no_header(publish_agent, query_agent): start=now, order="LAST_TO_FIRST").get(timeout=10) print('Query Result', result) - assert (len(result['values']) == 1) - assert (result['values'][0][1] == approx(mixed_reading)) + assert len(result['values']) == 1 + assert result['values'][0][1] == approx(mixed_reading) @pytest.mark.historian @@ -491,8 +491,8 @@ def test_log_topic(publish_agent, query_agent): topic="datalogger/PNNL/BUILDING1_ANON/Device/MixedAirTemperature", order="LAST_TO_FIRST").get(timeout=10) print('Query Result', result) - assert (len(result['values']) == 1) - assert (result['values'][0][1] == approx(mixed_reading)) + assert len(result['values']) == 1 + assert result['values'][0][1] == approx(mixed_reading) @pytest.mark.historian @@ -539,8 +539,8 @@ def test_log_topic_no_header(publish_agent, query_agent): start=current_time, order="LAST_TO_FIRST").get(timeout=10) print('Query Result', result) - assert (len(result['values']) == 1) - assert (result['values'][0][1] == approx(mixed_reading)) + assert len(result['values']) == 1 + assert result['values'][0][1] == approx(mixed_reading) @pytest.mark.historian @@ -567,23 +567,3 @@ def test_old_config(volttron_instances, forwarder): print("data_mover agent id: ", uuid) - -@pytest.mark.historian -@pytest.mark.forwarder -def test_default_config(volttron_instances): - """ - Test the default configuration file included with the agent - """ - publish_agent = volttron_instance1.build_agent(identity="test_agent") - gevent.sleep(1) - - config_path = os.path.join(get_services_core("DataMover"), "config") - with open(config_path, "r") as config_file: - config_json = json.load(config_file) - assert isinstance(config_json, dict) - volttron_instance1.install_agent( - agent_dir=get_services_core("DataMover"), - config_file=config_json, - start=True, - vip_identity="health_test") - assert publish_agent.vip.rpc.call("health_test", "health.get_status").get(timeout=10).get('status') == STATUS_GOOD diff --git a/services/core/ForwardHistorian/tests/test_forward_historian.py b/services/core/ForwardHistorian/tests/test_forward_historian.py index 668ff9b98f..2049c6a038 100644 --- a/services/core/ForwardHistorian/tests/test_forward_historian.py +++ b/services/core/ForwardHistorian/tests/test_forward_historian.py @@ -217,7 +217,7 @@ def test_devices_topic(publish_agent, query_agent): assert (len(result['values']) == 1) (time1_date, time1_time) = time1.split("T") assert (result['values'][0][0] == time1_date + 'T' + time1_time + '+00:00') - assert (result['values'][0][1] == approx(oat_reading)) + assert result['values'][0][1] == approx(oat_reading) assert set(result['metadata'].items()) == set(float_meta.items()) @@ -283,8 +283,8 @@ def test_analysis_topic(publish_agent, query_agent): (now_date, now_time) = now.split("T") if now_time[-1:] == 'Z': now_time = now_time[:-1] - assert (result['values'][0][0] == now_date + 'T' + now_time + '+00:00') - assert (result['values'][0][1] == approx(mixed_reading)) + assert result['values'][0][0] == now_date + 'T' + now_time + '+00:00' + assert result['values'][0][1] == approx(mixed_reading) @pytest.mark.historian @@ -342,8 +342,8 @@ def test_analysis_topic_no_header(publish_agent, query_agent): start=now, order="LAST_TO_FIRST").get(timeout=10) print('Query Result', result) - assert (len(result['values']) == 1) - assert (result['values'][0][1] == approx(mixed_reading)) + assert len(result['values']) == 1 + assert result['values'][0][1] == approx(mixed_reading) @pytest.mark.historian @@ -404,8 +404,8 @@ def test_log_topic(publish_agent, query_agent): topic="datalogger/PNNL/BUILDING1_ANON/Device/MixedAirTemperature", order="LAST_TO_FIRST").get(timeout=10) print('Query Result', result) - assert (len(result['values']) == 1) - assert (result['values'][0][1] == approx(mixed_reading)) + assert len(result['values']) == 1 + assert result['values'][0][1] == approx(mixed_reading) @pytest.mark.historian @@ -452,8 +452,8 @@ def test_log_topic_no_header(publish_agent, query_agent): start=current_time, order="LAST_TO_FIRST").get(timeout=10) print('Query Result', result) - assert (len(result['values']) == 1) - assert (result['values'][0][1] == approx(mixed_reading)) + assert len(result['values']) == 1 + assert result['values'][0][1] == approx(mixed_reading) @pytest.mark.historian @@ -549,7 +549,7 @@ def test_old_config(volttron_instances, forwarder): # gevent.sleep(1) # wait for topic to be forwarded and callback to happen # # # assert query_agent.callback.call_count == 1 -# print ('call args ', query_agent.callback.call_args_list) +# print('call args ', query_agent.callback.call_args_list) # # assert query_agent.callback.call_args[0][1] == 'platform.actuator' # assert query_agent.callback.call_args[0][3] == \ # topics.ACTUATOR_SCHEDULE_RESULT @@ -737,5 +737,5 @@ def test_default_config(volttron_instances, query_agent): assert (len(result['values']) == 1) (time1_date, time1_time) = time1.split("T") assert (result['values'][0][0] == time1_date + 'T' + time1_time + '+00:00') - assert (result['values'][0][1] == approx(oat_reading)) + assert result['values'][0][1] == approx(oat_reading) assert set(result['metadata'].items()) == set(float_meta.items()) diff --git a/services/core/ForwardHistorian/tests/test_forwarder_reconnections.py b/services/core/ForwardHistorian/tests/test_forwarder_reconnections.py index 04f05357a0..cdc4013bb9 100644 --- a/services/core/ForwardHistorian/tests/test_forwarder_reconnections.py +++ b/services/core/ForwardHistorian/tests/test_forwarder_reconnections.py @@ -112,8 +112,6 @@ def _device_capture(peer, sender, bus, topic, headers, message): pub_listener.core.stop() - - def test_target_shutdown(setup_instances): inst_forward, inst_target = setup_instances @@ -160,21 +158,22 @@ def _device_capture(peer, sender, bus, topic, headers, message): inst_target.restart_platform() assert inst_target.is_running() - + gevent.sleep(3) pub_listener = inst_target.build_agent() pub_listener.vip.pubsub.subscribe(peer="pubsub", prefix="devices", callback=_device_capture) - gevent.sleep(0.1) + gevent.sleep(3) all_topic = 'devices/campus/building/all' headers, message = publish_device_messages(inst_forward, all_topic=all_topic) + gevent.sleep(3) validate_published_device_data(headers, message, pubsub_retrieved[0][1], pubsub_retrieved[0][2]) def test_can_pause_publishing(setup_instances): - pass \ No newline at end of file + pass diff --git a/services/core/ForwardHistorian/tests/test_multi_messagebus_forwarder.py b/services/core/ForwardHistorian/tests/test_multi_messagebus_forwarder.py index 1f8eb55d96..b95e7476ac 100644 --- a/services/core/ForwardHistorian/tests/test_multi_messagebus_forwarder.py +++ b/services/core/ForwardHistorian/tests/test_multi_messagebus_forwarder.py @@ -136,6 +136,7 @@ def test_multi_messagebus_forwarder(multi_messagebus_forwarder): assert subscriber_agent.analysis_callback.call_count == 5 +@pytest.mark.timeout(600) @pytest.mark.forwarder def test_multi_messagebus_custom_topic_forwarder(multi_messagebus_forwarder): """ @@ -169,6 +170,7 @@ def test_multi_messagebus_custom_topic_forwarder(multi_messagebus_forwarder): assert subscriber_agent.callback.call_count == 5 +@pytest.mark.timeout(600) @pytest.mark.forwarder def test_multi_messagebus_forwarder_reconnection(multi_messagebus_forwarder): """ diff --git a/services/core/IEEE2030_5Agent/tests/test_IEEE2030_5_agent.py b/services/core/IEEE2030_5Agent/tests/test_IEEE2030_5_agent.py index f1d94b8ce3..5b931a0b65 100644 --- a/services/core/IEEE2030_5Agent/tests/test_IEEE2030_5_agent.py +++ b/services/core/IEEE2030_5Agent/tests/test_IEEE2030_5_agent.py @@ -106,8 +106,8 @@ def agent(request, volttron_instance_module_web): capabilities = {'edit_config_store': {'identity': PLATFORM_DRIVER}} volttron_instance_module_web.add_capabilities(test_agent.core.publickey, capabilities) # Configure a IEEE 2030.5 device in the Platform Driver - test_agent.vip.rpc.call('config.store', 'manage_delete_store', PLATFORM_DRIVER).get(timeout=10) - test_agent.vip.rpc.call('config.store', 'manage_store', PLATFORM_DRIVER, + test_agent.vip.rpc.call('config.store', 'delete_store', PLATFORM_DRIVER).get(timeout=10) + test_agent.vip.rpc.call('config.store', 'set_config', PLATFORM_DRIVER, 'devices/{}'.format(DRIVER_NAME), """{ "driver_config": { @@ -124,7 +124,7 @@ def agent(request, volttron_instance_module_web): "heart_beat_point": "Heartbeat" }""", 'json').get(timeout=10) - test_agent.vip.rpc.call('config.store', 'manage_store', PLATFORM_DRIVER, + test_agent.vip.rpc.call('config.store', 'set_config', PLATFORM_DRIVER, 'IEEE2030_5.csv', REGISTRY_CONFIG_STRING, 'csv').get(timeout=10) diff --git a/services/core/IEEE2030_5Agent/tests/test_IEEE2030_5_driver.py b/services/core/IEEE2030_5Agent/tests/test_IEEE2030_5_driver.py index 40e76206ac..ced543a8be 100644 --- a/services/core/IEEE2030_5Agent/tests/test_IEEE2030_5_driver.py +++ b/services/core/IEEE2030_5Agent/tests/test_IEEE2030_5_driver.py @@ -130,8 +130,8 @@ def agent(request, volttron_instance_module_web): test_agent = volttron_instance_module_web.build_agent() # Configure a IEEE 2030.5 device in the Platform Driver - test_agent.vip.rpc.call('config.store', 'manage_delete_store', 'platform.driver').get(timeout=10) - test_agent.vip.rpc.call('config.store', 'manage_store', 'platform.driver', + test_agent.vip.rpc.call('config.store', 'delete_store', 'platform.driver').get(timeout=10) + test_agent.vip.rpc.call('config.store', 'set_config', 'platform.driver', 'devices/{}'.format(DRIVER_NAME), """{ "driver_config": { @@ -148,7 +148,7 @@ def agent(request, volttron_instance_module_web): "heart_beat_point": "Heartbeat" }""", 'json').get(timeout=10) - test_agent.vip.rpc.call('config.store', 'manage_store', 'platform.driver', + test_agent.vip.rpc.call('config.store', 'set_config', 'platform.driver', 'IEEE2030_5.csv', REGISTRY_CONFIG_STRING, 'csv').get(timeout=10) diff --git a/services/core/MongodbTaggingService/README.md b/services/core/MongodbTaggingService/README.md index 449dbe7488..0136e3551c 100644 --- a/services/core/MongodbTaggingService/README.md +++ b/services/core/MongodbTaggingService/README.md @@ -21,11 +21,16 @@ keyword AND and OR, and use the keyword NOT to negate a conditions. ## Requirements -This historian requires a mongodb connector installed in your activated -volttron environment to talk to mongodb. Please execute the following -from an activated shell in order to install it. +This historian requires: - pip install pymongo +1. Mongodb server: Version tested - 7.0.1 + This agent requires mongodb server installed and running on the system. In addition create a database user and + database that can be used by this agent. +2. mongodb connector: Tested version - 4.5.0 + This agent requires a python mongo connected to be installed in your activated volttron environment to talk to + mongodb. Please execute the following from an activated shell in order to install it. + + pip install pymongo==4.5.0 ## Dependencies and Limitations diff --git a/services/core/MongodbTaggingService/mongotagging/tagging.py b/services/core/MongodbTaggingService/mongotagging/tagging.py index ed0c787098..34de203028 100644 --- a/services/core/MongodbTaggingService/mongotagging/tagging.py +++ b/services/core/MongodbTaggingService/mongotagging/tagging.py @@ -44,7 +44,7 @@ import pymongo import re -from pkg_resources import resource_string, resource_exists +from pymongo import InsertOne, UpdateOne from pymongo.errors import BulkWriteError from volttron.platform.agent import utils from volttron.platform.agent.base_tagging import BaseTaggingService @@ -133,9 +133,9 @@ def setup(self): collections = [] db = None try: - db = self._client.get_default_database() - collections = db.collection_names(include_system_collections=False) - _log.debug(collections) + db = self._client.get_database() + collections = db.list_collection_names() + _log.debug(f"GOT collections as {collections}") except Exception as e: err_message = "Unable to query list of existing tables from the " \ "database. Exception in init of tagging service: {}. " \ @@ -181,6 +181,8 @@ def setup(self): # _log.debug("status:{}".format(status)) self.vip.health.send_alert(TAGGING_SERVICE_SETUP_FAILED, status) self.core.stop() + else: + _log.info("Initialization complete") @doc_inherit def load_valid_tags(self): @@ -210,12 +212,11 @@ def _init_tags(self, db): # csv.DictReader uses first line in file for column headings # by default dr = csv.DictReader(csv_str.splitlines()) - bulk_tags = db[self.tags_collection].initialize_ordered_bulk_op() + inserts = [] for i in dr: - bulk_tags.insert({"_id":i['name'], - "kind":i['kind'], - "description":i['description']}) - bulk_tags.execute() + inserts.append(InsertOne( + {"_id": i['name'], "kind": i['kind'], "description": i['description']})) + db[self.tags_collection].bulk_write(inserts) else: raise ValueError( "Unable to load list of reference tags and its parent. No " @@ -231,18 +232,15 @@ def _init_tag_refs(self, db): # csv.DictReader uses first line in file for column headings # by default dr = csv.DictReader(csv_str.splitlines()) - bulk_tags = db[ - self.tag_refs_collection].initialize_ordered_bulk_op() + inserts = [] for i in dr: - bulk_tags.insert({"_id":i['tag'], - "parent":i['parent_tag']}) - bulk_tags.execute() + inserts.append(InsertOne({"_id": i['tag'], "parent": i['parent_tag']})) + db[self.tag_refs_collection].bulk_write(inserts) else: raise ValueError( "Unable to load list of reference tags and its parent. No " "such file: {}".format(file_path)) - def _init_categories(self, db): file_path = self.resource_sub_dir + '/categories.csv' _log.debug("Loading file :" + file_path) @@ -250,12 +248,10 @@ def _init_categories(self, db): csv_str = content_file.read() if csv_str: dr = csv.DictReader(csv_str.splitlines()) - bulk = db[ - self.categories_collection].initialize_ordered_bulk_op() + inserts = [] for i in dr: - bulk.insert({"_id": i['name'], - "description": i['description']}) - bulk.execute() + inserts.append(InsertOne({"_id": i['name'], "description": i['description']})) + db[self.categories_collection].bulk_write(inserts) else: _log.warning("No categories to initialize. No such file " + file_path) @@ -265,7 +261,7 @@ def _init_category_tags(self, db): with open(file_path, 'r') as content_file: txt_str = content_file.read() - bulk_tags = db[self.tags_collection].initialize_ordered_bulk_op() + updates = [] if txt_str: current_category = "" tags = set() @@ -283,15 +279,14 @@ def _init_category_tags(self, db): else: temp= line.split(":") # ignore description tags.update(re.split(" +", temp[0])) - if len(tags)>0: + if len(tags) > 0: for tag in tags: mapping[tag].add(current_category) for tag in mapping.keys(): - bulk_tags.find({"_id": tag}).update( - {'$set': {"categories": list(mapping[tag])}}) + updates.append(UpdateOne({"_id": tag}, {'$set': {"categories": list(mapping[tag])}})) - bulk_tags.execute() + db[self.tags_collection].bulk_write(updates) db[self.tags_collection].create_index( [('categories', pymongo.ASCENDING)], background=True) @@ -370,7 +365,7 @@ def query_tags_by_category(self, category, include_kind=False, @doc_inherit def insert_topic_tags(self, tags, update_version=False): db = self._client.get_default_database() - bulk = db[self.topic_tags_collection].initialize_unordered_bulk_op() + updates = [] result = dict() result['info'] = dict() result['error'] = dict() @@ -400,9 +395,9 @@ def insert_topic_tags(self, tags, update_version=False): temp['_id'] = prefix temp['id'] = prefix execute = True - bulk.find({'_id': prefix}).upsert().update_one( - {'$set': temp}) + updates.append(UpdateOne({'_id': prefix}, {'$set': temp}, upsert=True)) result['info'][topic_pattern].append(prefix) + if len(result['info'][topic_pattern]) == 1 and \ topic_pattern == result['info'][topic_pattern][0]: # means value sent was actually some pattern so add @@ -413,7 +408,7 @@ def insert_topic_tags(self, tags, update_version=False): result['info'].pop(topic_pattern) if execute: try: - bulk.execute() + db[self.topic_tags_collection].bulk_write(updates) except BulkWriteError as bwe: errors = bwe.details['writeErrors'] _log.error("bwe error count {}".format(len(errors))) diff --git a/services/core/MongodbTaggingService/requirements.txt b/services/core/MongodbTaggingService/requirements.txt index 891fc9d356..78cf745f08 100644 --- a/services/core/MongodbTaggingService/requirements.txt +++ b/services/core/MongodbTaggingService/requirements.txt @@ -1 +1 @@ -pymongo==3.7.2 +pymongo==4.5.0 diff --git a/services/core/MongodbTaggingService/scripts/insert_test_data.py b/services/core/MongodbTaggingService/scripts/insert_test_data.py index 2c468a9b23..ce3f7d9875 100644 --- a/services/core/MongodbTaggingService/scripts/insert_test_data.py +++ b/services/core/MongodbTaggingService/scripts/insert_test_data.py @@ -145,7 +145,7 @@ def mongo_insert(tags, execute_now=False): try: result = mongo_bulk.execute() if result['nInserted'] != mongo_batch_size: - print ("bulk execute result {}".format(result)) + print("bulk execute result {}".format(result)) errors = True except BulkWriteError as ex: print(str(ex.details)) @@ -205,7 +205,7 @@ def test_mongo_tags(): "equip_tag 7": {"$gt": 2}}, {"topic_prefix": 1}) topics = [record['_id'] for record in tags_cursor] print("example query result: {}".format(topics)) - print ("Time taken by mongo for result: {}".format( + print("Time taken by mongo for result: {}".format( datetime.datetime.now() - start)) @@ -230,8 +230,8 @@ def test_sqlite_tags(): ' INTERSECT ' 'select topic_prefix from test_tags where tag = "equip_tag 7" and ' 'value > 2') - print ("topics :{}".format(tags_cursor.fetchall())) - print ("Time taken by sqlite for result: {}".format( + print("topics :{}".format(tags_cursor.fetchall())) + print("Time taken by sqlite for result: {}".format( datetime.datetime.now() - start)) diff --git a/services/core/OpenADRVenAgent/IDENTITY b/services/core/OpenADRVenAgent/IDENTITY new file mode 100644 index 0000000000..186296b23b --- /dev/null +++ b/services/core/OpenADRVenAgent/IDENTITY @@ -0,0 +1 @@ +platform.openadr.ven \ No newline at end of file diff --git a/services/core/OpenADRVenAgent/config_example1.json b/services/core/OpenADRVenAgent/config_example1.json index 6e4f1b14ee..2d5bd36bf5 100644 --- a/services/core/OpenADRVenAgent/config_example1.json +++ b/services/core/OpenADRVenAgent/config_example1.json @@ -1,9 +1,8 @@ { "ven_name": "PNNLVEN", "vtn_url": "https://eiss2demo.ipkeys.com/oadr2/OpenADR2/Simple/2.0b", - "cert_path": "~/.ssh/secret/path_to_cert.pem", - "key_path": "~/.ssh/secret/path_to_privkey.pem", "debug": true, "disable_signature": true, - "openadr_client_type": "IPKeysClient" + "cert_path": "~/.ssh/secret/TEST_RSA_VEN_221206215541_cert.pem", + "key_path": "~/.ssh/secret/TEST_RSA_VEN_221206215541_privkey.pem" } diff --git a/services/core/OpenADRVenAgent/conftest.py b/services/core/OpenADRVenAgent/conftest.py index 8559470457..68e5e611b1 100644 --- a/services/core/OpenADRVenAgent/conftest.py +++ b/services/core/OpenADRVenAgent/conftest.py @@ -3,4 +3,4 @@ from volttrontesting.fixtures.volttron_platform_fixtures import * # Add system path of the agent's directory -sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) \ No newline at end of file +sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) diff --git a/services/core/OpenADRVenAgent/openadr_ven/agent.py b/services/core/OpenADRVenAgent/openadr_ven/agent.py index eff9aed6fa..c505bf8aac 100644 --- a/services/core/OpenADRVenAgent/openadr_ven/agent.py +++ b/services/core/OpenADRVenAgent/openadr_ven/agent.py @@ -42,14 +42,8 @@ from pathlib import Path from pprint import pformat -from datetime import timedelta, datetime, date, time, timezone -from typing import Callable, Dict, Any -from openleadr.enums import OPT, REPORT_NAME, MEASUREMENTS -from openleadr.client import OpenADRClient -from openleadr.objects import Event - -from volttron.platform import jsonapi +from typing import Callable, Dict from volttron.platform.agent.utils import ( get_aware_utc_now, setup_logging, @@ -59,6 +53,14 @@ ) from volttron.platform.messaging import topics, headers from volttron.platform.vip.agent import Agent, RPC +from .volttron_openadr_client import ( + VolttronOpenADRClient, + OpenADRClientInterface, + OpenADREvent, + OpenADRReportName, + OpenADRMeasurements, + OpenADROpt, +) from .constants import ( REQUIRED_KEYS, @@ -73,10 +75,9 @@ CA_FILE, VEN_ID, DISABLE_SIGNATURE, - OPENADR_CLIENT_TYPE, - IDENTITY, ) -from .volttron_openadr_client import openadr_clients + +from openleadr.objects import Event setup_logging() _log = logging.getLogger(__name__) @@ -96,12 +97,12 @@ class OpenADRVenAgent(Agent): def __init__(self, config_path: str, **kwargs) -> None: # adding 'fake_ven_client' to support dependency injection and preventing call to super class for unit testing - if kwargs.get('fake_ven_client'): - self.ven_client = kwargs['fake_ven_client'] + self.ven_client: OpenADRClientInterface + if kwargs.get("fake_ven_client"): + self.ven_client = kwargs["fake_ven_client"] else: - self.ven_client: OpenADRClient super(OpenADRVenAgent, self).__init__(enable_web=True, **kwargs) - + self.default_config = self._parse_config(config_path) # SubSystem/ConfigStore @@ -115,7 +116,7 @@ def __init__(self, config_path: str, **kwargs) -> None: def _configure_ven_client( self, config_name: str, action: str, contents: Dict ) -> None: - """Initializes the agent's configuration and creates an OpenADR Client using OpenLeadr. + """Initializes the agent's configuration, creates and starts VolttronOpenADRClient. :param config_name: :param action: the action @@ -125,80 +126,53 @@ def _configure_ven_client( config.update(contents) _log.info(f"config_name: {config_name}, action: {action}") - _log.info(f"Configuring agent with: \n {pformat(config)} ") - - # instantiate VEN client - client_type = config.get(OPENADR_CLIENT_TYPE) - openADRClient = openadr_clients().get(client_type) - if openADRClient is None: - msg = f"Invalid client type: {client_type}. Please use valid client types: {list(openadr_clients().keys())}" - _log.debug(msg) - raise KeyError(msg) - - _log.info(f"Creating OpenADRClient type: {client_type}...") - self.ven_client = openADRClient( - config.get(VEN_NAME), - config.get(VTN_URL), - debug=config.get(DEBUG), - cert=config.get(CERT), - key=config.get(KEY), - passphrase=config.get(PASSPHRASE), - vtn_fingerprint=config.get(VTN_FINGERPRINT), - show_fingerprint=config.get(SHOW_FINGERPRINT), - ca_file=config.get(CA_FILE), - ven_id=config.get(VEN_ID), - disable_signature=config.get(DISABLE_SIGNATURE), - ) - _log.info(f"{client_type} successfully created.") + _log.info(f"Configuring VEN client with: \n {pformat(config)} ") + + self.ven_client = VolttronOpenADRClient.build_client(config) - _log.info( - f"Adding capabilities (e.g. handlers, reports) to {client_type}..." - ) # Add event handling capability to the client # if you want to add more handlers on a specific event, you must create a coroutine in this class # and then add it as the second input for 'self.ven_client.add_handler(, )' self.ven_client.add_handler("on_event", self.handle_event) - _log.info("Capabilities successfully added.") + _log.info("Starting OpenADRVen agent...") gevent.spawn_later(3, self._start_asyncio_loop) def _start_asyncio_loop(self) -> None: - """ Start event loop - """ - _log.info("Starting agent...") loop = asyncio.get_event_loop() loop.create_task(self.ven_client.run()) loop.run_forever() # ***************** Methods for Servicing VTN Requests ******************** - async def handle_event(self, event: Event) -> OPT: + async def handle_event(self, event: Event) -> OpenADROpt: """Publish event to the Volttron message bus. This coroutine will be called when there is an event to be handled. :param event: The event sent from a VTN :return: Message to VTN to opt in to the event. """ + openadr_event = OpenADREvent(event) try: - _log.debug("Received event. Processing event now...") - signal = event.get("event_signals")[0] - _log.info(f"Event signal:\n {pformat(signal)}") + _log.info( + f"Received event. Processing event now...\n Event signal:\n {pformat(openadr_event.get_event_signals())}" + ) except IndexError as e: _log.debug( - f"Event signals is empty. {e} \n Showing whole event: {pformat(event)}" + f"Event signals is empty. {e} \n Showing whole event: {pformat(openadr_event)}" ) pass - self.publish_event(event) + self.publish_event(openadr_event) - return OPT.OPT_IN + return OpenADROpt.OPT_IN @RPC.export def add_report_capability( self, callback: Callable, - report_name: REPORT_NAME, + report_name: OpenADRReportName, resource_id: str, - measurement: MEASUREMENTS, + measurement: OpenADRMeasurements, ) -> tuple: """Add a new reporting capability to the client. @@ -223,14 +197,14 @@ def add_report_capability( return report_specifier_id, r_id # ***************** VOLTTRON Pub/Sub Requests ******************** - def publish_event(self, event: Event) -> None: + def publish_event(self, event: OpenADREvent) -> None: """Publish an event to the Volttron message bus. When an event is created/updated, it is published to the VOLTTRON bus with a topic that includes 'openadr/event_update'. :param event: The Event received from the VTN """ # OADR rule 6: If testEvent is present and != "false", handle the event as a test event. try: - if event["event_descriptor"]["test_event"]: + if event.isTestEvent(): _log.debug("Suppressing publication of test event") return except KeyError as e: @@ -240,42 +214,14 @@ def publish_event(self, event: Event) -> None: _log.debug(f"Publishing real/non-test event \n {pformat(event)}") self.vip.pubsub.publish( peer="pubsub", - topic=f"{topics.OPENADR_EVENT}/{self.ven_client.ven_name}", + topic=f"{topics.OPENADR_EVENT}/{self.ven_client.get_ven_name()}", headers={headers.TIMESTAMP: format_timestamp(get_aware_utc_now())}, - message=self._parse_event(event), + message=event.parse_event(), ) return # ***************** Helper methods ******************** - def _parse_event(self, obj: Event) -> Any: - """Parse event so that it properly displays on message bus. - - :param obj: The event received from a VTN - :return: A deserialized Event that is converted into a python object - """ - - # function that gets called for objects that can’t otherwise be serialized. - def _default_serialzer(x): - if isinstance(x, timedelta): - return int(x.total_seconds()) - elif isinstance(x, datetime): - return format_timestamp(x) - elif isinstance(x, date): - return x.isoformat() - elif isinstance(x, time): - return x.isoformat() - elif isinstance(x, timezone): - return int(x.utcoffset().total_seconds()) - else: - return None - - obj_string = jsonapi.dumps( - obj, - default=_default_serialzer - ) - return jsonapi.loads(obj_string) - def _parse_config(self, config_path: str) -> Dict: """Parses the OpenADR agent's configuration file. @@ -300,26 +246,24 @@ def _parse_config(self, config_path: str) -> Dict: self._check_required_key(required_key, key_actual) req_keys_actual[required_key] = key_actual - # optional configurations - debug = config.get(DEBUG) - - # keypair paths + # optional configurations cert = config.get(CERT) if cert: cert = str(Path(cert).expanduser().resolve(strict=True)) key = config.get(KEY) if key: key = str(Path(key).expanduser().resolve(strict=True)) - + ca_file = config.get(CA_FILE) + if ca_file: + ca_file = str(Path(ca_file).expanduser().resolve(strict=True)) + debug = config.get(DEBUG) ven_name = config.get(VEN_NAME) vtn_url = config.get(VTN_URL) passphrase = config.get(PASSPHRASE) vtn_fingerprint = config.get(VTN_FINGERPRINT) - show_fingerprint = bool(config.get(SHOW_FINGERPRINT)) - ca_file = config.get(CA_FILE) + show_fingerprint = bool(config.get(SHOW_FINGERPRINT, True)) ven_id = config.get(VEN_ID) disable_signature = bool(config.get(DISABLE_SIGNATURE)) - openadr_client_type = config.get(OPENADR_CLIENT_TYPE) return { VEN_NAME: ven_name, @@ -333,7 +277,6 @@ def _parse_config(self, config_path: str) -> Dict: CA_FILE: ca_file, VEN_ID: ven_id, DISABLE_SIGNATURE: disable_signature, - OPENADR_CLIENT_TYPE: openadr_client_type, } def _check_required_key(self, required_key: str, key_actual: str) -> None: @@ -349,16 +292,12 @@ def _check_required_key(self, required_key: str, key_actual: str) -> None: raise KeyError( f"{VTN_URL} is required. Ensure {VTN_URL} is given a URL to the VTN." ) - elif required_key == OPENADR_CLIENT_TYPE and not key_actual: - raise KeyError( - f"{OPENADR_CLIENT_TYPE} is required. Specify one of the following valid client types: {list(openadr_clients().keys())}" - ) return def main(): """Main method called to start the agent.""" - vip_main(OpenADRVenAgent, IDENTITY) + vip_main(OpenADRVenAgent) if __name__ == "__main__": diff --git a/services/core/OpenADRVenAgent/openadr_ven/constants.py b/services/core/OpenADRVenAgent/openadr_ven/constants.py index b33ec22871..7b67871766 100644 --- a/services/core/OpenADRVenAgent/openadr_ven/constants.py +++ b/services/core/OpenADRVenAgent/openadr_ven/constants.py @@ -48,7 +48,4 @@ CA_FILE = "ca_file" VEN_ID = "ven_id" DISABLE_SIGNATURE = "disable_signature" -OPENADR_CLIENT_TYPE = "openadr_client_type" -REQUIRED_KEYS = [VEN_NAME, VTN_URL, OPENADR_CLIENT_TYPE] - -IDENTITY = "openadr_ven" +REQUIRED_KEYS = [VEN_NAME, VTN_URL] diff --git a/services/core/OpenADRVenAgent/openadr_ven/volttron_openadr_client.py b/services/core/OpenADRVenAgent/openadr_ven/volttron_openadr_client.py index d6b7c6c9e5..f4849fcf73 100644 --- a/services/core/OpenADRVenAgent/openadr_ven/volttron_openadr_client.py +++ b/services/core/OpenADRVenAgent/openadr_ven/volttron_openadr_client.py @@ -36,239 +36,138 @@ # under Contract DE-AC05-76RL01830 # }}} -import logging -import asyncio - -from abc import ABC -from functools import partial -from lxml import etree from openleadr.client import OpenADRClient -from openleadr.preflight import preflight_message -from openleadr.messaging import TEMPLATES, SIGNER, _create_replay_protect -from openleadr import utils, enums +from openleadr.objects import Event +from volttron.platform import jsonapi +import abc -from volttron.platform.agent.utils import setup_logging +from .constants import ( + VEN_NAME, + VTN_URL, + DEBUG, + CERT, + KEY, + PASSPHRASE, + VTN_FINGERPRINT, + SHOW_FINGERPRINT, + CA_FILE, + VEN_ID, + DISABLE_SIGNATURE, +) +from openleadr.enums import OPT, REPORT_NAME, MEASUREMENTS +from datetime import timedelta, datetime, date, time, timezone +from typing import Callable +from volttron.platform.agent.utils import format_timestamp -setup_logging() -logger = logging.getLogger(__name__) +class OpenADRReportName(REPORT_NAME): + def __init__(self): + super.__init__() -class OpenADRClientBase(OpenADRClient, ABC): - """ - The Volttron OpenADR VEN agent uses the python library OpenLEADR https://github.com/openleadr/openleadr-python to create - an OpenADR VEN client. OpenADRClientBase is extended from OpenLEADR's OpenADRClient, giving us the flexibility - to connect to any implementation of an OpenADR VTN. For example, to connect to an IPKeys VTN that was implemented - on an old OpenADR protocol, the IPKeysClient subclass was created so that it can successfully connect to an IPKeys VTN. - If you have a specific VTN that you want to connect to and require further customization of the OpenADRVEN client, create your - own OpenADRClient by extending the base class OpenADRClientBase, updating your client with your business logic, and putting that subclass in this module. - """ +class OpenADRMeasurements(MEASUREMENTS): + def __init__(self): + super.__init__() - def __init__(self, ven_name, vtn_url, disable_signature=False, **kwargs): - """ - Initializes a new OpenADR Client (Virtual End Node) - :param str ven_name: The name for this VEN - :param str vtn_url: The URL of the VTN (Server) to connect to - :param bool: The boolean flag to disable signatures on messages - """ - super().__init__(ven_name, vtn_url, **kwargs) - self.disable_signature = disable_signature +class OpenADROpt(OPT): + def __init__(self): + super.__init__() -class IPKeysClient(OpenADRClientBase, ABC): - def __init__(self, ven_name, vtn_url, disable_signature, **kwargs): - """ - Initializes a new OpenADR Client (Virtual End Node) +class OpenADREvent: + def __init__(self, event: Event): + self.event = event - :param str ven_name: The name for this VEN - :param str vtn_url: The URL of the VTN (Server) to connect to - :param bool: The boolean flag to disable signatures on messages - """ - super().__init__(ven_name, vtn_url, disable_signature, **kwargs) + def get_event_signals(self): + return self.event.get("event_signals")[0] - self._create_message = partial( - self.create_message_ipkeys, - cert=self.cert_path, - key=self.key_path, - passphrase=self.passphrase, - disable_signature=self.disable_signature, - ) + def isTestEvent(self): + return self.event["event_descriptor"]["test_event"] - async def _on_event(self, message): - """ - :param message dict: dictionary containing event information - """ - logger.debug("The VEN received an event") - events = message["events"] - try: - results = [] + def parse_event(self) -> Event: + """Parse event so that it properly displays on message bus. - for event in message["events"]: - event_id = event["event_descriptor"]["event_id"] - event_status = event["event_descriptor"]["event_status"] - modification_number = event["event_descriptor"][ - "modification_number" - ] - received_event = utils.find_by( - self.received_events, "event_descriptor.event_id", event_id - ) + :param obj: The event received from a VTN + :return: A deserialized Event that is converted into a python object + """ - if received_event: - if ( - received_event["event_descriptor"][ - "modification_number" - ] - == modification_number - ): - # Re-submit the same opt type as we already had previously - result = self.responded_events[event_id] - else: - # Replace the event with the fresh copy - utils.pop_by( - self.received_events, - "event_descriptor.event_id", - event_id, - ) - self.received_events.append(event) - # Wait for the result of the on_update_event handler - result = await utils.await_if_required( - self.on_update_event(event) - ) - else: - # Wait for the result of the on_event - self.received_events.append(event) - result = self.on_event(event) - if asyncio.iscoroutine(result): - result = await result - results.append(result) - if ( - event_status - in ( - enums.EVENT_STATUS.COMPLETED, - enums.EVENT_STATUS.CANCELLED, - ) - and event_id in self.responded_events - ): - self.responded_events.pop(event_id) - else: - self.responded_events[event_id] = result - for i, result in enumerate(results): - if ( - result not in ("optIn", "optOut") - and events[i]["response_required"] == "always" - ): - logger.error( - "Your on_event or on_update_event handler must return 'optIn' or 'optOut'; " - f"you supplied {result}. Please fix your on_event handler." - ) - results[i] = "optOut" - except Exception as err: - logger.error( - "Your on_event handler encountered an error. Will Opt Out of the event. " - f"The error was {err.__class__.__name__}: {str(err)}" - ) - results = ["optOut"] * len(events) + # function that gets called for objects that can’t otherwise be serialized. + def _default_serialzer(x): + if isinstance(x, timedelta): + return int(x.total_seconds()) + elif isinstance(x, datetime): + return format_timestamp(x) + elif isinstance(x, date): + return x.isoformat() + elif isinstance(x, time): + return x.isoformat() + elif isinstance(x, timezone): + return int(x.utcoffset().total_seconds()) + else: + return None + + obj_string = jsonapi.dumps(self.event, default=_default_serialzer) + return jsonapi.loads(obj_string) + + +class OpenADRClientInterface(metaclass=abc.ABCMeta): + @abc.abstractmethod + async def run(self): + pass + + @abc.abstractmethod + def get_ven_name(self): + pass + + @abc.abstractmethod + def add_handler(self, event: OpenADREvent, function): + pass + + @abc.abstractmethod + def add_report( + self, + callback: Callable, + report_name: OpenADRReportName, + resource_id: str, + measurement: OpenADRMeasurements, + ): + pass - event_responses = [ - { - "response_code": 200, - "response_description": "OK", - "opt_type": results[i], - "request_id": message["request_id"], - "modification_number": events[i]["event_descriptor"][ - "modification_number" - ], - "event_id": events[i]["event_descriptor"]["event_id"], - } - for i, event in enumerate(events) - if event["response_required"] == "always" - and not utils.determine_event_status(event["active_period"]) - == "completed" - ] - if len(event_responses) > 0: - response = { - "response_code": 200, - "response_description": "OK", - "request_id": message["request_id"], - } - message = self._create_message( - "oadrCreatedEvent", - response=response, - event_responses=event_responses, - ven_id=self.ven_id, - ) - service = "EiEvent" - response_type, response_payload = await self._perform_request( - service, message - ) - logger.info(response_type, response_payload) - else: - logger.info( - "Not sending any event responses, because a response was not required/allowed by the VTN." - ) +class VolttronOpenADRClient(OpenADRClientInterface): + def __init__(self, openadr_client: OpenADRClient) -> None: + self._openadr_client = openadr_client @staticmethod - def create_message_ipkeys( - message_type, - cert=None, - key=None, - passphrase=None, - disable_signature=False, - **message_payload, - ): - """ - Create and optionally sign an OpenADR message. Returns an XML string. - - :param message_type string: The type of message you are sending - :param str cert: The path to a PEM-formatted Certificate file to use - for signing messages. - :param str key: The path to a PEM-formatted Private Key file to use - for signing messages. - :param str passphrase: The passphrase for the Private Key - :param bool: The boolean flag to disable signatures on messages - """ - message_payload = preflight_message(message_type, message_payload) - template = TEMPLATES.get_template(f"{message_type}.xml") - signed_object = utils.flatten_xml(template.render(**message_payload)) - envelope = TEMPLATES.get_template("oadrPayload.xml") - if cert and key and not disable_signature: - tree = etree.fromstring(signed_object) - signature_tree = SIGNER.sign( - tree, - key=key, - cert=cert, - passphrase=utils.ensure_bytes(passphrase), - reference_uri="#oadrSignedObject", - signature_properties=_create_replay_protect(), + def build_client(config): + # Creates a VEN client using openleadr library + return VolttronOpenADRClient( + OpenADRClient( + config.get(VEN_NAME), + config.get(VTN_URL), + debug=config.get(DEBUG), + cert=config.get(CERT), + key=config.get(KEY), + passphrase=config.get(PASSPHRASE), + vtn_fingerprint=config.get(VTN_FINGERPRINT), + show_fingerprint=config.get(SHOW_FINGERPRINT, True), + ca_file=config.get(CA_FILE), + ven_id=config.get(VEN_ID), + disable_signature=config.get(DISABLE_SIGNATURE), ) - signature = etree.tostring(signature_tree).decode("utf-8") - else: - signature = None - msg = envelope.render( - template=f"{message_type}", - signature=signature, - signed_object=signed_object, ) - return msg + ##### Abstract methods implemented##### + async def run(self): + await self._openadr_client.run() + + def get_ven_name(self): + self._openadr_client.ven_name -def openadr_clients(): - """ - Returns a dictionary in which the keys are the class names of OpenADRClientBase subclasses and the values are the subclass objects. - For example: + def add_handler(self, event, function): + self._openadr_client.add_handler(event, function) - { "IPKeysClient": IPKeysClient } - """ - clients = {} - children = [OpenADRClient] - while children: - parent = children.pop() - for child in parent.__subclasses__(): - child_name = child.__name__ - if child_name not in clients: - clients[child_name] = child - children.append(child) - return clients + def add_report(self, callback, report_name, resource_id, measurement): + self._openadr_client.add_report(callback, report_name, resource_id, measurement) diff --git a/services/core/OpenADRVenAgent/requirements.txt b/services/core/OpenADRVenAgent/requirements.txt index 1dce359f4a..070898a182 100644 --- a/services/core/OpenADRVenAgent/requirements.txt +++ b/services/core/OpenADRVenAgent/requirements.txt @@ -1,2 +1,2 @@ -openleadr==0.5.24 -lxml==4.6.4 +openleadr==0.5.30 +cryptography==37.0.4 diff --git a/services/core/OpenADRVenAgent/setup.py b/services/core/OpenADRVenAgent/setup.py index c9f1bccf8c..286478069a 100644 --- a/services/core/OpenADRVenAgent/setup.py +++ b/services/core/OpenADRVenAgent/setup.py @@ -3,41 +3,41 @@ from os import path from setuptools import setup, find_packages -MAIN_MODULE = 'agent' +MAIN_MODULE = "agent" # Find the agent package that contains the main module -packages = find_packages('.') -agent_package = '' +packages = find_packages(".") +agent_package = "" for package in find_packages(): # Because there could be other packages such as tests - if path.isfile(package + '/' + MAIN_MODULE + '.py'): + if path.isfile(package + "/" + MAIN_MODULE + ".py"): agent_package = package break if not agent_package: - raise RuntimeError(f"None of the packages under {path.abspath('.')} contain the file {MAIN_MODULE}.py") + raise RuntimeError( + f"None of the packages under {path.abspath('.')} contain the file {MAIN_MODULE}.py" + ) # Find the version number from the main module agent_module = f"{agent_package}.{MAIN_MODULE}" -_temp = __import__(agent_module, globals(), locals(), ['__version__'], 0) +_temp = __import__(agent_module, globals(), locals(), ["__version__"], 0) __version__ = _temp.__version__ setup( name=f"{agent_package}agent", version=__version__, - install_requires=['volttron'], + install_requires=["volttron"], packages=packages, - entry_points={ - "setuptools.installation": [ - f"eggsecutable = {agent_module}:main" - ] - }, - classifiers=["Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "Topic :: Home Automation", - "Topic :: Software Development :: Embedded Systems", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9"] + entry_points={"setuptools.installation": [f"eggsecutable = {agent_module}:main"]}, + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Topic :: Home Automation", + "Topic :: Software Development :: Embedded Systems", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + ], ) diff --git a/services/core/OpenADRVenAgent/tests/config_test.json b/services/core/OpenADRVenAgent/tests/config_test.json index 47e8086616..21419d7b6c 100644 --- a/services/core/OpenADRVenAgent/tests/config_test.json +++ b/services/core/OpenADRVenAgent/tests/config_test.json @@ -1,5 +1,4 @@ { "ven_name": "PNNLVEN", - "vtn_url": "https://eiss2demo.ipkeys.com/oadr2/OpenADR2/Simple/2.0b", - "openadr_client_type": "OpenADRClientBase" + "vtn_url": "https://eiss2demo.ipkeys.com/oadr2/OpenADR2/Simple/2.0b" } diff --git a/services/core/OpenADRVenAgent/tests/test_openadr_ven_agent.py b/services/core/OpenADRVenAgent/tests/test_openadr_ven_agent.py index 5b18a26daf..7a5f7dd588 100644 --- a/services/core/OpenADRVenAgent/tests/test_openadr_ven_agent.py +++ b/services/core/OpenADRVenAgent/tests/test_openadr_ven_agent.py @@ -1,9 +1,14 @@ import pytest +from openadr_ven.volttron_openadr_client import OpenADRClientInterface + try: import openleadr except ModuleNotFoundError as e: - pytest.skip(f"openleadr not found! \nPlease install openleadr to run \ - tests: pip install openleadr.\n Original error message: {e}", allow_module_level=True) + pytest.skip( + f"openleadr not found! \nPlease install openleadr to run \ + tests: pip install openleadr.\n Original error message: {e}", + allow_module_level=True, + ) from pathlib import Path from mock import MagicMock @@ -23,19 +28,43 @@ async def test_handle_event_should_return_optIn(mock_openadr_ven): vipmock.pubsub.publish = pubsub_publishmock mock_openadr_ven.vip = vipmock - expected = await mock_openadr_ven.handle_event({"event_signals": [42]}) + expected = await mock_openadr_ven.handle_event( + {"event_descriptor": {"test_event": True}, "event_signals": [42]} + ) - assert expected == 'optIn' + assert expected == "optIn" @pytest.fixture def mock_openadr_ven(): - config_path = str(Path('config_test.json').absolute()) - OpenADRVenAgent.__bases__ = (AgentMock.imitate(Agent, OpenADRVenAgent(config_path)),) + config_path = f"{Path(__file__).parent.absolute()}/config_test.json" + OpenADRVenAgent.__bases__ = ( + AgentMock.imitate(Agent, OpenADRVenAgent(config_path)), + ) yield OpenADRVenAgent(config_path, fake_ven_client=FakeOpenADRClient()) -class FakeOpenADRClient: +class FakeOpenADRClient(OpenADRClientInterface): def __init__(self): - self.ven_name = 'fake_ven_name' + self.ven_name = "fake_ven_name" + + async def run(self): + pass + + + def get_ven_name(self): + pass + + + def add_handler(self, event, function): + pass + + def add_report( + self, + callback, + report_name, + resource_id, + measurement, + ): + pass diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/tests/test_chargepoint_driver.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/tests/test_chargepoint_driver.py index 7851d675e4..ccd6197c9b 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/tests/test_chargepoint_driver.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/chargepoint/tests/test_chargepoint_driver.py @@ -157,7 +157,7 @@ def agent(request, volttron_instance): md_agent = volttron_instance.build_agent() # Clean out platform driver configurations. md_agent.vip.rpc.call('config.store', - 'manage_delete_store', + 'delete_store', 'platform.driver').get(timeout=10) driver1_config = DRIVER1_CONFIG_STRING % os.environ.get('CHARGEPOINT_PASSWORD', 'Must set a password') @@ -166,21 +166,21 @@ def agent(request, volttron_instance): # Add test configurations. md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', 'platform.driver', 'devices/chargepoint1', driver1_config, 'json').get(timeout=10) md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', 'platform.driver', 'devices/chargepoint2', driver2_config, 'json').get(timeout=10) md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', 'platform.driver', 'chargepoint.csv', REGISTRY_CONFIG_STRING, diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/__init__.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/__init__.py new file mode 100644 index 0000000000..91b2cd5813 --- /dev/null +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/__init__.py @@ -0,0 +1 @@ +from .dnp3 import * diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/dnp3-driver.md b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/dnp3-driver.md new file mode 100644 index 0000000000..3771c0c5b6 --- /dev/null +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/dnp3-driver.md @@ -0,0 +1,469 @@ +# DNP3 Driver + +Distributed Network Protocol (DNP +or [DNP3](https://en.wikipedia.org/wiki/DNP3)) +has achieved a large-scale acceptance since its introduction in 1993. This +protocol is an immediately deployable solution for monitoring remote sites because it was developed for communication of +critical infrastructure status, allowing for reliable remote control. + +DNP3 is typically used between centrally located masters and distributed remotes. The master provides the interface +between the human network manager and the monitoring system. The remote (RTUs and intelligent electronic devices) +provides the interface between the master and the physical device(s) being monitored and/or controlled. +The DNP3-Driver is a wrapper on the DNP3 master following +the [VOLTTRON driver framework](https://volttron.readthedocs.io/en/develop/agent-framework/driver-framework/drivers-overview.html#driver-framework). + +Note that the DNP3-Driver requires a DNP3 outstation instance to properly function. e.g., polling data, setting point +values, etc. The [dnp3-python](https://github.com/VOLTTRON/dnp3-python) can provide the essential outstation +functionality, and as part of the DNP3-Driver dependency, it is immediately available after the DNP3-Driver is +installed. + +### Requirements + +The DNP3 driver requires the [dnp3-python](https://github.com/VOLTTRON/dnp3-python) package, a wrapper on Pydnp3 +package. +This package can be installed in an activated environment with: + + pip install dnp3-python==0.2.3b3 + +### Quick Start + +The following recipe walks through the steps to install and configure a DNP3 Driver. Note that it uses default setup to +work out-of-the-box. Please feel free to refer to related documentations for details. + +1. Install volttron and start the platform. + + Refer + to [VOLTTRON Quick Start](https://volttron.readthedocs.io/en/main/tutorials/quick-start.html#volttron-quick-start) to + install the platform. + Then start the platform with the following command. (Please see `volttron --help` for more details.) + + ```shell + # Start platform with output going to volttron.log + volttron -vv -l volttron.log & + ``` + +1. Install the volttron platform driver: + + Install the required dependency for driver using `python bootstrap.py --drivers`. (Please + see `python bootstrap.py --help` for more details.) + + Note: for reproducibility, this demo will install platform driver with `vip-identity==platform_driver_for_dnp3`. + Free feel to specify any agent vip-identity as desired. + + ```shell + vctl install services/core/PlatformDriverAgent/ \ + --vip-identity platform_driver_for_dnp3 \ + --agent-config services/core/PlatformDriverAgent/platform-driver.agent \ + --tag platform_driver_for_dnp3 \ + -f \ + --start + ``` + +
+ Verify with `vctl status`. + + ```shell + (env) kefei@ubuntu-22:~/sandbox/dnp3-driver-sandbox$ vctl status + + UUID AGENT IDENTITY TAG PRIORITY STATUS HEALTH + + 5 platform_driveragent-4.0 platform_driver_for_dnp3 running [23217] GOOD + ``` + +### Configure the DNP3 driver + +1. Install a DNP3 Driver onto the Platform Driver. + + Installing a DNP3 driver in the Platform Driver Agent requires adding copies of the device configuration and registry + configuration files to the Platform Driver’s configuration store. For demo purpose, we will use default configure + files. + + Prepare the default config files: + + ```shell + # Create config file place holders + mkdir config + touch config/dnp3-config.json + touch config/dnp3.csv + ``` + + An example config file is available at " + services/core/PlatformDriverAgent/platform_driver/interfaces/udd_dnp3/examples/dnp3.config" + + ```json + { + "driver_config": { + "master_ip": "0.0.0.0", + "outstation_ip": "127.0.0.1", + "master_id": 2, + "outstation_id": 1, + "port": 20000 + }, + "registry_config": "config://dnp3.csv", + "driver_type": "dnp3", + "interval": 5, + "timezone": "UTC", + "publish_depth_first_all": true, + "heart_beat_point": "random_bool" + } + ``` + + Another example config file is available at " + services/core/PlatformDriverAgent/platform_driver/interfaces/udd_dnp3/examples/dnp3.csv" + + ```csv + Point Name,Volttron Point Name,Group,Variation,Index,Scaling,Units,Writable,Notes + AnalogInput_index0,AnalogInput_index0,30,6,0,1,NA,FALSE,Double Analogue input without status + AnalogInput_index1,AnalogInput_index1,30,6,1,1,NA,FALSE,Double Analogue input without status + AnalogInput_index2,AnalogInput_index2,30,6,2,1,NA,FALSE,Double Analogue input without status + AnalogInput_index3,AnalogInput_index3,30,6,3,1,NA,FALSE,Double Analogue input without status + BinaryInput_index0,BinaryInput_index0,1,2,0,1,NA,FALSE,Single bit binary input with status + BinaryInput_index1,BinaryInput_index1,1,2,1,1,NA,FALSE,Single bit binary input with status + BinaryInput_index2,BinaryInput_index2,1,2,2,1,NA,FALSE,Single bit binary input with status + BinaryInput_index3,BinaryInput_index3,1,2,3,1,NA,FALSE,Single bit binary input with status + AnalogOutput_index0,AnalogOutput_index0,40,4,0,1,NA,TRUE,Double-precision floating point with flags + AnalogOutput_index1,AnalogOutput_index1,40,4,1,1,NA,TRUE,Double-precision floating point with flags + AnalogOutput_index2,AnalogOutput_index2,40,4,2,1,NA,TRUE,Double-precision floating point with flags + AnalogOutput_index3,AnalogOutput_index3,40,4,3,1,NA,TRUE,Double-precision floating point with flags + BinaryOutput_index0,BinaryOutput_index0,10,2,0,1,NA,TRUE,Binary Output with flags + BinaryOutput_index1,BinaryOutput_index1,10,2,1,1,NA,TRUE,Binary Output with flags + BinaryOutput_index2,BinaryOutput_index2,10,2,2,1,NA,TRUE,Binary Output with flags + BinaryOutput_index3,BinaryOutput_index3,10,2,3,1,NA,TRUE,Binary Output with flags + + ``` + + Add config to the configuration store: + + ``` + vctl config store platform_driver_for_dnp3 devices/campus/building/dnp3 services/core/PlatformDriverAgent/platform_driver/interfaces/udd_dnp3/examples/dnp3.config + vctl config store platform_driver_for_dnp3 dnp3.csv services/core/PlatformDriverAgent/platform_driver/interfaces/udd_dnp3/examples/dnp3.csv --csv + ``` + +
+ Verify with `vctl config list` and `vctl config get` command. + (Please refer to the `vctl config` documentation for more details.) + + ```shell + (env) kefei@ubuntu-22:~/sandbox/dnp3-driver-sandbox$ vctl config get platform_driver_for_dnp3 devices/campus/building/dnp3 + { + "driver_config": { + "master_ip": "0.0.0.0", + "outstation_ip": "127.0.0.1", + "master_id": 2, + "outstation_id": 1, + "port": 20000 + }, + "registry_config": "config://dnp3.csv", + "driver_type": "dnp3", + "interval": 5, + "timezone": "UTC", + "publish_depth_first_all": true, + "heart_beat_point": "random_bool" + } + + (env) kefei@ubuntu-22:~/sandbox/dnp3-driver-sandbox$ vctl config get platform_driver_for_dnp3 dnp3.csv + [ + { + "Point Name": "AnalogInput_index0", + "Volttron Point Name": "AnalogInput_index0", + "Group": "30", + "Variation": "6", + "Index": "0", + ... + ] + ``` + +
+ +1. Verify with logging data + + When the DNP3-Driver is properly installed and configured, we can verify with logging data in "volttron.log". + + ``` + tail -f /volttron.log + ``` + +
+ Expected logging example + + ```shell + ... + 2023-03-13 23:26:56,611 (volttron-platform-driver-0.2.0rc1 23666) volttron.driver.base.driver(334) DEBUG: finish publishing: devices/campus/building/dnp3/all + 2023-03-13 23:26:57,897 () volttron.services.auth.auth_service(235) DEBUG: after getting peerlist to send auth updates + 2023-03-13 23:26:57,897 () volttron.services.auth.auth_service(239) DEBUG: Sending auth update to peers platform.control + 2023-03-13 23:26:57,897 () volttron.services.auth.auth_service(239) DEBUG: Sending auth update to peers platform_driver_for_dnp3 + 2023-03-13 23:26:57,898 () volttron.services.auth.auth_service(239) DEBUG: Sending auth update to peers platform.health + 2023-03-13 23:26:57,898 () volttron.services.auth.auth_service(239) DEBUG: Sending auth update to peers platform.config_store + 2023-03-13 23:26:57,898 () volttron.services.auth.auth_service(193) INFO: auth file /home/kefei/.volttron/auth.json loaded + 2023-03-13 23:26:57,898 () volttron.services.auth.auth_service(172) INFO: loading auth file /home/kefei/.volttron/auth.json + 2023-03-13 23:26:57,898 () volttron.services.auth.auth_service(185) DEBUG: Sending auth updates to peers + 2023-03-13 23:26:58,241 (volttron-platform-driver-0.2.0rc1 23666) (0) INFO: ['ms(1678768018241) INFO tcpclient - Connecting to: 127.0.0.1'] + 2023-03-13 23:26:58,241 (volttron-platform-driver-0.2.0rc1 23666) (0) INFO: ['ms(1678768018241) WARN tcpclient - Error Connecting: Connection refused'] + 2023-03-13 23:26:59,905 () volttron.services.auth.auth_service(235) DEBUG: after getting peerlist to send auth updates + 2023-03-13 23:26:59,905 () volttron.services.auth.auth_service(239) DEBUG: Sending auth update to peers platform.control + 2023-03-13 23:26:59,905 () volttron.services.auth.auth_service(239) DEBUG: Sending auth update to peers platform_driver_for_dnp3... + ] + ``` +
+ +1. (Optional) Verify with published data polled from outstation + + To see data being polled from an outstation and published to the bus, we need to + + * Set up an outstation, and + * install a [Listener Agent](https://pypi.org/project/volttron-listener/): + + **Set up an outstation**: The [dnp3-python](https://github.com/VOLTTRON/dnp3-python) is part of the dnp3-driver + dependency, and it is immediately available after the DNP3-Driver is installed. + + **Open another terminal**, and run `dnp3demo outstation`. For demo purpose, we assign arbitrary values to the + point. ( + More details about the "dnp3demo" module, please + see [dnp3demo-Module.md](https://github.com/VOLTTRON/dnp3-python/blob/main/docs/dnp3demo-Module.md)) + + ```shell + ==== Outstation Operation MENU ================================== + - update analog-input point value (for local reading) + - update analog-output point value (for local control) + - update binary-input point value (for local reading) + - update binary-output point value (for local control) +
- display database + - display configuration + ================================================================= + + ======== Your Input Here: ==(outstation)====== + ai + You chose - update analog-input point value (for local reading) + Type in and . Separate with space, then hit ENTER. + Type 'q', 'quit', 'exit' to main menu. + + + ======== Your Input Here: ==(outstation)====== + 0.1212 0 + {'Analog': {0: 0.1212, 1: None, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}} + You chose - update analog-input point value (for local reading) + Type in and . Separate with space, then hit ENTER. + Type 'q', 'quit', 'exit' to main menu. + + + ======== Your Input Here: ==(outstation)====== + 1.2323 1 + {'Analog': {0: 0.1212, 1: 1.2323, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}} + You chose - update analog-input point value (for local reading) + Type in and . Separate with space, then hit ENTER. + Type 'q', 'quit', 'exit' to main menu. + ``` +
+ Example of interaction with the `dnp3demo outstation` sub-command + + ```shell + (env) kefei@ubuntu-22:~/sandbox/dnp3-driver-sandbox$ dnp3demo outstation + dnp3demo.run_outstation {'command': 'outstation', 'outstation_ip=': '0.0.0.0', 'port=': 20000, 'master_id=': 2, 'outstation_id=': 1} + ms(1678770551216) INFO manager - Starting thread (0) + 2023-03-14 00:09:11,216 control_workflow_demo INFO Connection Config + 2023-03-14 00:09:11,216 control_workflow_demo INFO Connection Config + 2023-03-14 00:09:11,216 control_workflow_demo INFO Connection Config + ms(1678770551216) INFO server - Listening on: 0.0.0.0:20000 + 2023-03-14 00:09:11,216 control_workflow_demo DEBUG Initialization complete. Outstation in command loop. + 2023-03-14 00:09:11,216 control_workflow_demo DEBUG Initialization complete. Outstation in command loop. + 2023-03-14 00:09:11,216 control_workflow_demo DEBUG Initialization complete. Outstation in command loop. + Connection error. + Connection Config {'outstation_ip_str': '0.0.0.0', 'port': 20000, 'masterstation_id_int': 2, 'outstation_id_int': 1} + Start retry... + Connection error. + Connection Config {'outstation_ip_str': '0.0.0.0', 'port': 20000, 'masterstation_id_int': 2, 'outstation_id_int': 1} + ms(1678770565247) INFO server - Accepted connection from: 127.0.0.1 + ==== Outstation Operation MENU ================================== + - update analog-input point value (for local reading) + - update analog-output point value (for local control) + - update binary-input point value (for local reading) + - update binary-output point value (for local control) +
- display database + - display configuration + ================================================================= + + + ======== Your Input Here: ==(outstation)====== + ai + You chose - update analog-input point value (for local reading) + Type in and . Separate with space, then hit ENTER. + Type 'q', 'quit', 'exit' to main menu. + + + ======== Your Input Here: ==(outstation)====== + 0.1212 0 + {'Analog': {0: 0.1212, 1: None, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}} + You chose - update analog-input point value (for local reading) + Type in and . Separate with space, then hit ENTER. + Type 'q', 'quit', 'exit' to main menu. + + + ======== Your Input Here: ==(outstation)====== + 1.2323 1 + {'Analog': {0: 0.1212, 1: 1.2323, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}} + You chose - update analog-input point value (for local reading) + Type in and . Separate with space, then hit ENTER. + Type 'q', 'quit', 'exit' to main menu. + + + ======== Your Input Here: ==(outstation)====== + ``` +
+ + **Install the [Listener Agent](https://pypi.org/project/volttron-listener/)**: + Run `vctl install volttron-listener --start`. Once installed, you should see the data being published by viewing the + Volttron logs file. (i.e., `tail -f /volttron.log`) + > **Note**: + > it is recommended to restart the Platform Driver after a specific driver is installed and configured. i.e., + > using the `vctl restart ` command.) The expected logging will be similar as follows: + + ```shell + 2023-03-14 00:11:55,000 (volttron-platform-driver-0.2.0rc0 24737) volttron.driver.base.driver(277) DEBUG: scraping device: campus/building/dnp3 + 2023-03-14 00:11:55,805 (volttron-platform-driver-0.2.0rc0 24737) volttron.driver.base.driver(330) DEBUG: publishing: devices/campus/building/dnp3/all + 2023-03-14 00:11:55,810 (volttron-listener-0.2.0rc0 24424) listener.agent(104) INFO: Peer: pubsub, Sender: platform_driver_for_dnp3:, Bus: , Topic: devices/campus/building/dnp3/all, Headers: {'Date': '2023-03-14T05:11:55.805245+00:00', 'TimeStamp': '2023-03-14T05:11:55.805245+00:00', 'SynchronizedTimeStamp': '2023-03-14T05:11:55.000000+00:00', 'min_compatible_version': '3.0', 'max_compatible_version': ''}, Message: + [{'AnalogInput_index0': 0.1212, + 'AnalogInput_index1': 1.2323, + 'AnalogInput_index2': 0.0, + 'AnalogInput_index3': 0.0, + 'AnalogOutput_index0': 0.0, + 'AnalogOutput_index1': 0.0, + 'AnalogOutput_index2': 0.0, + 'AnalogOutput_index3': 0.0, + 'BinaryInput_index0': False, + 'BinaryInput_index1': False, + 'BinaryInput_index2': False, + 'BinaryInput_index3': False, + 'BinaryOutput_index0': False, + 'BinaryOutput_index1': False, + 'BinaryOutput_index2': False, + 'BinaryOutput_index3': False}, + {'AnalogInput_index0': {'type': 'integer', 'tz': 'UTC', 'units': 'NA'}, + 'AnalogInput_index1': {'type': 'integer', 'tz': 'UTC', 'units': 'NA'}, + 'AnalogInput_index2': {'type': 'integer', 'tz': 'UTC', 'units': 'NA'}, + 'AnalogInput_index3': {'type': 'integer', 'tz': 'UTC', 'units': 'NA'}, + 'AnalogOutput_index0': {'type': 'integer', 'tz': 'UTC', 'units': 'NA'}, + 'AnalogOutput_index1': {'type': 'integer', 'tz': 'UTC', 'units': 'NA'}, + 'AnalogOutput_index2': {'type': 'integer', 'tz': 'UTC', 'units': 'NA'}, + 'AnalogOutput_index3': {'type': 'integer', 'tz': 'UTC', 'units': 'NA'}, + 'BinaryInput_index0': {'type': 'integer', 'tz': 'UTC', 'units': 'NA'}, + 'BinaryInput_index1': {'type': 'integer', 'tz': 'UTC', 'units': 'NA'}, + 'BinaryInput_index2': {'type': 'integer', 'tz': 'UTC', 'units': 'NA'}, + 'BinaryInput_index3': {'type': 'integer', 'tz': 'UTC', 'units': 'NA'}, + 'BinaryOutput_index0': {'type': 'integer', 'tz': 'UTC', 'units': 'NA'}, + 'BinaryOutput_index1': {'type': 'integer', 'tz': 'UTC', 'units': 'NA'}, + 'BinaryOutput_index2': {'type': 'integer', 'tz': 'UTC', 'units': 'NA'}, + 'BinaryOutput_index3': {'type': 'integer', 'tz': 'UTC', 'units': 'NA'}}] + 2023-03-14 00:11:55,810 (volttron-platform-driver-0.2.0rc0 24737) volttron.driver.base.driver(334) DEBUG: finish publishing: devices/campus/building/dnp3/all + 2023-03-14 00:11:56,825 (volttron-listener-0.2.0rc0 24424) listener.agent(104) INFO: Peer: pubsub, Sender: volttron-listener-0.2.0rc0_2:, Bus: , Topic: heartbeat/volttron-listener-0.2.0rc0_2, Headers: {'TimeStamp': '2023-03-14T05:11:56.820827+00:00', 'min_compatible_version': '3.0', 'max_compatible_version': ''}, Message: + + ``` + +1. Shutdown the platform + + ```shell + ./stop-volttron + ``` + +### DNP3 Registry Configuration File + +The driver's registry configuration file, a [CSV](https://en.wikipedia.org/wiki/Comma-separated_values) file, +specifies which DNP3 points the driver will read and/or write. Each row configures a single DNP3 point. + +The following columns are required for each row: + +- **Volttron Point Name** - The name used by the VOLTTRON platform and agents to refer to the point. +- **Group** - The point's DNP3 group number. +- **Variation** - THe permit negotiated exchange of data formatted, i.e., data type. +- **Index** - The point's index number within its DNP3 data type (which is derived from its DNP3 group number). +- **Scaling** - A factor by which to multiply point values. +- **Units** - Point value units. +- **Writable** - TRUE or FALSE, indicating whether the point can be written by the driver (FALSE = read-only). + +Consult the **DNP3 data dictionary** for a point's Group and Index values. Point +definitions in the data dictionary are by agreement between the DNP3 Outstation and Master. +The VOLTTRON DNP3Agent loads the data dictionary of point definitions from the JSON file +at "point_definitions_path" in the DNP3Agent's config file. + +### Testing + +1. (If not satisfied,) install the dependencies for testing. + + ```shell + python bootstrap.py --testing + ``` + +1. Run pytest + + ```shell + pytest ./services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/. + ``` + + Note: the tests run on port=20000 by default. Make sure there is no other dnp3 instances running on port 20000 when + running the tests. + +
+ Example output + + ```shell + ===================================================================================================== test session starts ===================================================================================================== + platform linux -- Python 3.10.6, pytest-7.1.2, pluggy-1.0.0 -- /home/kefei/project/volttron/env/bin/python + cachedir: .pytest_cache + rootdir: /home/kefei/project/volttron, configfile: pytest.ini + plugins: rerunfailures-10.2, asyncio-0.19.0, timeout-2.1.0 + asyncio: mode=auto + timeout: 300.0s + timeout method: signal + timeout func_only: False + collected 27 items + + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py::TestDummy::test_dummy PASSED [ 3%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py::TestStation::test_station_init PASSED [ 7%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py::TestStation::test_station_get_val_analog_input_float PASSED [ 11%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py::TestStation::test_station_set_val_analog_input_float PASSED [ 14%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py::TestDNPRegister::test_init PASSED [ 18%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py::TestDNPRegister::test_get_register_value_analog_float PASSED [ 22%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py::TestDNPRegister::test_get_register_value_analog_int PASSED [ 25%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py::TestDNPRegister::test_get_register_value_binary PASSED [ 29%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py::TestDNP3RegisterControlWorkflow::test_set_register_value_analog_float PASSED [ 33%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py::TestDNP3RegisterControlWorkflow::test_set_register_value_analog_int PASSED [ 37%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py::TestDNP3RegisterControlWorkflow::test_set_register_value_binary PASSED [ 40%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py::TestDNP3InterfaceNaive::test_init PASSED [ 44%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py::TestDNP3InterfaceNaive::test_get_reg_point PASSED [ 48%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py::TestDNP3InterfaceNaive::test_set_reg_point PASSED [ 51%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py::TestDummy::test_dummy PASSED [ 55%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py::TestDummyAgentFixture::test_agent_dummy[volttron_instance0] SKIPPED (only for debugging purpose) [ 59%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py::TestDnp3DriverRPC::test_interface_get_point[volttron_instance0] PASSED [ 62%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py::TestDnp3DriverRPC::test_interface_set_point[volttron_instance0] PASSED [ 66%]ms(1684117269227) INFO manager - Exiting thread (0) + + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py::TestDummyAgentFixture::test_agent_dummy[volttron_instance1] SKIPPED (RabbitMQ is not setup and/or...) [ 70%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py::TestDnp3DriverRPC::test_interface_get_point[volttron_instance1] SKIPPED (RabbitMQ is not setup an...) [ 74%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py::TestDnp3DriverRPC::test_interface_set_point[volttron_instance1] SKIPPED (RabbitMQ is not setup an...) [ 77%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py::TestDummyAgentFixture::test_agent_dummy[volttron_instance2] SKIPPED (only for debugging purpose) [ 81%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py::TestDnp3DriverRPC::test_interface_get_point[volttron_instance2] PASSED [ 85%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py::TestDnp3DriverRPC::test_interface_set_point[volttron_instance2] PASSED [ 88%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py::TestDnp3DriverRPC::test_scrape_all SKIPPED (TODO) [ 92%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py::TestDnp3DriverRPC::test_revert_all SKIPPED (TODO) [ 96%] + services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py::TestDnp3DriverRPC::test_revert_point SKIPPED (TODO) [100%] + + ====================================================================================================== warnings summary ======================================================================================================= + env/lib/python3.10/site-packages/wheel/paths.py:7 + /home/kefei/project/volttron/env/lib/python3.10/site-packages/wheel/paths.py:7: DeprecationWarning: The distutils package is deprecated and slated for removal in Python 3.12. Use setuptools or check PEP 632 for potential alternatives + import distutils.command.install as install + + ../../../../usr/lib/python3.10/distutils/command/install.py:13 + /usr/lib/python3.10/distutils/command/install.py:13: DeprecationWarning: The distutils.sysconfig module is deprecated, use sysconfig instead + from distutils.sysconfig import get_config_vars, is_virtual_environment + + -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html + =================================================================================================== short test summary info =================================================================================================== + SKIPPED [2] services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py: only for debugging purpose + SKIPPED [1] services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py: RabbitMQ is not setup and/or SSL does not work in CI + SKIPPED [1] services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py:65: RabbitMQ is not setup and/or SSL does not work in CI + SKIPPED [1] services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py:85: RabbitMQ is not setup and/or SSL does not work in CI + SKIPPED [1] services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py:104: TODO + SKIPPED [1] services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py:117: TODO + SKIPPED [1] services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py:129: TODO + ==================================================================================== 19 passed, 8 skipped, 2 warnings in 227.38s (0:03:47) ==================================================================================== + ``` + +
diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/dnp3.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/dnp3.py new file mode 100644 index 0000000000..c64fd775ee --- /dev/null +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/dnp3.py @@ -0,0 +1,211 @@ +from .driver_wrapper import WrapperInterface, WrapperRegister +from .driver_wrapper import ImplementedRegister, RegisterValue +from typing import List, Optional, Dict + +from dnp3_python.dnp3station.master_new import MyMasterNew + +# TODO-developer: Your code here +# Add dependency as needed, and update in requirements + +import logging +import sys + + +stdout_stream = logging.StreamHandler(sys.stdout) +stdout_stream.setFormatter(logging.Formatter('%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s')) + +_log = logging.getLogger(__name__) +_log.addHandler(stdout_stream) +_log.setLevel(logging.DEBUG) +_log.setLevel(logging.WARNING) +_log.setLevel(logging.ERROR) + + +# TODO-developer: Your code here +# Change the classname "UserDevelopRegister" as needed +class UserDevelopRegisterDnp3(WrapperRegister): + # TODO-developer: Your code here + def __init__(self, master_application, reg_def, *args, **kwargs): + super().__init__(*args, **kwargs) + # self.master_application = kwargs['master_application'] + # self.reg_def = kwargs['reg_def'] + self.master_application = master_application + self.reg_def = reg_def + + def get_register_value(self) -> RegisterValue: + # TODO-developer: Your code here + # Implement get-register-value logic here + # Note: Keep the method name as it is including the signatures. + # Use a helper method if needed. + + # EXAMPLE: + # def get_register_value(self) -> RegisterValue: + # return _get_register_value_helper(url=self.driver_config.get("url")) + # def _get_register_value_helper(self, url: str): + # ... + + # print("silly implementation") + # the url will be in the config file + + try: + reg_def = self.reg_def + group = int(reg_def.get("Group")) + variation = int(reg_def.get("Variation")) + index = int(reg_def.get("Index")) + val = self._get_outstation_pt(self.master_application, group, variation, index) + # val = str(val) + + if val is not None: + return val + else: + _log.warning("dnp3 driver (master) couldn't collect data from the outstation.") + raise ValueError(f"Returned invalid dnp3 data point {val}") # do not publish invalid values + except Exception as e: + # print(f"!!!!!!!!!!!!!!!!!!!!{e}") + _log.error(e) + + raise Exception(e) + + @staticmethod + def _get_outstation_pt(master_application, group, variation, index) -> RegisterValue: + """ + Core logic to retrieve register value by polling a dnp3 outstation + Note: using def get_db_by_group_variation_index + Returns + ------- + + """ + return_point_value = master_application.get_val_by_group_variation_index(group=group, + variation=variation, + index=index) + return return_point_value + + def set_register_value(self, value, **kwargs) -> Optional[RegisterValue]: + """ + TODO: docstring + """ + try: + reg_def = self.reg_def + group = int(reg_def.get("Group")) + variation = int(reg_def.get("Variation")) + index = int(reg_def.get("Index")) + + val: Optional[RegisterValue] + self._set_outstation_pt(self.master_application, group, variation, index, set_value=value) + val = None + + return val + except Exception as e: + _log.error(e) + _log.warning("dnp3 driver (master) couldn't set value for the outstation.") + + @staticmethod + def _set_outstation_pt(master_application, group, variation, index, set_value) -> None: + """ + Core logic to send point operate command to outstation + Note: using def send_direct_point_command + Returns None + ------- + + """ + master_application.send_direct_point_command(group=group, variation=variation, index=index, + val_to_set=set_value) + + +class Interface(WrapperInterface): + # TODO-developer: Your code here + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.master_application = None + + # TODO-developer: Your code here + # Register type configuration + @staticmethod + def pass_register_types(csv_config: dict, driver_config_in_json_config: List[dict], + register_type_list: List[ImplementedRegister] = None): + """ + Note: based on the config.csv file. + By default, assuming register points are dnp3-register type + (optional) heartbeat register + """ + return [UserDevelopRegisterDnp3] * len(csv_config) + + @staticmethod + def _create_master_station(driver_config: dict): + """ + init a master station and later pass to registers + + Note: rely on XX.config json file convention, e.g., + "driver_config": + {"master_ip": "0.0.0.0", + "outstation_ip": "127.0.0.1", + "master_id": 2, + "outstation_id": 1, + "port": 20000}, + + Returns + ------- + + """ + + master_application = MyMasterNew( + masterstation_ip_str=driver_config.get("master_ip"), + outstation_ip_str=driver_config.get("outstation_ip"), + port=driver_config.get("port"), + masterstation_id_int=driver_config.get("master_id"), + outstation_id_int=driver_config.get("outstation_id"), + ) + # master_application.start() + return master_application + + def create_register(self, driver_config, + point_name, + data_type, + units, + read_only, + default_value, + description, + csv_config, + reg_def, + register_type, *args, **kwargs): + def get_master_station(): + # Note: this a closure, since parameter driver_config is required. + # (at current state) only create_register workflow should use it. + if self.master_application: + return self.master_application + else: + self.master_application = self._create_master_station(driver_config) + return self.master_application + + master = get_master_station() + master.start() + + register = UserDevelopRegisterDnp3( + driver_config=driver_config, + point_name=point_name, + data_type=data_type, # TODO: make it more clear in documentation + units=units, + read_only=read_only, + default_value=default_value, + description=description, + csv_config=csv_config, + reg_def=reg_def, + master_application=master + ) + return register + + @staticmethod + def get_reg_point(register: ImplementedRegister): + """ + Core logic for get_point + Note: Can be used for vip-agent-mock testing + """ + return register.get_register_value() + + @staticmethod + def set_reg_point(register: ImplementedRegister, value_to_set: RegisterValue): + """ + Core logic for set_point, i.e., _set_point without verification + Note: Can be used for vip-agent-mock testing + """ + set_pt_response = register.set_register_value(value=value_to_set) + return set_pt_response diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/driver_wrapper.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/driver_wrapper.py new file mode 100644 index 0000000000..26d490cfb3 --- /dev/null +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/driver_wrapper.py @@ -0,0 +1,836 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright 2020, Battelle Memorial Institute. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This material was prepared as an account of work sponsored by an agency of +# the United States Government. Neither the United States Government nor the +# United States Department of Energy, nor Battelle, nor any of their +# employees, nor any jurisdiction or organization that has cooperated in the +# development of these materials, makes any warranty, express or +# implied, or assumes any legal liability or responsibility for the accuracy, +# completeness, or usefulness or any information, apparatus, product, +# software, or process disclosed, or represents that its use would not infringe +# privately owned rights. Reference herein to any specific commercial product, +# process, or service by trade name, trademark, manufacturer, or otherwise +# does not necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors expressed +# herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY operated by +# BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 +# }}} +import abc + +from platform_driver.interfaces import BaseInterface, BaseRegister, BasicRevert +# from ...platform_driver.interfaces import BaseInterface, BaseRegister, BasicRevert +from csv import DictReader +from io import StringIO +import logging +import sys + +import requests + +from typing import List, Type, Dict, Union, Optional, TypeVar +from time import sleep + +stdout_stream = logging.StreamHandler(sys.stdout) +stdout_stream.setFormatter(logging.Formatter('%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s')) + +_log = logging.getLogger(__name__) +# _log = logging.getLogger("data_retrieval_demo") +_log.addHandler(stdout_stream) +_log.setLevel(logging.DEBUG) +_log.setLevel(logging.WARNING) + +# TODO: parse to python_type based on literal. i.e., locate("int")("1") -> int(1) +# Design the data type validation logic (recommend but not enforce?) +type_mapping = {"string": str, + "int": int, + "integer": int, + "float": float, + "bool": bool, + "boolean": bool} + +# Type alias +RegisterValue = Union[int, str, float, bool] +Register = TypeVar("Register", bound=BaseRegister) + + +class WrapperRegister(BaseRegister): + """ + Template Register, host boilerplate code + """ + + # TODO: do we need to separate read-only and writable register? How a writable register looks like? + # TODO: e.g., How the set-value pass to the register class? + # TODO: (Mimic what happen to get_register_value method, we might need a controller method. + def __init__(self, driver_config: dict, point_name: str, data_type: RegisterValue, units: str, read_only: bool, + default_value=None, description='', csv_config={}, *args, **kwargs): + """ + Parameters # TODO: clean this up, + ---------- + config_dict: associated with `driver_config` in driver-config.config (json-like file) + user inputs are put here, e.g., IP address, url, etc. + + read_only: associated with `Writable` in driver-config.csv + point_name: associated with `Volttron Point Name` in driver-config.csv + units: associated with `Units` in driver-config.csv + reg_type: ?? # TODO: clean this up, + default_value: ?? # TODO: clean this up, + description: ?? # TODO: clean this up, + + Associated with Point Name,Volttron Point Name,Units,Units Details,Writable,Starting Value,Type,Notes + read_only = regDef['Writable'].lower() != 'true' + point_name = regDef['Volttron Point Name'] + description = regDef.get('Notes', '') + units = regDef['Units'] + default_value = regDef.get("Starting Value", 'sin').strip() + """ + super().__init__("byte", read_only, point_name, units, description='') + self._value: str = "" + self.driver_config: dict = driver_config + + self.point_name: str = point_name + self.data_type_str: str = data_type # "byte" or "bit" + self.units: Optional[str] = units + self.read_only: bool = read_only + self.default_value: Optional[RegisterValue] = default_value + self.description: str = description + self.csv_config: list = csv_config + + @property + def value(self): + self._value = self.get_register_value() # pre-requite methods + return self._value + + @value.setter + def value(self, x: RegisterValue): + if self.read_only: + raise RuntimeError( # TODO: Is RuntimeError necessary + "Trying to write to a point configured read only: " + self.point_name) # TODO: clean up + self._value = x + + @abc.abstractmethod + def get_register_value(self, **kwargs) -> RegisterValue: + """ + Override this to get register value + Examples 1 retrieve: + def get_register_value(): + some_url: str = self.config_dict.get("url") + return self.get_restAPI_value(url=some_url) + def get_restAPI_value(url=some_url) + ... + Returns + ------- + + """ + + @abc.abstractmethod + def set_register_value(self, value, **kwargs) -> Optional[RegisterValue]: # TODO: need an example/redesign for this + pass + # """ + # Override this to set register value. (Only for writable==True/read_only==False) + # Examples: + # def set_register_value(): + # some_temperature: int = get_comfortable_temperature(...) + # self.value(some_temperature) + # def get_comfortable_temperature(**kwargs) -> int: + # ... + # Returns + # ------- + # + # """ + + +# alias +ImplementedRegister = Union[WrapperRegister, Type[WrapperRegister]] + + +class DriverConfig: + """ + For validate driver configuration, e.g., driver-config.csv + """ + + def __init__(self, csv_config: List[dict]): + self.csv_config: List[dict] = csv_config + """ + + Parameters + ---------- + csv_config + + Returns + ------- + Examples: + [{'Point Name': 'Heartbeat', 'Volttron Point Name': 'Heartbeat', 'Units': 'On/Off', + 'Units Details': 'On/Off', 'Writable': 'TRUE', 'Starting Value': '0', 'Type': 'boolean', + 'Notes': 'Point for heartbeat toggle'}, + {'Point Name': 'Catfact', 'Volttron Point Name': 'Catfact', 'Units': 'No cat fact', + 'Units Details': 'No cat fact', 'Writable': 'TRUE', 'Starting Value': 'No cat fact', 'Type': 'str', + 'Notes': 'Cat fact extract from REST API'}] + """ + + @staticmethod + def _validate_header(point_config: dict): + """ + Require the header include the following keys + "PointName", "DataType", "Units", "ReadOnly", "DefaultValue", "Description" + (or allow parsing with minimal effort) + "PointName" <- "Point Name", "point name", "point-name", but not "point names" or "the point name" + Parameters + ---------- + point_config + + Returns + ------- + + """ + + def _to_alpha_lower(key: str): + return ''.join([x.lower() for x in key if x.isalpha()]) + + new_dict = {_to_alpha_lower(k): v for k, v in point_config.items()} + new_keys = new_dict.keys() + + standardized_valid_names = ["Volttron Point Name", "Data Type", "Units", "Writable", "Default Value", "Notes"] + for valid_name in standardized_valid_names: + if valid_name.lower() not in new_keys: + raise ValueError(f"`{valid_name}` is not in the config") + return new_dict + + def key_validate(self) -> List[dict]: + """ + + Returns + EXAMPLE: + {'pointname': 'Heartbeat', + 'datatype': 'boolean', + 'units': 'On/Off', + 'readonly': 'TRUE', + 'defaultvalue': '0', + 'description': 'Point for heartbeat toggle', + 'volttronpointname': 'Heartbeat', + 'unitsdetails': 'On/Off'} + ------- + + """ + key_validate_csv = [self._validate_header(point_config) for point_config in self.csv_config] + return key_validate_csv + + +class WrapperInterface(BasicRevert, BaseInterface): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.point_map: Dict[str, ImplementedRegister] = {} # {register.point_name: register} + self.register_types: List[ + ImplementedRegister] = [] # TODO: add sanity check for restister_types, e.g., count == register counts + + self.csv_config = None # TODO: try to get this value, potentially from def configure. get inspiration from modbus_tk testing + self.driver_config_in_json_config = None # TODO: try to get this value, potentially from def configure + + # TODO: clean up this public interface + # from *.csv configure file "driver_config": {...} + # self.driver_config: dict = {} + + def configure(self, driver_config_in_json_config: dict, csv_config: List[ + dict]): # TODO: ask driver.py, BaseInterface.configure to update signature when evoking + """ + Used by driver.py + def get_interface(self, driver_type, config_dict, config_string): + interface.configure(config_dict, config_string) + + Parameters # TODO: follow BaseInterface.configure signatures. But the names are wrong. + ---------- + driver_config_in_json_config: associated with `driver_config` in driver-config.config (json-like file) + user inputs are put here, e.g., IP address, url, etc. + csv_config: associated with the whole driver-config.csv file + Examples: + [{'Point Name': 'Heartbeat', 'Volttron Point Name': 'Heartbeat', 'Units': 'On/Off', + 'Units Details': 'On/Off', 'Writable': 'TRUE', 'Starting Value': '0', 'Type': 'boolean', + 'Notes': 'Point for heartbeat toggle'}, + {'Point Name': 'Catfact', 'Volttron Point Name': 'Catfact', 'Units': 'No cat fact', + 'Units Details': 'No cat fact', 'Writable': 'TRUE', 'Starting Value': 'No cat fact', 'Type': 'str', + 'Notes': 'Cat fact extract from REST API'}] + + """ + # print("========================================== csv_config, ", csv_config) + # print("========================================== driver_config_in_json_config, ", driver_config_in_json_config) + self.csv_config = csv_config + self.driver_config_in_json_config = driver_config_in_json_config + + # TODO configuration validation, i.e., self.config_check(...) + # self.config_check + self.parse_config(csv_config, driver_config_in_json_config) + + @staticmethod + @abc.abstractmethod + def pass_register_types(csv_config: dict, driver_config_in_json_config: List[dict], + register_type_list: List[ImplementedRegister] = None) -> List[ImplementedRegister]: + """ + For ingesting the register types list + Will be used by concrete Interface class inherit this template + + Parameters + ---------- + driver_config_in_json_config: associated with `driver_config` in driver-config.config (json-like file) + user inputs are put here, e.g., IP address, url, etc. + csv_config: associated with the whole driver-config.csv file + Examples: + [{'Point Name': 'Heartbeat', 'Volttron Point Name': 'Heartbeat', 'Units': 'On/Off', + 'Units Details': 'On/Off', 'Writable': 'TRUE', 'Starting Value': '0', 'Type': 'boolean', + 'Notes': 'Point for heartbeat toggle'}, + {'Point Name': 'Catfact', 'Volttron Point Name': 'Catfact', 'Units': 'No cat fact', + 'Units Details': 'No cat fact', 'Writable': 'TRUE', 'Starting Value': 'No cat fact', 'Type': 'str', + 'Notes': 'Cat fact extract from REST API'}] + register_type_list: + Example: + [RestAPIRegister, RestAPIRegister, RestAPIRegister, RandomBoolRegister] + """ + pass + return register_type_list + + def parse_config(self, csv_config, driver_config_in_json_config): # TODO: this configDict is from *.csv not .config + # print("========================================== csv_config, ", csv_config) + # print("========================================== driver_config_in_json_config, ", driver_config_in_json_config) + + # driver_config: DriverConfig = DriverConfig(csv_config) + # valid_csv_config = DriverConfig(csv_config).key_validate() + # print("========================================== valid_csv_config, ", valid_csv_config) + + if csv_config is None: # TODO: leave it now. Later for central data check + return + + register_types: List[ImplementedRegister] = self.pass_register_types(csv_config, driver_config_in_json_config) + valid_csv_config = csv_config # TODO: Design the config check (No config check for now.) + for reg_def, register_type_iter in zip(valid_csv_config, register_types): + # Skip lines that have no address yet. # TODO: understand why + if not reg_def['Point Name']: + continue + + point_name = reg_def['Volttron Point Name'] + type_name = reg_def.get("Data Type", 'string') + reg_type = type_mapping.get(type_name, str) + units = reg_def['Units'] + read_only = reg_def['Writable'].lower() != 'true' # TODO: watch out for this is opposite logic + + description = reg_def.get('Notes', '') + + # default_value = reg_def.get("defaultvalue", 'sin').strip() + default_value = reg_def.get( + "Default Value") # TODO: redesign default value logic, e.g., beable to map to real python type + if not default_value: + default_value = None + + # register_type = FakeRegister if not point_name.startswith('Cat') else CatfactRegister # TODO: change this + register_type = register_type_iter # TODO: Inconventional, document this. + + # print("========================================== point_name, ", point_name) + # print("========================================== reg_type, ", reg_type) + # print("========================================== units, ", units) + # print("========================================== read_only, ", read_only) + # print("========================================== default_value, ", default_value) + # print("========================================== description, ", description) + # print("========================================== reg_def, ", reg_def) + # Note: the following is to init a register_type object, e.g., WrapperRegister + try: + # register: WrapperRegister = register_type(driver_config=driver_config_in_json_config, + # point_name=point_name, + # data_type=reg_type, # TODO: make it more clear in documentation + # units=units, + # read_only=read_only, + # default_value=default_value, + # description=description, + # csv_config=csv_config, + # reg_def=reg_def) + + register: WrapperRegister = self.create_register(driver_config=driver_config_in_json_config, + point_name=point_name, + data_type=reg_type, + # TODO: make it more clear in documentation + units=units, + read_only=read_only, + default_value=default_value, + description=description, + csv_config=csv_config, + reg_def=reg_def, + register_type=register_type) + if default_value is not None: + self.set_default(point_name, register.value) + + self.insert_register(register) + except Exception as e: + print(e) + + + + def create_register(self, driver_config, + point_name, + data_type, + units, + read_only, + default_value, + description, + csv_config, + reg_def, + register_type, *args, **kwargs) -> ImplementedRegister: + pass + """ + Factory method to init (WrapperRegister) register object + + :param register_type: the class name of the to-be-created register, e.g., WrapperRegister + :param driver_config_in_json_config: json config file, + :param csv_config: csv config file, Dict[str, str] + + """ + register: WrapperRegister = register_type(driver_config=driver_config, + point_name=point_name, + data_type=data_type, # TODO: make it more clear in documentation + units=units, + read_only=read_only, + default_value=default_value, + description=description, + csv_config=csv_config, + reg_def=reg_def) + return register + + def insert_register(self, register: WrapperRegister): + """ + Inserts a register into the :py:class:`Interface`. + + :param register: Register to add to the interface. + :type register: :py:class:`BaseRegister` + """ + register_point: str = register.point_name + self.point_map[register_point] = register + + register_type = register.get_register_type() + self.registers[register_type].append(register) + + def get_point(self, point_name, **kwargs) -> RegisterValue: + """ + Override BasicInvert method + Note: this method should be evoked by vip agent + EXAMPLE: + rs = a.vip.rpc.call("platform.driver", "get_point", + "campus-vm/building-vm/Dnp3", + "AnalogInput_index0").get() + """ + register: WrapperRegister = self.get_register_by_name(point_name) + val = self.get_reg_point(register) + return val + + # def _set_point(self, point_name: str, + # value_to_set: RegisterValue): # TODO: this method has some problem. Understand the logic: overall + example + + def set_point(self, point_name, value): + """ + Override/Restate BasicInvert method for convenience + Note: this method should be evoked by vip agent + EXAMPLE: + rs = a.vip.rpc.call("platform.driver", "set_point", + "campus-vm/building-vm/Dnp3", + "AnalogInput_index0", 0.543).get() + """ + # result = self._set_point(point_name, value) + # self._tracker.mark_dirty_point(point_name) + return super().set_point(point_name, value) + + def _set_point(self, point_name, value, **kwargs): + """ + Parameters + ---------- + point_name + value + + Returns + ------- + + """ + # value_to_set = value + register: ImplementedRegister = self.get_register_by_name(point_name) + + # response = self.set_reg_point_w_verification(value_to_set=value, register=register) + response = self.set_reg_point_async_w_verification(value_to_set=value, register=register) + return response + + @staticmethod + def get_reg_point(register: ImplementedRegister): + """ + Core logic for get_point + """ + return register.value + + @staticmethod + def set_reg_point(register: ImplementedRegister, value_to_set: RegisterValue): + """ + Core logic for set_point, i.e., _set_point without verification + Note: Can be used for vip-agent-mock testing + """ + set_pt_response = register.set_register_value(value=value_to_set) + return set_pt_response + + @classmethod + def set_reg_point_w_verification(cls, value_to_set: RegisterValue, register: ImplementedRegister, + relax_verification=True): + """ + Core logic for set_point, i.e., _set_point with verification + Note: Can be used for vip-agent-mock testing + """ + # Note: leave register method to verify, e.g., check writability. + + # set point workflow + set_pt_response = cls.set_reg_point(register=register, value_to_set=value_to_set) + + # verify with get_point + get_pt_response = cls.get_reg_point(register) + + success_flag_strict = (get_pt_response == value_to_set) + success_flag_relax = (str(get_pt_response) == str(value_to_set)) + if relax_verification: + success_flag = success_flag_relax + else: + success_flag = success_flag_strict + + response = {"success_flag": success_flag, + "value_to_set": value_to_set, + "set_pt_response": set_pt_response, + "get_pt_response": get_pt_response} + if not success_flag: + _log.warning(f"Set value failed, {response}") + return response + + @classmethod + def set_reg_point_async_w_verification(cls, value_to_set: RegisterValue, register: ImplementedRegister, + relax_verification=True): + """ + Counterpart of set_reg_point_w_verification for asynchronous workflow with delay and retry. + """ + + # set point workflow + set_pt_response = cls.set_reg_point(register=register, value_to_set=value_to_set) + + # verify with get_point + get_pt_response = cls.get_reg_point(register) + + def check_success_flag(): + _success_flag_strict = (get_pt_response == value_to_set) + _success_flag_relax = (str(get_pt_response) == str(value_to_set)) + if relax_verification: + _success_flag = _success_flag_relax + else: + _success_flag = _success_flag_strict + return _success_flag + + # note: only delay and retry the read/get logic NOT the send/set logic + # note: hard-coded delay time and number of retry. Use small delay, large retry number strategy. + # For local instances, 2 sec should be sufficient. + retry_delay = 0.2 + retry_max = 20 + retry_count = 0 + success_flag = check_success_flag() + while not success_flag and retry_count < retry_max: + sleep(retry_delay) + retry_count += 1 + + get_pt_response = cls.get_reg_point(register) + + success_flag = check_success_flag() + + response = {"success_flag": success_flag, + "value_to_set": value_to_set, + "set_pt_response": set_pt_response, + "get_pt_response": get_pt_response} + if not success_flag: + _log.warning(f"Set value failed, {response}") + return response + + def _scrape_all(self) -> Dict[str, any]: + result: Dict[str, RegisterValue] = {} # Dict[register.point_name, register.value] + read_registers = self.get_registers_by_type(reg_type="byte", + read_only=True) # TODO: Parameterize the "byte" hard-code here + write_registers = self.get_registers_by_type(reg_type="byte", read_only=False) + all_registers: List[ImplementedRegister] = read_registers + write_registers + for register in all_registers: + result[register.point_name] = register.value + return result + + def get_register_by_name(self, name: str) -> WrapperRegister: + """ + Get a register by it's point name. + + :param name: Point name of register. + :type name: str + :return: An instance of BaseRegister + :rtype: :py:class:`BaseRegister` + """ + try: + return self.point_map[name] + except KeyError: + raise DriverInterfaceError("Point not configured on device: " + name) + + +class WrapperInterfaceNew: + """ + Use composition instead of inheritance + """ + + def __init__(self, *args, **kwargs): + # self.basic_revert = BasicRevert(**kwargs) + # self.basic_interface = BaseInterface(**kwargs) + self.basic_revert = BasicRevert() + self.basic_interface = BaseInterface() + self._tracker = self.basic_revert._tracker + + self.point_map: Dict[str, ImplementedRegister] = {} # {register.point_name: register} + self.register_types: List[ + ImplementedRegister] = [] # TODO: add sanity check for restister_types, e.g., count == register counts + + self.csv_config = None # TODO: try to get this value, potentially from def configure. get inspiration from modbus_tk testing + self.driver_config_in_json_config = None # TODO: try to get this value, potentially from def configure + + def configure(self, driver_config_in_json_config: dict, csv_config: List[ + dict]): # TODO: ask driver.py, BaseInterface.configure to update signature when evoking + """ + Used by driver.py + def get_interface(self, driver_type, config_dict, config_string): + interface.configure(config_dict, config_string) + + Parameters # TODO: follow BaseInterface.configure signatures. But the names are wrong. + ---------- + driver_config_in_json_config: associated with `driver_config` in driver-config.config (json-like file) + user inputs are put here, e.g., IP address, url, etc. + csv_config: associated with the whole driver-config.csv file + Examples: + [{'Point Name': 'Heartbeat', 'Volttron Point Name': 'Heartbeat', 'Units': 'On/Off', + 'Units Details': 'On/Off', 'Writable': 'TRUE', 'Starting Value': '0', 'Type': 'boolean', + 'Notes': 'Point for heartbeat toggle'}, + {'Point Name': 'Catfact', 'Volttron Point Name': 'Catfact', 'Units': 'No cat fact', + 'Units Details': 'No cat fact', 'Writable': 'TRUE', 'Starting Value': 'No cat fact', 'Type': 'str', + 'Notes': 'Cat fact extract from REST API'}] + + """ + # print("========================================== csv_config, ", csv_config) + # print("========================================== driver_config_in_json_config, ", driver_config_in_json_config) + self.csv_config = csv_config + self.driver_config_in_json_config = driver_config_in_json_config + + # TODO configuration validation, i.e., self.config_check(...) + # self.config_check + self.parse_config(csv_config, driver_config_in_json_config) + + def parse_config(self, csv_config, driver_config_in_json_config, + register_type_list): # TODO: this configDict is from *.csv not .config + # print("========================================== csv_config, ", csv_config) + # print("========================================== driver_config_in_json_config, ", driver_config_in_json_config) + + # driver_config: DriverConfig = DriverConfig(csv_config) + # valid_csv_config = DriverConfig(csv_config).key_validate() + # print("========================================== valid_csv_config, ", valid_csv_config) + + if csv_config is None: # TODO: leave it now. Later for central data check + return + + # register_types: List[ImplementedRegister] = register_type_list + register_types: List[ImplementedRegister] = self.pass_register_types(csv_config, driver_config_in_json_config) + valid_csv_config = csv_config # TODO: Design the config check (No config check for now.) + for reg_def, register_type_iter in zip(valid_csv_config, register_types): + # Skip lines that have no address yet. # TODO: understand why + if not reg_def['Point Name']: + continue + + point_name = reg_def['Volttron Point Name'] + type_name = reg_def.get("Data Type", 'string') + reg_type = type_mapping.get(type_name, str) + units = reg_def['Units'] + read_only = reg_def['Writable'].lower() != 'true' # TODO: watch out for this is opposite logic + + description = reg_def.get('Notes', '') + + # default_value = reg_def.get("defaultvalue", 'sin').strip() + default_value = reg_def.get( + "Default Value") # TODO: redesign default value logic, e.g., beable to map to real python type + if not default_value: + default_value = None + + # register_type = FakeRegister if not point_name.startswith('Cat') else CatfactRegister # TODO: change this + register_type = register_type_iter # TODO: Inconventional, document this. + + # print("========================================== point_name, ", point_name) + # print("========================================== reg_type, ", reg_type) + # print("========================================== units, ", units) + # print("========================================== read_only, ", read_only) + # print("========================================== default_value, ", default_value) + # print("========================================== description, ", description) + # print("========================================== reg_def, ", reg_def) + # Note: the following is to init a register_type object, e.g., WrapperRegister + try: + register: WrapperRegister = self.create_register(driver_config=driver_config_in_json_config, + point_name=point_name, + data_type=reg_type, + # TODO: make it more clear in documentation + units=units, + read_only=read_only, + default_value=default_value, + description=description, + csv_config=csv_config, + reg_def=reg_def, + register_type=register_type) + + if default_value: + self.basic_revert.set_default(point_name, register.value) + + self.insert_register(register) + + except Exception as e: + print(e) + + @staticmethod + @abc.abstractmethod + def pass_register_types(csv_config: dict, driver_config_in_json_config: List[dict], + register_type_list: List[ImplementedRegister] = None) -> List[ImplementedRegister]: + """ + For ingesting the register types list + Will be used by concrete Interface class inherit this template + + Parameters + ---------- + driver_config_in_json_config: associated with `driver_config` in driver-config.config (json-like file) + user inputs are put here, e.g., IP address, url, etc. + csv_config: associated with the whole driver-config.csv file + Examples: + [{'Point Name': 'Heartbeat', 'Volttron Point Name': 'Heartbeat', 'Units': 'On/Off', + 'Units Details': 'On/Off', 'Writable': 'TRUE', 'Starting Value': '0', 'Type': 'boolean', + 'Notes': 'Point for heartbeat toggle'}, + {'Point Name': 'Catfact', 'Volttron Point Name': 'Catfact', 'Units': 'No cat fact', + 'Units Details': 'No cat fact', 'Writable': 'TRUE', 'Starting Value': 'No cat fact', 'Type': 'str', + 'Notes': 'Cat fact extract from REST API'}] + register_type_list: + Example: + [RestAPIRegister, RestAPIRegister, RestAPIRegister, RandomBoolRegister] + """ + pass + return register_type_list + + def create_register(self, driver_config, + point_name, + data_type, + units, + read_only, + default_value, + description, + csv_config, + reg_def, + register_type, *args, **kwargs) -> ImplementedRegister: + pass + """ + Factory method to init (WrapperRegister) register object + + :param register_type: the class name of the to-be-created register, e.g., WrapperRegister + :param driver_config_in_json_config: json config file, + :param csv_config: csv config file, Dict[str, str] + + """ + register: WrapperRegister = register_type(driver_config=driver_config, + point_name=point_name, + data_type=data_type, # TODO: make it more clear in documentation + units=units, + read_only=read_only, + default_value=default_value, + description=description, + csv_config=csv_config, + reg_def=reg_def) + return register + + def insert_register(self, register: WrapperRegister): + """ + Inserts a register into the :py:class:`Interface`. + + :param register: Register to add to the interface. + :type register: :py:class:`BaseRegister` + """ + register_point: str = register.point_name + self.point_map[register_point] = register + + register_type = register.get_register_type() + self.basic_interface.registers[register_type].append(register) + + def get_point(self, point_name, **kwargs) -> RegisterValue: + register: WrapperRegister = self.get_register_by_name(point_name) + # val: RegisterValue = register.get_register_value() + + # return "testing_value" + return register.value + + def get_register_by_name(self, name: str) -> Register: + return self.basic_interface.get_register_by_name(name) + + def set_point(self, point_name, value): + """ + Implementation of :py:meth:`BaseInterface.set_point` + + Passes arguments through to :py:meth:`BasicRevert._set_point` + """ + # return self.basic_revert.set_point(point_name, value) + result = self._set_point(point_name, value) + self._tracker.mark_dirty_point(point_name) + return result + + def _set_point(self, point_name, value, **kwargs): + """ + Parameters + ---------- + point_name + value + + Returns + ------- + + """ + value_to_set = value + register: ImplementedRegister = self.get_register_by_name(point_name) + # Note: leave register method to verify, e.g., check writability. + # register.value(value_to_set) + # value_response: RegisterValue = register.value + + set_pt_response = register.set_register_value(value=value_to_set) + # verify with get_point + get_pt_response = self.get_point(point_name=point_name) + + success_flag_strict = (get_pt_response == value_to_set) + success_flag_relax = (str(get_pt_response) == str(value_to_set)) + success_flag = success_flag_relax + + response = {"success_flag": success_flag, + "value_to_set": value_to_set, + "set_pt_response": set_pt_response, + "get_pt_response": get_pt_response} + if not success_flag: + _log.warning(f"Set value failed, {response}") + return response + + def scrape_all(self): + """ + Implementation of :py:meth:`BaseInterface.scrape_all` + """ + return self.basic_revert.scrape_all() + + +class DriverInterfaceError(Exception): + pass diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/examples/dnp3.config b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/examples/dnp3.config new file mode 100644 index 0000000000..fea35bc607 --- /dev/null +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/examples/dnp3.config @@ -0,0 +1,11 @@ +{ + "driver_config": {"master_ip": "0.0.0.0", "outstation_ip": "127.0.0.1", + "master_id": 2, "outstation_id": 1, + "port": 20000}, + "registry_config":"config://dnp3.csv", + "driver_type": "dnp3", + "interval": 5, + "timezone": "UTC", + "publish_depth_first_all": true, + "heart_beat_point": "random_bool" +} diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/examples/dnp3.csv b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/examples/dnp3.csv new file mode 100644 index 0000000000..e71b832b3d --- /dev/null +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/examples/dnp3.csv @@ -0,0 +1,17 @@ +Point Name,Volttron Point Name,Group,Variation,Index,Scaling,Units,Writable,Notes +AnalogInput_index0,AnalogInput_index0,30,6,0,1,NA,FALSE,Double Analogue input without status +AnalogInput_index1,AnalogInput_index1,30,6,1,1,NA,FALSE,Double Analogue input without status +AnalogInput_index2,AnalogInput_index2,30,6,2,1,NA,FALSE,Double Analogue input without status +AnalogInput_index3,AnalogInput_index3,30,6,3,1,NA,FALSE,Double Analogue input without status +BinaryInput_index0,BinaryInput_index0,1,2,0,1,NA,FALSE,Single bit binary input with status +BinaryInput_index1,BinaryInput_index1,1,2,1,1,NA,FALSE,Single bit binary input with status +BinaryInput_index2,BinaryInput_index2,1,2,2,1,NA,FALSE,Single bit binary input with status +BinaryInput_index3,BinaryInput_index3,1,2,3,1,NA,FALSE,Single bit binary input with status +AnalogOutput_index0,AnalogOutput_index0,40,4,0,1,NA,TRUE,Double-precision floating point with flags +AnalogOutput_index1,AnalogOutput_index1,40,4,1,1,NA,TRUE,Double-precision floating point with flags +AnalogOutput_index2,AnalogOutput_index2,40,4,2,1,NA,TRUE,Double-precision floating point with flags +AnalogOutput_index3,AnalogOutput_index3,40,4,3,1,NA,TRUE,Double-precision floating point with flags +BinaryOutput_index0,BinaryOutput_index0,10,2,0,1,NA,TRUE,Binary Output with flags +BinaryOutput_index1,BinaryOutput_index1,10,2,1,1,NA,TRUE,Binary Output with flags +BinaryOutput_index2,BinaryOutput_index2,10,2,2,1,NA,TRUE,Binary Output with flags +BinaryOutput_index3,BinaryOutput_index3,10,2,3,1,NA,TRUE,Binary Output with flags diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py new file mode 100644 index 0000000000..73eaf8ad9e --- /dev/null +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver.py @@ -0,0 +1,504 @@ +import pytest +import gevent +import logging +import time +import csv +import json +from pathlib import Path +import random + +from services.core.PlatformDriverAgent.platform_driver.interfaces. \ + dnp3 import UserDevelopRegisterDnp3 +from pydnp3 import opendnp3 +from services.core.PlatformDriverAgent.platform_driver.interfaces. \ + dnp3.dnp3 import Interface as DNP3Interface + +from dnp3_python.dnp3station.master_new import MyMasterNew +from dnp3_python.dnp3station.outstation_new import MyOutStationNew + +import os + +TEST_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class TestDummy: + """ + Dummy test to check pytest setup + """ + + def test_dummy(self): + print("I am a silly dummy test.") + + +@pytest.fixture( + scope="module" +) +def outstation_app(request): + """ + outstation using default configuration (including default database) + Note: since outstation cannot shut down gracefully, + outstation_app fixture need to in "module" scope to prevent interrupting pytest during outstation shut-down + """ + # Note: allow parsing argument to fixture change port number using `request.param` + try: + port = request.param + except AttributeError: + port = 20000 + outstation_appl = MyOutStationNew(port=port) # Note: using default port 20000 + outstation_appl.start() + # time.sleep(3) + yield outstation_appl + # clean-up + outstation_appl.shutdown() + + +@pytest.fixture( + # scope="module" +) +def master_app(request): + """ + master station using default configuration + Note: outstation needs to exist first to make connection. + """ + + # Note: allow parsing argument to fixture change port number using `request.param` + try: + port = request.param + except AttributeError: + port = 20000 + # Note: using default port 20000, + # Note: using small "stale_if_longer_than" to force update + master_appl = MyMasterNew(port=port, stale_if_longer_than=0.1) + master_appl.start() + # Note: add delay to prevent conflict + # (there is a delay when master shutdown. And all master shares the same config) + time.sleep(1) + yield master_appl + # clean-up + master_appl.shutdown() + time.sleep(1) + + +class TestStation: + """ + Testing the underlying pydnp3 package station-related fuctions. + """ + + def test_station_init(self, master_app, outstation_app): + # master_app = MyMasterNew() + # master_app.start() + driver_wrapper_init_arg = {'driver_config': {}, 'point_name': "", 'data_type': "", 'units': "", 'read_only': ""} + UserDevelopRegisterDnp3(master_application=master_app, reg_def={}, + **driver_wrapper_init_arg) + + def test_station_get_val_analog_input_float(self, master_app, outstation_app): + + # outstation update with values + analog_input_val = [1.2454, 33453.23, 45.21] + for i, val_update in enumerate(analog_input_val): + outstation_app.apply_update(opendnp3.Analog(value=val_update, + flags=opendnp3.Flags(24), + time=opendnp3.DNPTime(3094)), + index=i) + # Note: group=30, variation=6 is AnalogInputFloat + for i, val_update in enumerate(analog_input_val): + val_get = master_app.get_val_by_group_variation_index(group=30, variation=6, index=i) + # print(f"===val_update {val_update}, val_get {val_get}") + assert val_get == val_update + + time.sleep(1) # add delay buffer to pass the "stale_if_longer_than" checking statge + + # outstation update with random values + analog_input_val_random = [random.random() for i in range(3)] + for i, val_update in enumerate(analog_input_val_random): + outstation_app.apply_update(opendnp3.Analog(value=val_update), + index=i) + # Note: group=30, variation=6 is AnalogInputFloat + for i, val_update in enumerate(analog_input_val_random): + val_get = master_app.get_val_by_group_variation_index(group=30, variation=6, index=i) + # print(f"===val_update {val_update}, val_get {val_get}") + assert val_get == val_update + + def test_station_set_val_analog_input_float(self, master_app, outstation_app): + + # outstation update with values + analog_output_val = [1.2454, 33453.23, 45.21] + for i, val_to_set in enumerate(analog_output_val): + master_app.send_direct_point_command(group=40, variation=4, index=i, + val_to_set=val_to_set) + # Note: group=40, variation=4 is AnalogOutFloat + for i, val_to_set in enumerate(analog_output_val): + val_get = master_app.get_val_by_group_variation_index(group=40, variation=4, index=i) + # print(f"===val_update {val_update}, val_get {val_get}") + assert val_get == val_to_set + + time.sleep(1) # add delay buffer to pass the "stale_if_longer_than" checking statge + + # outstation update with random values + analog_output_val_random = [random.random() for i in range(3)] + for i, val_to_set in enumerate(analog_output_val_random): + master_app.send_direct_point_command(group=40, variation=4, index=i, + val_to_set=val_to_set) + # Note: group=40, variation=4 is AnalogOutFloat + for i, val_to_set in enumerate(analog_output_val_random): + val_get = master_app.get_val_by_group_variation_index(group=40, variation=4, index=i) + # print(f"===val_update {val_update}, val_get {val_get}") + assert val_get == val_to_set + + +@pytest.fixture +def dnp3_inherit_init_args(csv_config, driver_config_in_json_config): + """ + args required for parent class init (i.e., class WrapperRegister) + """ + args = {'driver_config': driver_config_in_json_config, + 'point_name': "", + 'data_type': "", + 'units': "", + 'read_only': ""} + return args + + +@pytest.fixture +def driver_config_in_json_config(): + """ + associated with `driver_config` in driver-config.config (json-like file) + user inputs are put here, e.g., IP address, url, etc. + """ + json_path = Path("./testing_data/dnp3.config") + json_path = Path(TEST_DIR, json_path) + with open(json_path) as json_f: + driver_config = json.load(json_f) + k = "driver_config" + return {k: driver_config.get(k)} + + +@pytest.fixture +def csv_config(): + """ + associated with the whole driver-config.csv file + """ + csv_path = Path("./testing_data/dnp3.csv") + csv_path = Path(TEST_DIR, csv_path) + with open(csv_path) as f: + reader = csv.DictReader(f, delimiter=',') + csv_config = [row for row in reader] + + return csv_config + + +@pytest.fixture +def reg_def_dummy(): + """ + register definition, row of csv config file + """ + # reg_def = {'Point Name': 'AnalogInput_index0', 'Volttron Point Name': 'AnalogInput_index0', + # 'Group': '30', 'Variation': '6', 'Index': '0', 'Scaling': '1', 'Units': 'NA', + # 'Writable': 'FALSE', 'Notes': 'Double Analogue input without status'} + reg_def = {'Point Name': 'pn', 'Volttron Point Name': 'pn', + 'Group': 'int', 'Variation': 'int', 'Index': 'int', 'Scaling': '1', 'Units': 'NA', + 'Writable': 'NA', 'Notes': ''} + return reg_def + + +class TestDNPRegister: + """ + Tests for UserDevelopRegisterDnp3 class + + init + + get_register_value + analog input float + analog input int + binary input + """ + + def test_init(self, master_app, csv_config, dnp3_inherit_init_args): + for reg_def in csv_config: + UserDevelopRegisterDnp3(master_application=master_app, + reg_def=reg_def, + **dnp3_inherit_init_args + ) + + def test_get_register_value_analog_float(self, outstation_app, master_app, csv_config, + dnp3_inherit_init_args, reg_def_dummy): + + # dummy test variable + analog_input_val = [445.33, 1123.56, 98.456] + [random.random() for i in range(3)] + + # dummy reg_def (csv config row) + # Note: group = 30, variation = 6 is AnalogInputFloat + reg_def = reg_def_dummy + reg_defs = [] + for i in range(len(analog_input_val)): + reg_def["Group"] = "30" + reg_def["Variation"] = "6" + reg_def["Index"] = str(i) + reg_defs.append(reg_def.copy()) # Note: Python gotcha, mutable don't evaluate til the end of the loop. + + # outstation update values + for i, val_update in enumerate(analog_input_val): + outstation_app.apply_update(opendnp3.Analog(value=val_update), index=i) + + # verify: driver read value + for i, (val_update, csv_row) in enumerate(zip(analog_input_val, reg_defs)): + # print(f"====== reg_defs {reg_defs}, analog_input_val {analog_input_val}") + dnp3_register = UserDevelopRegisterDnp3(master_application=master_app, + reg_def=csv_row, + **dnp3_inherit_init_args + ) + val_get = dnp3_register.get_register_value() + # print("===========val_get, val_update", val_get, val_update) + assert val_get == val_update + + def test_get_register_value_analog_int(self, outstation_app, master_app, csv_config, + dnp3_inherit_init_args, reg_def_dummy): + + # dummy test variable + analog_input_val = [345, 1123, 98] + [random.randint(1, 100) for i in range(3)] + + # dummy reg_def (csv config row) + # Note: group = 30, variation = 1 is AnalogInputInt32 + reg_def = reg_def_dummy + reg_defs = [] + for i in range(len(analog_input_val)): + reg_def["Group"] = "30" + reg_def["Variation"] = "1" + reg_def["Index"] = str(i) + reg_defs.append(reg_def.copy()) # Note: Python gotcha, mutable don't evaluate til the end of the loop. + + # outstation update values + for i, val_update in enumerate(analog_input_val): + outstation_app.apply_update(opendnp3.Analog(value=val_update), index=i) + + # verify: driver read value + for i, (val_update, csv_row) in enumerate(zip(analog_input_val, reg_defs)): + # print(f"====== reg_defs {reg_defs}, analog_input_val {analog_input_val}") + dnp3_register = UserDevelopRegisterDnp3(master_application=master_app, + reg_def=csv_row, + **dnp3_inherit_init_args + ) + val_get = dnp3_register.get_register_value() + # print("===========val_get, val_update", val_get, val_update) + assert val_get == val_update + + def test_get_register_value_binary(self, outstation_app, master_app, csv_config, + dnp3_inherit_init_args, reg_def_dummy): + + # dummy test variable + binary_input_val = [True, False, True] + [random.choice([True, False]) for i in range(3)] + + # dummy reg_def (csv config row) + # Note: group = 1, variation = 2 is BinaryInput + reg_def = reg_def_dummy + reg_defs = [] + for i in range(len(binary_input_val)): + reg_def["Group"] = "1" + reg_def["Variation"] = "2" + reg_def["Index"] = str(i) + reg_defs.append(reg_def.copy()) # Note: Python gotcha, mutable don't evaluate til the end of the loop. + + # outstation update values + for i, val_update in enumerate(binary_input_val): + outstation_app.apply_update(opendnp3.Binary(value=val_update), index=i) + + # verify: driver read value + for i, (val_update, csv_row) in enumerate(zip(binary_input_val, reg_defs)): + # print(f"====== reg_defs {reg_defs}, analog_input_val {analog_input_val}") + dnp3_register = UserDevelopRegisterDnp3(master_application=master_app, + reg_def=csv_row, + **dnp3_inherit_init_args + ) + val_get = dnp3_register.get_register_value() + # print(f"=========== i {i}, val_get {val_get}, val_update {val_update}") + assert val_get == val_update + + +class TestDNP3RegisterControlWorkflow: + + def test_set_register_value_analog_float(self, outstation_app, master_app, csv_config, + dnp3_inherit_init_args, reg_def_dummy): + + # dummy test variable + # Note: group=40, variation=4 is AnalogOutputDoubleFloat + output_val = [343.23, 23.1109, 58.2] + [random.random() for i in range(3)] + + # dummy reg_def (csv config row) + # Note: group = 1, variation = 2 is BinaryInput + reg_def = reg_def_dummy + reg_defs = [] + for i in range(len(output_val)): + reg_def["Group"] = "40" + reg_def["Variation"] = "4" + reg_def["Index"] = str(i) + reg_defs.append(reg_def.copy()) # Note: Python gotcha, mutable don't evaluate til the end of the loop. + + # master set values + for i, (val_set, csv_row) in enumerate(zip(output_val, reg_defs)): + dnp3_register = UserDevelopRegisterDnp3(master_application=master_app, + reg_def=csv_row, + **dnp3_inherit_init_args + ) + dnp3_register.set_register_value(value=val_set) + + # verify: driver read value + for i, (val_set, csv_row) in enumerate(zip(output_val, reg_defs)): + # print(f"====== reg_defs {reg_defs}, analog_input_val {analog_input_val}") + dnp3_register = UserDevelopRegisterDnp3(master_application=master_app, + reg_def=csv_row, + **dnp3_inherit_init_args + ) + val_get = dnp3_register.get_register_value() + # print("===========val_get, val_update", val_get, val_update) + assert val_get == val_set + + def test_set_register_value_analog_int(self, outstation_app, master_app, csv_config, + dnp3_inherit_init_args, reg_def_dummy): + + # dummy test variable + # Note: group=40, variation=4 is AnalogOutputDoubleFloat + output_val = [45343, 344, 221] + [random.randint(1, 1000) for i in range(3)] + + # dummy reg_def (csv config row) + # Note: group = 1, variation = 2 is BinaryInput + reg_def = reg_def_dummy + reg_defs = [] + for i in range(len(output_val)): + reg_def["Group"] = "40" + reg_def["Variation"] = "1" + reg_def["Index"] = str(i) + reg_defs.append(reg_def.copy()) # Note: Python gotcha, mutable don't evaluate til the end of the loop. + + # master set values + for i, (val_set, csv_row) in enumerate(zip(output_val, reg_defs)): + dnp3_register = UserDevelopRegisterDnp3(master_application=master_app, + reg_def=csv_row, + **dnp3_inherit_init_args + ) + dnp3_register.set_register_value(value=val_set) + + # verify: driver read value + for i, (val_set, csv_row) in enumerate(zip(output_val, reg_defs)): + # print(f"====== reg_defs {reg_defs}, analog_input_val {analog_input_val}") + dnp3_register = UserDevelopRegisterDnp3(master_application=master_app, + reg_def=csv_row, + **dnp3_inherit_init_args + ) + val_get = dnp3_register.get_register_value() + # print("===========val_get, val_update", val_get, val_update) + assert val_get == val_set + + def test_set_register_value_binary(self, outstation_app, master_app, csv_config, + dnp3_inherit_init_args, reg_def_dummy): + + # dummy test variable + # Note: group=40, variation=4 is AnalogOutputDoubleFloat + output_val = [True, False, True] + [random.choice([True, False]) for i in range(3)] + + # dummy reg_def (csv config row) + # Note: group = 1, variation = 2 is BinaryInput + reg_def = reg_def_dummy + reg_defs = [] + for i in range(len(output_val)): + reg_def["Group"] = "10" + reg_def["Variation"] = "2" + reg_def["Index"] = str(i) + reg_defs.append(reg_def.copy()) # Note: Python gotcha, mutable don't evaluate til the end of the loop. + + # master set values + for i, (val_set, csv_row) in enumerate(zip(output_val, reg_defs)): + dnp3_register = UserDevelopRegisterDnp3(master_application=master_app, + reg_def=csv_row, + **dnp3_inherit_init_args + ) + dnp3_register.set_register_value(value=val_set) + + # verify: driver read value + for i, (val_set, csv_row) in enumerate(zip(output_val, reg_defs)): + # print(f"====== reg_defs {reg_defs}, analog_input_val {analog_input_val}") + dnp3_register = UserDevelopRegisterDnp3(master_application=master_app, + reg_def=csv_row, + **dnp3_inherit_init_args + ) + val_get = dnp3_register.get_register_value() + # print("===========val_get, val_update", val_get, val_update) + assert val_get == val_set + + +class TestDNP3InterfaceNaive: + + def test_init(self): + pass + dnp3_interface = DNP3Interface() + + def test_get_reg_point(self, outstation_app, master_app, csv_config, + dnp3_inherit_init_args, reg_def_dummy): + # dummy test variable + analog_input_val = [445.33, 1123.56, 98.456] + [random.random() for i in range(3)] + + # dummy reg_def (csv config row) + # Note: group = 30, variation = 6 is AnalogInputFloat + reg_def = reg_def_dummy + reg_defs = [] + for i in range(len(analog_input_val)): + reg_def["Group"] = "30" + reg_def["Variation"] = "6" + reg_def["Index"] = str(i) + reg_defs.append(reg_def.copy()) # Note: Python gotcha, mutable don't evaluate til the end of the loop. + + # outstation update values + for i, val_update in enumerate(analog_input_val): + outstation_app.apply_update(opendnp3.Analog(value=val_update), index=i) + + # verify: driver read value + dnp3_interface = DNP3Interface() + for i, (val_update, csv_row) in enumerate(zip(analog_input_val, reg_defs)): + # print(f"====== reg_defs {reg_defs}, analog_input_val {analog_input_val}") + dnp3_register = UserDevelopRegisterDnp3(master_application=master_app, + reg_def=csv_row, + **dnp3_inherit_init_args + ) + + val_get = dnp3_interface.get_reg_point(register=dnp3_register) + # print("======== dnp3_register.value", dnp3_register.value) + # print("===========val_get, val_update", val_get, val_update) + assert val_get == val_update + + def test_set_reg_point(self, outstation_app, master_app, csv_config, + dnp3_inherit_init_args, reg_def_dummy): + # dummy test variable + analog_output_val = [445.33, 1123.56, 98.456] + [random.random() for i in range(3)] + + # dummy reg_def (csv config row) + # Note: group = 30, variation = 6 is AnalogInputFloat + reg_def = reg_def_dummy + reg_defs = [] + for i in range(len(analog_output_val)): + reg_def["Group"] = "40" + reg_def["Variation"] = "4" + reg_def["Index"] = str(i) + reg_defs.append(reg_def.copy()) # Note: Python gotcha, mutable don't evaluate til the end of the loop. + + dnp3_interface = DNP3Interface() + + # dnp3_interface update values + for i, (val_update, csv_row) in enumerate(zip(analog_output_val, reg_defs)): + dnp3_register = UserDevelopRegisterDnp3(master_application=master_app, + reg_def=csv_row, + **dnp3_inherit_init_args + ) + dnp3_interface.set_reg_point(register=dnp3_register, value_to_set=val_update) + + # verify: driver read value + + for i, (val_update, csv_row) in enumerate(zip(analog_output_val, reg_defs)): + # print(f"====== reg_defs {reg_defs}, analog_input_val {analog_input_val}") + dnp3_register = UserDevelopRegisterDnp3(master_application=master_app, + reg_def=csv_row, + **dnp3_inherit_init_args + ) + + val_get = dnp3_interface.get_reg_point(register=dnp3_register) + # print("======== dnp3_register.value", dnp3_register.value) + # print("===========val_get, val_update", val_get, val_update) + assert val_get == val_update diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py new file mode 100644 index 0000000000..9a33d384c4 --- /dev/null +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/test_dnp3_driver_integration_volttron.py @@ -0,0 +1,208 @@ +import pytest +import gevent +import logging +import time +import random + +from volttron.platform import get_services_core, jsonapi + +from volttron.platform.agent.known_identities import PLATFORM_DRIVER + +from pydnp3 import opendnp3 + +from dnp3_python.dnp3station.outstation_new import MyOutStationNew +from pathlib import Path + +import os + +TEST_DIR = os.path.dirname(os.path.abspath(__file__)) + + +# TODO: add IP, port pool to avoid conflict +# TODO: make sleep more robust and flexible. (Currently relies on manually setup sleep time.) + + +class TestDummy: + """ + Dummy test to check pytest setup + """ + + def test_dummy(self): + print("I am a silly dummy test.") + + +@pytest.fixture( + scope="class" +) +def outstation_app_p20000(): + """ + outstation using default configuration (including default database) + Note: since outstation cannot shut down gracefully, + outstation_app fixture need to in "module" scope to prevent interrupting pytest during outstation shut-down + """ + port = 20000 + outstation_appl = MyOutStationNew(port=port) # Note: using default port 20000 + outstation_appl.start() + gevent.sleep(10) + yield outstation_appl + + outstation_appl.shutdown() + + +@pytest.mark.skip(reason="only for debugging purpose") +class TestDummyAgentFixture: + """ + Dummy test to check VOLTTRON agent (carry on test VOLTTRON instance) setup + """ + + def test_agent_dummy(self, dnp3_tester_agent): + print("I am a fixture agent dummy test.") + + +class TestDnp3DriverRPC: + + def test_interface_get_point( + self, + dnp3_tester_agent, + outstation_app_p20000, + ): + val_update = 7.124 + random.random() + outstation_app_p20000.apply_update(opendnp3.Analog(value=val_update, + flags=opendnp3.Flags(24), + time=opendnp3.DNPTime(3094)), + index=0) + + time.sleep(2) + + res_val = dnp3_tester_agent.vip.rpc.call("platform.driver", "get_point", + "campus-vm/building-vm/Dnp3-port20000", + "AnalogInput_index0").get(timeout=5) + + print(f"======res_val {res_val}") + assert res_val == val_update + + def test_interface_set_point( + self, + dnp3_tester_agent, + outstation_app_p20000, + ): + val_set = 8.342 + random.random() + + res_val = dnp3_tester_agent.vip.rpc.call("platform.driver", "set_point", + "campus-vm/building-vm/Dnp3-port20000", + "AnalogOutput_index0", val_set).get(timeout=5) + + # print(f"======res_val {res_val}") + # Expected output + # {'success_flag': True, 'value_to_set': 8.342, 'set_pt_response': None, 'get_pt_response': 8.342} + try: + assert res_val.get("success_flag") + except AssertionError: + print(f"======res_val {res_val}") + + @pytest.mark.skip(reason="TODO") + def test_scrape_all(self, ): + """ + Issue a get_point RPC call for the device and return the result. + + @param agent: The test Agent. + @param device_name: The driver name, by default: 'devices/device_name'. + @return: The dictionary mapping point names to their actual values from + the RPC call. + """ + # return agent.vip.rpc.call(PLATFORM_DRIVER, 'scrape_all', device_name) \ + # .get(timeout=10) + + @pytest.mark.skip(reason="TODO") + def test_revert_all(self, ): + """ + Issue a get_point RPC call for the device and return the result. + + @param agent: The test Agent. + @param device_name: The driver name, by default: 'devices/device_name'. + @return: Return value from the RPC call. + """ + # return agent.vip.rpc.call(PLATFORM_DRIVER, 'revert_device', + # device_name).get(timeout=10) + + @pytest.mark.skip(reason="TODO") + def test_revert_point(self, ): + """ + Issue a get_point RPC call for the named point and return the result. + + @param agent: The test Agent. + @param device_name: The driver name, by default: 'devices/device_name'. + @param point_name: The name of the point to query. + @return: Return value from the RPC call. + """ + # return agent.vip.rpc.call(PLATFORM_DRIVER, 'revert_point', + # device_name, point_name).get(timeout=10) + + +@pytest.fixture(scope="module") +# @pytest.fixture +def dnp3_tester_agent(request, volttron_instance): + """ + Build PlatformDriverAgent, add modbus driver & csv configurations + """ + + # Build platform driver agent + tester_agent = volttron_instance.build_agent(identity="test_dnp3_agent") + gevent.sleep(1) + capabilities = {'edit_config_store': {'identity': PLATFORM_DRIVER}} + # Note: commented out the add_capabilities due to complained by volttron_instance fixture, i.e., + # pytest.param(dict(messagebus='rmq', ssl_auth=True), + # marks=rmq_skipif), # complain add_capabilities + # dict(messagebus='zmq', auth_enabled=False), # complain add_capabilities + if volttron_instance.auth_enabled: + volttron_instance.add_capabilities(tester_agent.core.publickey, capabilities) + + # Clean out platform driver configurations + # wait for it to return before adding new config + tester_agent.vip.rpc.call(peer='config.store', + method='manage_delete_store', + identity=PLATFORM_DRIVER).get(timeout=5) + + json_config_path = Path("../examples/dnp3.config") + json_config_path = Path(TEST_DIR, json_config_path) + with open(json_config_path, "r") as f: + json_str_p20000 = f.read() + + csv_config_path = Path("../examples/dnp3.csv") + csv_config_path = Path(TEST_DIR, csv_config_path) + with open(csv_config_path, "r") as f: + csv_str = f.read() + + tester_agent.vip.rpc.call(peer='config.store', + method='manage_store', + identity=PLATFORM_DRIVER, + config_name="dnp3.csv", + raw_contents=csv_str, + config_type='csv' + ).get(timeout=5) + + tester_agent.vip.rpc.call('config.store', + method='manage_store', + identity=PLATFORM_DRIVER, + config_name="devices/campus-vm/building-vm/Dnp3-port20000", + raw_contents=json_str_p20000, + config_type='json' + ).get(timeout=5) + + platform_uuid = volttron_instance.install_agent( + agent_dir=get_services_core("PlatformDriverAgent"), + config_file={}, + start=True) + + gevent.sleep(10) # Note: important, wait for the agent to start and start the devices, otherwise rpc call may fail. + # time.sleep(10) # wait for the agent to start and start the devices + + def stop(): + """ + Stop platform driver agent + """ + volttron_instance.stop_agent(platform_uuid) + tester_agent.core.stop() + + yield tester_agent + request.addfinalizer(stop) diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/testing_data/dnp3.config b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/testing_data/dnp3.config new file mode 100644 index 0000000000..fea35bc607 --- /dev/null +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/testing_data/dnp3.config @@ -0,0 +1,11 @@ +{ + "driver_config": {"master_ip": "0.0.0.0", "outstation_ip": "127.0.0.1", + "master_id": 2, "outstation_id": 1, + "port": 20000}, + "registry_config":"config://dnp3.csv", + "driver_type": "dnp3", + "interval": 5, + "timezone": "UTC", + "publish_depth_first_all": true, + "heart_beat_point": "random_bool" +} diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/testing_data/dnp3.csv b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/testing_data/dnp3.csv new file mode 100644 index 0000000000..e71b832b3d --- /dev/null +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/dnp3/tests/testing_data/dnp3.csv @@ -0,0 +1,17 @@ +Point Name,Volttron Point Name,Group,Variation,Index,Scaling,Units,Writable,Notes +AnalogInput_index0,AnalogInput_index0,30,6,0,1,NA,FALSE,Double Analogue input without status +AnalogInput_index1,AnalogInput_index1,30,6,1,1,NA,FALSE,Double Analogue input without status +AnalogInput_index2,AnalogInput_index2,30,6,2,1,NA,FALSE,Double Analogue input without status +AnalogInput_index3,AnalogInput_index3,30,6,3,1,NA,FALSE,Double Analogue input without status +BinaryInput_index0,BinaryInput_index0,1,2,0,1,NA,FALSE,Single bit binary input with status +BinaryInput_index1,BinaryInput_index1,1,2,1,1,NA,FALSE,Single bit binary input with status +BinaryInput_index2,BinaryInput_index2,1,2,2,1,NA,FALSE,Single bit binary input with status +BinaryInput_index3,BinaryInput_index3,1,2,3,1,NA,FALSE,Single bit binary input with status +AnalogOutput_index0,AnalogOutput_index0,40,4,0,1,NA,TRUE,Double-precision floating point with flags +AnalogOutput_index1,AnalogOutput_index1,40,4,1,1,NA,TRUE,Double-precision floating point with flags +AnalogOutput_index2,AnalogOutput_index2,40,4,2,1,NA,TRUE,Double-precision floating point with flags +AnalogOutput_index3,AnalogOutput_index3,40,4,3,1,NA,TRUE,Double-precision floating point with flags +BinaryOutput_index0,BinaryOutput_index0,10,2,0,1,NA,TRUE,Binary Output with flags +BinaryOutput_index1,BinaryOutput_index1,10,2,1,1,NA,TRUE,Binary Output with flags +BinaryOutput_index2,BinaryOutput_index2,10,2,2,1,NA,TRUE,Binary Output with flags +BinaryOutput_index3,BinaryOutput_index3,10,2,3,1,NA,TRUE,Binary Output with flags diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/ecobee.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/ecobee.py index 6b2ceb6901..1e81a01d26 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/ecobee.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/ecobee.py @@ -287,10 +287,10 @@ def get_auth_config_from_store(self): :return: Fetch currently stored auth configuration info from config store, returns empty dict if none is present """ - configs = self.vip.rpc.call(CONFIGURATION_STORE, "manage_list_configs", PLATFORM_DRIVER).get(timeout=3) + configs = self.vip.rpc.call(CONFIGURATION_STORE, "list_configs", PLATFORM_DRIVER).get(timeout=3) if self.auth_config_path in configs: return jsonapi.loads(self.vip.rpc.call( - CONFIGURATION_STORE, "manage_get", PLATFORM_DRIVER, self.auth_config_path).get(timeout=3)) + CONFIGURATION_STORE, "get_config", PLATFORM_DRIVER, self.auth_config_path).get(timeout=3)) else: _log.warning("No Ecobee auth file found in config store") return {} diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/fakedriver.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/fakedriver.py index f4fd1ca0cc..cc4c51408b 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/fakedriver.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/fakedriver.py @@ -43,8 +43,6 @@ from math import pi from platform_driver.interfaces import BaseInterface, BaseRegister, BasicRevert -from csv import DictReader -from io import StringIO import logging _log = logging.getLogger(__name__) diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/config_cmd.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/config_cmd.py index ec58de4b96..497891a1df 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/config_cmd.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/config_cmd.py @@ -79,7 +79,7 @@ def set_device_type_maps(self): yaml_file = "{0}/{1}".format(self._directories['map_dir'], file_name) if file_name and os.stat(yaml_file).st_size: with open("{0}/maps.yaml".format(self._directories['map_dir'])) as yaml_file: - device_type_maps = yaml.load(yaml_file) + device_type_maps = yaml.safe_load(yaml_file) return device_type_maps def _sh(self, shell_command): @@ -165,7 +165,7 @@ def get_existed_file(self, file_dir, file_name): if file_dir: while True: if not os.path.exists("{0}/{1}".format(file_dir, file_name)): - print ("'{0}' file '{1}' does not exist in the directory '{2}'".format(file_type, + print("'{0}' file '{1}' does not exist in the directory '{2}'".format(file_type, file_name, file_dir)) if file_name == 'maps.yaml': @@ -268,20 +268,19 @@ def do_edit_directories(self, line): print("No change made to '{0}'".format(dir_key)) else: self._directories[dir_key] = dir + elif line not in self._directories: + print("Directory type '{0}' does not exist".format(line)) + print("Please select another directory type from: {0}".format([k for k in self._directories.keys()])) + print("Enter a directory type. Press if edit all: ", end='') + self.do_edit_directories(input().lower()) else: - if line not in self._directories: - print("Directory type '{0}' does not exist".format(line)) - print("Please select another directory type from: {0}".format([k for k in self._directories.keys()])) - print("Enter a directory type. Press if edit all: ", end='') - self.do_edit_directories(input().lower()) + print("Enter the directory path for {0}. Press if no change needed: ".format(line), end='') + dir_path = input() + dir = self.get_existed_directory(dir_path, line) if dir_path else None + if not dir or dir == self._directories[line]: + print("No change made to {0}".format(line)) else: - print("Enter the directory path for {0}. Press if no change needed: ".format(line), end='') - dir_path = input() - dir = self.get_existed_directory(dir_path, line) if dir_path else None - if not dir or dir == self._directories[line]: - print("No change made to {0}".format(line)) - else: - self._directories[line] = dir + self._directories[line] = dir self.do_list_directories('') @@ -818,7 +817,7 @@ def do_delete_volttron_config(self, line): print("DRIVER NAME".ljust(16) + "| VOLTTRON PATH") for d in drivers.keys(): print("{0:15} | {1}".format(d, drivers[d])) - print ("\nEnter driver name to delete: ", end='') + print("\nEnter driver name to delete: ", end='') driver_name = input() if driver_name not in drivers: diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/maps/__init__.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/maps/__init__.py index 210f960510..f7e3858547 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/maps/__init__.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/maps/__init__.py @@ -197,11 +197,10 @@ def _table(self): return table_map[table] except KeyError: raise Exception("Invalid modbus table '{0}' for register '{1}'".format(table, self._name)) + elif self._datatype == helpers.BOOL: + return helpers.COIL_READ_WRITE if self._writable else helpers.COIL_READ_ONLY else: - if self._datatype == helpers.BOOL: - return helpers.COIL_READ_WRITE if self._writable else helpers.COIL_READ_ONLY - else: - return helpers.REGISTER_READ_WRITE if self._writable else helpers.REGISTER_READ_ONLY + return helpers.REGISTER_READ_WRITE if self._writable else helpers.REGISTER_READ_ONLY @property def _op_mode(self): @@ -330,7 +329,7 @@ def __init__(self): yaml_path = os.path.dirname(__file__) + '/' + yaml_path with open(yaml_path, 'rb') as yaml_file: - for map in yaml.load(yaml_file): + for map in yaml.safe_load(yaml_file): map = dict((k.lower(), v) for k, v in map.items()) Catalog._data[map['name']] = Map(file=map.get('file', ''), map_dir=os.path.dirname(__file__), diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/maps/maps.yaml b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/maps/maps.yaml index 3ecf6f3237..ab387b700c 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/maps/maps.yaml +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/maps/maps.yaml @@ -48,4 +48,4 @@ - addressing: offset endian: big file: battery_meter.csv - name: battery_meter \ No newline at end of file + name: battery_meter diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_battery_meter.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_battery_meter.py index 43313652e2..5c7a868b66 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_battery_meter.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_battery_meter.py @@ -3,8 +3,10 @@ import logging import time +from struct import pack, unpack + from volttron.platform import get_services_core, jsonapi -from volttrontesting.utils.utils import get_rand_ip_and_port +from volttrontesting.utils.utils import get_rand_ip_and_port, is_running_in_container from platform_driver.interfaces.modbus_tk.server import Server from platform_driver.interfaces.modbus_tk.maps import Map, Catalog from volttron.platform.agent.known_identities import PLATFORM_DRIVER @@ -282,12 +284,12 @@ def agent(request, volttron_instance): # Clean out platform driver configurations # wait for it to return before adding new config md_agent.vip.rpc.call('config.store', - 'manage_delete_store', + 'delete_store', PLATFORM_DRIVER).get() # Add driver configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'devices/modbus_tk', jsonapi.dumps(DRIVER_CONFIG), @@ -295,14 +297,14 @@ def agent(request, volttron_instance): # Add csv configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'modbus_tk.csv', REGISTRY_CONFIG_STRING, config_type='csv') md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'modbus_tk_map.csv', REGISTER_MAP, @@ -332,7 +334,8 @@ def modbus_server(request): server_process = Server(address=IP, port=PORT) server_process.define_slave(1, modbus_client, unsigned=False) - + for k in registers_dict: + server_process.set_values(1, modbus_client().field_by_name(k), unpack('f', 0))) server_process.start() time.sleep(1) yield server_process @@ -384,6 +387,7 @@ def scrape_all(self, agent, device_name): return agent.vip.rpc.call(PLATFORM_DRIVER, 'scrape_all', device_name)\ .get(timeout=10) + @pytest.mark.xfail(is_running_in_container(), reason='Fails to set points on this test setup, only in Docker.') def test_scrape_all(self, agent): for key in registers_dict.keys(): self.set_point(agent, 'modbus_tk', key, registers_dict[key]) diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_driver_demo_board.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_driver_demo_board.py new file mode 100644 index 0000000000..ea58ded782 --- /dev/null +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_driver_demo_board.py @@ -0,0 +1,120 @@ +import os + +import gevent +import pytest +from volttron.platform.agent.known_identities import CONFIGURATION_STORE, PLATFORM_DRIVER +from volttron.platform import jsonapi +from volttrontesting.utils.platformwrapper import PlatformWrapper + +MODBUS_TEST_IP = "MODBUS_TEST_IP" + +# apply skipif to all tests +skip_msg = f"Env var {MODBUS_TEST_IP} not set. Please set the env var to the proper IP to run this integration test." +pytestmark = pytest.mark.skipif(os.environ.get(MODBUS_TEST_IP) is None, reason=skip_msg) + + +def test_get_point(publish_agent): + registers = ["SupplyTemp", "ReturnTemp", "OutsideTemp"] + for point_name in registers: + point_val = publish_agent.vip.rpc.call(PLATFORM_DRIVER, "get_point", "modbustk", + point_name).get(timeout=10) + print(f"Point: {point_name} has point value of {point_val}") + assert isinstance(point_val, int) + + +def test_set_point(publish_agent): + point_name = "SecondStageCoolingDemandSetPoint" + point_val = 42 + publish_agent.vip.rpc.call(PLATFORM_DRIVER, "set_point", "modbustk", point_name, + point_val).get(timeout=10) + assert publish_agent.vip.rpc.call(PLATFORM_DRIVER, "get_point", "modbustk", + point_name).get(timeout=10) == point_val + + +@pytest.fixture(scope="module") +def publish_agent(volttron_instance: PlatformWrapper): + assert volttron_instance.is_running() + vi = volttron_instance + assert vi is not None + assert vi.is_running() + + config = { + "driver_scrape_interval": 0.05, + "publish_breadth_first_all": "false", + "publish_depth_first": "false", + "publish_breadth_first": "false" + } + puid = vi.install_agent(agent_dir=Path(__file__).parent.parent.parent.parent.parent.absolute().resolve(), + config_file=config, + start=False, + vip_identity=PLATFORM_DRIVER) + assert puid is not None + gevent.sleep(1) + assert vi.start_agent(puid) + assert vi.is_agent_running(puid) + + # create the publish agent + publish_agent = volttron_instance.build_agent() + assert publish_agent.core.identity + gevent.sleep(1) + + capabilities = {"edit_config_store": {"identity": PLATFORM_DRIVER}} + volttron_instance.add_capabilities(publish_agent.core.publickey, capabilities) + gevent.sleep(1) + + # Add Modbus Driver TK registry map to Platform Driver + registry_config_string = """Register Name,Address,Type,Units,Writable + SupplyTemp,0,uint16,degC,FALSE + ReturnTemp,1,uint16,degC,FALSE + OutsideTemp,2,uint16,degC,FALSE + SecondStageCoolingDemandSetPoint,14,uint16,degC,TRUE""" + publish_agent.vip.rpc.call(CONFIGURATION_STORE, + "manage_store", + PLATFORM_DRIVER, + "m2000_rtu_TK_map.csv", + registry_config_string, + config_type="csv").get(timeout=10) + + # Add Modbus Driver registry to Platform Driver + registry_config_string = """Register Name,Volttron Point Name + SupplyTemp,SupplyTemp + ReturnTemp,ReturnTemp + OutsideTemp,OutsideTemp + SecondStageCoolingDemandSetPoint,SecondStageCoolingDemandSetPoint""" + publish_agent.vip.rpc.call(CONFIGURATION_STORE, + "manage_store", + PLATFORM_DRIVER, + "m2000_rtu_TK.csv", + registry_config_string, + config_type="csv").get(timeout=10) + + # Add Modbus Driver config to Platform Driver + device_address = os.environ.get(MODBUS_TEST_IP) + driver_config = { + "driver_config": { + "device_address": device_address, + "slave_id": 8, + "port": 502, + "register_map": "config://m2000_rtu_TK_map.csv" + }, + "campus": "PNNL", + "building": "DEMO", + "unit": "M2000", + "driver_type": "modbus_tk", + "registry_config": "config://m2000_rtu_TK.csv", + "interval": 60, + "timezone": "Pacific", + "heart_beat_point": "heartbeat" + } + + publish_agent.vip.rpc.call(CONFIGURATION_STORE, + "manage_store", + PLATFORM_DRIVER, + "devices/modbustk", + jsonapi.dumps(driver_config), + config_type='json').get(timeout=10) + + yield publish_agent + + volttron_instance.stop_agent(puid) + publish_agent.core.stop() diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_ion6200.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_ion6200.py index e4c91934f2..6215d688c0 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_ion6200.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_ion6200.py @@ -88,12 +88,12 @@ def ion_driver_agent(request, volttron_instance): # Clean out platform driver configurations # wait for it to return before adding new config md_agent.vip.rpc.call('config.store', - 'manage_delete_store', + 'delete_store', PLATFORM_DRIVER).get() # Add driver configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'devices/ion6200', ION6200_DRIVER_CONFIG, @@ -101,14 +101,14 @@ def ion_driver_agent(request, volttron_instance): # Add csv configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'ion6200.csv', ION6200_CSV_CONFIG, config_type='csv') md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'ion6200_map.csv', ION6200_CSV_MAP, diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_mixed_endian.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_mixed_endian.py index b4201a6189..e90d1df07f 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_mixed_endian.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_mixed_endian.py @@ -87,19 +87,19 @@ def agent(request, volttron_instance): # Clean out platform driver configurations # wait for it to return before adding new config md_agent.vip.rpc.call('config.store', - 'manage_delete_store', + 'delete_store', PLATFORM_DRIVER).get() # Add driver configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'devices/modbus', ORIGINAL_DRIVER_CONFIG, config_type='json') md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'devices/modbus_tk', NEW_DRIVER_CONFIG, @@ -107,21 +107,21 @@ def agent(request, volttron_instance): # Add csv configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'modbus.csv', ORIGINAL_REGISTRY_CONFIG, config_type='csv') md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'modbus_tk.csv', NEW_REGISTRY_CONFIG, config_type='csv') md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'modbus_tk_map.csv', NEW_REGISTER_MAP, diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_modbus_tk_driver.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_modbus_tk_driver.py index b422962f3f..c13d00079b 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_modbus_tk_driver.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_modbus_tk_driver.py @@ -111,19 +111,19 @@ def agent(request, volttron_instance): # Clean out platform driver configurations # wait for it to return before adding new config md_agent.vip.rpc.call('config.store', - 'manage_delete_store', + 'delete_store', PLATFORM_DRIVER).get() # Add driver configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'devices/modbus_tk', jsonapi.dumps(DRIVER_CONFIG), config_type='json') md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'devices/modbus', jsonapi.dumps(OLD_VOLTTRON_DRIVER_CONFIG), @@ -131,21 +131,21 @@ def agent(request, volttron_instance): # Add csv configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'modbus_tk.csv', REGISTRY_CONFIG_STRING, config_type='csv') md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'modbus_tk_map.csv', REGISTER_MAP, config_type='csv') md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'modbus.csv', OLD_VOLTTRON_REGISTRY_CONFIG, diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_scale_reg.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_scale_reg.py index 17d9f3fe92..c93d3a00e7 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_scale_reg.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_scale_reg.py @@ -58,12 +58,12 @@ def agent(request, volttron_instance): # Clean out platform driver configurations # wait for it to return before adding new config md_agent.vip.rpc.call('config.store', - 'manage_delete_store', + 'delete_store', PLATFORM_DRIVER).get() # Add driver configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'devices/modbus_tk', DRIVER_CONFIG_STRING, @@ -71,22 +71,22 @@ def agent(request, volttron_instance): # Add csv configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'modbus_tk.csv', REGISTRY_CONFIG_STRING, config_type='csv') md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'modbus_tk_map.csv', REGISTER_MAP, config_type='csv') platform_uuid = volttron_instance.install_agent(agent_dir=get_services_core("PlatformDriverAgent"), - config_file={}, - start=True) + config_file={}, + start=True) gevent.sleep(10) # wait for the agent to start and start the devices diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_scale_reg_pow_10.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_scale_reg_pow_10.py index bfa4e8b282..329f6b9732 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_scale_reg_pow_10.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_scale_reg_pow_10.py @@ -5,7 +5,7 @@ from volttron.platform import get_services_core from platform_driver.interfaces.modbus_tk.server import Server -from platform_driver.interfaces.modbus_tk.maps import Map, Catalog +from platform_driver.interfaces.modbus_tk.maps import Catalog from volttron.platform.agent.known_identities import PLATFORM_DRIVER logger = logging.getLogger(__name__) @@ -58,12 +58,12 @@ def agent(request, volttron_instance): # Clean out platform driver configurations # wait for it to return before adding new config md_agent.vip.rpc.call('config.store', - 'manage_delete_store', + 'delete_store', PLATFORM_DRIVER).get() # Add driver configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'devices/modbus_tk', DRIVER_CONFIG_STRING, @@ -71,14 +71,14 @@ def agent(request, volttron_instance): # Add csv configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'modbus_tk.csv', REGISTRY_CONFIG_STRING, config_type='csv') md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'modbus_tk_map.csv', REGISTER_MAP, diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_scrape_all.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_scrape_all.py index 9c8dcb318d..6ab19d5c17 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_scrape_all.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_scrape_all.py @@ -101,19 +101,19 @@ def agent(request, volttron_instance): # Clean out platform driver configurations # wait for it to return before adding new config md_agent.vip.rpc.call('config.store', - 'manage_delete_store', + 'delete_store', PLATFORM_DRIVER).get() # Add driver configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'devices/modbus_tk', jsonapi.dumps(DRIVER_CONFIG), config_type='json') md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'devices/modbus', jsonapi.dumps(OLD_VOLTTRON_DRIVER_CONFIG), @@ -121,21 +121,21 @@ def agent(request, volttron_instance): # Add csv configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'modbus_tk.csv', REGISTRY_CONFIG_STRING, config_type='csv') md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'modbus_tk_map.csv', REGISTER_MAP, config_type='csv') md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'modbus.csv', OLD_VOLTTRON_REGISTRY_CONFIG, diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_watts_on.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_watts_on.py index efb8f15515..45aba42e9a 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_watts_on.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_watts_on.py @@ -66,12 +66,12 @@ def agent(request, volttron_instance): # Clean out platform driver configurations # wait for it to return before adding new config md_agent.vip.rpc.call('config.store', - 'manage_delete_store', + 'delete_store', PLATFORM_DRIVER).get() # Add driver configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', 'platform.driver', 'devices/watts_on', DRIVER_CONFIG_STRING, @@ -79,14 +79,14 @@ def agent(request, volttron_instance): # Add csv configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', 'platform.driver', 'watts_on.csv', REGISTRY_CONFIG_STRING, config_type='csv') md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', 'platform.driver', 'watts_on_map.csv', REGISTRY_CONFIG_MAP, diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_write_single_registers.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_write_single_registers.py index b6027ee010..178dc2d4bf 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_write_single_registers.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/modbus_tk/tests/test_write_single_registers.py @@ -56,12 +56,12 @@ def agent(request, volttron_instance): # Clean out platform driver configurations # wait for it to return before adding new config md_agent.vip.rpc.call('config.store', - 'manage_delete_store', + 'delete_store', PLATFORM_DRIVER).get() # Add driver configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'devices/write_single_registers', DRIVER_CONFIG_STRING, @@ -69,14 +69,14 @@ def agent(request, volttron_instance): # Add csv configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'write_single_registers.csv', REGISTRY_CONFIG_STRING, config_type='csv') md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'write_single_registers_map.csv', REGISTRY_CONFIG_MAP, diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/radiothermostat.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/radiothermostat.py index 1c0791360c..a6a89dbbdc 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/radiothermostat.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/radiothermostat.py @@ -37,7 +37,7 @@ """ -from platform_driver.interfaces import BaseInterface, BaseRegister, DriverInterfaceError +from platform_driver.interfaces import BaseInterface, BaseRegister from . import thermostat_api from volttron.platform import jsonapi diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/universal.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/universal.py index 3ee2b70350..702b2183a6 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/universal.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/universal.py @@ -93,13 +93,13 @@ def __init__(self, **kwargs): args = parser.parse_args() self._verboseness = args.verbosity - if (self._verboseness == 0): + if self._verboseness == 0: verbiage = logging.ERROR - if (self._verboseness == 1): + if self._verboseness == 1: verbiage = logging.WARNING # '-v' - elif (self._verboseness == 2): + elif self._verboseness == 2: verbiage = logging.INFO # '-vv' - elif (self._verboseness >= 3): + elif self._verboseness >= 3: verbiage = logging.DEBUG # '-vvv' _log.setLevel(verbiage) @@ -112,7 +112,7 @@ def configure(self, config_dict, registry_config_str): which calls volttron/platform/store.py: def get_configs(self): self.vip.rpc.call(identity, "config.initial_update" sets list of registry_configs - scripts/install_platform_driver_configs.py calls 'manage_store' rpc, which is in volttron/platform/store.py + scripts/install_platform_driver_configs.py calls 'set_config' rpc, which is in volttron/platform/store.py which calls process_raw_config(), which stores it as a dict. process_raw_config() is also called by process_store() in store.py when the platform starts ( class ConfigStoreService): @@ -156,15 +156,15 @@ def configure(self, config_dict, registry_config_dict): # 4.0 passes in a reg D vip.heartbeat vip.config ''' - if (device_type == "heater"): + if device_type == "heater": self.agent = HeaterDriver(None, config_dict['device_id']) - elif (device_type == "meter"): + elif device_type == "meter": self.agent = MeterDriver(None, config_dict['device_id'], ) - elif (device_type == "thermostat"): + elif device_type == "thermostat": self.agent = ThermostatDriver(None, config_dict['device_id']) - elif (device_type == "blinds"): + elif device_type == "blinds": self.agent = BlindsDriver(None, config_dict['device_id']) - elif (device_type == "vehicle"): + elif device_type == "vehicle": self.agent = VehicleDriver(None, config_dict['device_id']) else: raise RuntimeError("Unsupported Device Type: '{}'".format(device_type)) @@ -201,7 +201,7 @@ def _set_point(self, point_name, value): if register.read_only: raise IOError("Trying to write to a point configured read only: " + point_name) - if (self.agent.SetPoint(register, value)): + if self.agent.SetPoint(register, value): register._value = register.reg_type(value) self.point_map[point_name]._value = register._value return register._value @@ -213,7 +213,7 @@ def _scrape_all(self): read_registers = self.get_registers_by_type("byte", True) write_registers = self.get_registers_by_type("byte", False) for register in read_registers + write_registers: - if (self._verboseness == 2): + if self._verboseness == 2: _log.info("Universal Scraping Value for '{}': {}".format(register.point_name, register._value)) result[register.point_name] = register._value return result @@ -226,7 +226,7 @@ def _reset_all(self): old_value = register._value register._value = register._default_value # _log.info( "point_map[register]._value = {}".format(self.point_map[register.point_name]._value)) - if (self._verboseness == 2): + if self._verboseness == 2: _log.info("Hardware not reachable, Resetting Value for '{}' from {} to {}".format(register.point_name, old_value, register._value)) diff --git a/services/core/PlatformDriverAgent/tests/test_bacnet.py b/services/core/PlatformDriverAgent/tests/test_bacnet.py index 2eef334f90..2058de7ad0 100644 --- a/services/core/PlatformDriverAgent/tests/test_bacnet.py +++ b/services/core/PlatformDriverAgent/tests/test_bacnet.py @@ -184,7 +184,7 @@ def config_store(config_store_connection): # registry config config_store_connection.call( - "manage_store", + "set_config", PLATFORM_DRIVER, registry_config, registry_string, @@ -201,7 +201,7 @@ def config_store(config_store_connection): } config_store_connection.call( - "manage_store", + "set_config", PLATFORM_DRIVER, BACNET_DEVICE_TOPIC, driver_config, @@ -210,5 +210,5 @@ def config_store(config_store_connection): yield config_store_connection print("Wiping out store.") - config_store_connection.call("manage_delete_store", PLATFORM_DRIVER) + config_store_connection.call("delete_store", PLATFORM_DRIVER) gevent.sleep(0.1) diff --git a/services/core/PlatformDriverAgent/tests/test_device_groups.py b/services/core/PlatformDriverAgent/tests/test_device_groups.py index 4f4a801610..ec9d73ba7e 100644 --- a/services/core/PlatformDriverAgent/tests/test_device_groups.py +++ b/services/core/PlatformDriverAgent/tests/test_device_groups.py @@ -37,7 +37,7 @@ # }}} """ -py.test cases for global platform driver settings. +pytest cases for global platform driver settings. """ import pytest @@ -136,12 +136,12 @@ def test_agent(volttron_instance): # Clean out platform driver configurations # wait for it to return before adding new config - md_agent.vip.rpc.call("config.store", "manage_delete_store", PLATFORM_DRIVER).get() + md_agent.vip.rpc.call("config.store", "delete_store", PLATFORM_DRIVER).get() # Add a fake.csv to the config store md_agent.vip.rpc.call( "config.store", - "manage_store", + "set_config", PLATFORM_DRIVER, "fake.csv", registry_config_string, @@ -166,7 +166,7 @@ def setup_config(test_agent, config_name, config_string, **kwargs): print("Adding", config_name, "to store") test_agent.vip.rpc.call( "config.store", - "manage_store", + "set_config", PLATFORM_DRIVER, config_name, config, @@ -177,7 +177,7 @@ def setup_config(test_agent, config_name, config_string, **kwargs): def remove_config(test_agent, config_name): print("Removing", config_name, "from store") test_agent.vip.rpc.call( - "config.store", "manage_delete_config", PLATFORM_DRIVER, config_name + "config.store", "delete_config", PLATFORM_DRIVER, config_name ).get() diff --git a/services/core/PlatformDriverAgent/tests/test_device_groups_p2.py b/services/core/PlatformDriverAgent/tests/test_device_groups_p2.py index 652b26f3f8..51ee93dec4 100644 --- a/services/core/PlatformDriverAgent/tests/test_device_groups_p2.py +++ b/services/core/PlatformDriverAgent/tests/test_device_groups_p2.py @@ -37,7 +37,7 @@ # }}} """ -a single py.test case for global platform driver settings. +a single pytest case for global platform driver settings. """ import pytest @@ -195,12 +195,12 @@ def test_agent(volttron_instance): # Clean out platform driver configurations # wait for it to return before adding new config - md_agent.vip.rpc.call("config.store", "manage_delete_store", PLATFORM_DRIVER).get() + md_agent.vip.rpc.call("config.store", "delete_store", PLATFORM_DRIVER).get() # Add a fake.csv to the config store md_agent.vip.rpc.call( "config.store", - "manage_store", + "set_config", PLATFORM_DRIVER, "fake.csv", registry_config_string, @@ -225,7 +225,7 @@ def setup_config(test_agent, config_name, config_string, **kwargs): print("Adding", config_name, "to store") test_agent.vip.rpc.call( "config.store", - "manage_store", + "set_config", PLATFORM_DRIVER, config_name, config, @@ -236,5 +236,5 @@ def setup_config(test_agent, config_name, config_string, **kwargs): def remove_config(test_agent, config_name): print("Removing", config_name, "from store") test_agent.vip.rpc.call( - "config.store", "manage_delete_config", PLATFORM_DRIVER, config_name + "config.store", "delete_config", PLATFORM_DRIVER, config_name ).get() diff --git a/services/core/PlatformDriverAgent/tests/test_eagle.py b/services/core/PlatformDriverAgent/tests/test_eagle.py index 8856236739..59ac050895 100644 --- a/services/core/PlatformDriverAgent/tests/test_eagle.py +++ b/services/core/PlatformDriverAgent/tests/test_eagle.py @@ -202,19 +202,19 @@ def agent(volttron_instance): volttron_instance.add_capabilities(agent.core.publickey, capabilities) # Clean out platform driver configurations. agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_delete_store', + 'delete_store', PLATFORM_DRIVER).get(timeout=10) # Add test configurations. agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_store', + 'set_config', PLATFORM_DRIVER, "devices/campus/building/unit", driver_config_string, "json").get(timeout=10) agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_store', + 'set_config', PLATFORM_DRIVER, "eagle.json", register_config_string, diff --git a/services/core/PlatformDriverAgent/tests/test_ecobee_driver.py b/services/core/PlatformDriverAgent/tests/test_ecobee_driver.py index 984e7fe264..827ec0c491 100644 --- a/services/core/PlatformDriverAgent/tests/test_ecobee_driver.py +++ b/services/core/PlatformDriverAgent/tests/test_ecobee_driver.py @@ -578,14 +578,14 @@ def test_scrape_all_trigger_refresh(mock_ecobee): # } # ecobee_driver_config = jsonapi.load(get_examples("configurations/drivers/ecobee.config")) # ecobee_driver_config["interval"] = 3 -# query_agent.vip.rpc.call(CONFIGURATION_STORE, "manage_store", PLATFORM_DRIVER, +# query_agent.vip.rpc.call(CONFIGURATION_STORE, "set_config", PLATFORM_DRIVER, # "devices/campus/building/test_ecobee", driver_config) # # with open("configurations/drivers/ecobee.csv") as registry_file: # registry_string = registry_file.read() # registry_path = re.search("(?!config:\/\/)[a-zA-z]+\.csv", ecobee_driver_config.get("registry_config")) # -# query_agent.vip.rpc.call(CONFIGURATION_STORE, "manage_store", PLATFORM_DRIVER, registry_path, registry_string, +# query_agent.vip.rpc.call(CONFIGURATION_STORE, "set_config", PLATFORM_DRIVER, registry_path, registry_string, # config_type="csv") # # ecobee_driver_config.update(driver_config) diff --git a/services/core/PlatformDriverAgent/tests/test_global_override.py b/services/core/PlatformDriverAgent/tests/test_global_override.py index 4c0f3a17c7..6f5db74948 100644 --- a/services/core/PlatformDriverAgent/tests/test_global_override.py +++ b/services/core/PlatformDriverAgent/tests/test_global_override.py @@ -37,7 +37,7 @@ # }}} """ -py.test cases for global override settings. +pytest cases for global override settings. """ import pytest @@ -88,12 +88,12 @@ def test_agent(volttron_instance): # Clean out platform driver configurations # wait for it to return before adding new config - md_agent.vip.rpc.call("config.store", "manage_delete_store", PLATFORM_DRIVER).get() + md_agent.vip.rpc.call("config.store", "delete_store", PLATFORM_DRIVER).get() # Add configuration for platform driver md_agent.vip.rpc.call( "config.store", - "manage_store", + "set_config", PLATFORM_DRIVER, "config", jsonapi.dumps(PLATFORM_DRIVER_CONFIG), @@ -108,7 +108,7 @@ def test_agent(volttron_instance): registry_config_string = f.read() md_agent.vip.rpc.call( "config.store", - "manage_store", + "set_config", PLATFORM_DRIVER, "fake.csv", registry_config_string, @@ -120,7 +120,7 @@ def test_agent(volttron_instance): config_name = f"devices/fakedriver{i}" md_agent.vip.rpc.call( "config.store", - "manage_store", + "set_config", PLATFORM_DRIVER, config_name, jsonapi.dumps(FAKE_DEVICE_CONFIG), @@ -790,7 +790,7 @@ def test_override_pattern(test_agent): config_name = config_path.format(camel_device_path) test_agent.vip.rpc.call( "config.store", - "manage_store", + "set_config", PLATFORM_DRIVER, config_name, jsonapi.dumps(FAKE_DEVICE_CONFIG), diff --git a/services/core/PlatformDriverAgent/tests/test_global_settings.py b/services/core/PlatformDriverAgent/tests/test_global_settings.py index 67a245019e..f1b83b24e1 100644 --- a/services/core/PlatformDriverAgent/tests/test_global_settings.py +++ b/services/core/PlatformDriverAgent/tests/test_global_settings.py @@ -37,7 +37,7 @@ # }}} """ -py.test cases for global platform driver settings. +pytest cases for global platform driver settings. """ import pytest @@ -167,12 +167,12 @@ def test_agent(volttron_instance): # Clean out platform driver configurations # wait for it to return before adding new config - md_agent.vip.rpc.call("config.store", "manage_delete_store", PLATFORM_DRIVER).get() + md_agent.vip.rpc.call("config.store", "delete_store", PLATFORM_DRIVER).get() # Add a fake.csv to the config store md_agent.vip.rpc.call( "config.store", - "manage_store", + "set_config", PLATFORM_DRIVER, "fake.csv", registry_config_string, @@ -197,7 +197,7 @@ def setup_config(test_agent, config_name, config_string, **kwargs): print("Adding", config_name, "to store") test_agent.vip.rpc.call( "config.store", - "manage_store", + "set_config", PLATFORM_DRIVER, config_name, config, diff --git a/services/core/PlatformDriverAgent/tests/test_modbus_driver.py b/services/core/PlatformDriverAgent/tests/test_modbus_driver.py index b70491ab6b..41320a1f0a 100644 --- a/services/core/PlatformDriverAgent/tests/test_modbus_driver.py +++ b/services/core/PlatformDriverAgent/tests/test_modbus_driver.py @@ -80,12 +80,12 @@ def agent(request, volttron_instance): # Clean out platform driver configurations # wait for it to return before adding new config md_agent.vip.rpc.call('config.store', - 'manage_delete_store', + 'delete_store', PLATFORM_DRIVER).get() # Add driver configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'devices/modbus', jsonapi.dumps(DRIVER_CONFIG), @@ -93,7 +93,7 @@ def agent(request, volttron_instance): # Add csv configurations md_agent.vip.rpc.call('config.store', - 'manage_store', + 'set_config', PLATFORM_DRIVER, 'modbus.csv', REGISTRY_CONFIG_STRING, diff --git a/services/core/PlatformDriverAgent/tests/test_rest_driver.py b/services/core/PlatformDriverAgent/tests/test_rest_driver.py index f93c4a3cb8..6a50ce3a28 100644 --- a/services/core/PlatformDriverAgent/tests/test_rest_driver.py +++ b/services/core/PlatformDriverAgent/tests/test_rest_driver.py @@ -82,19 +82,19 @@ def agent(request, volttron_instance): capabilities = {'edit_config_store': {'identity': PLATFORM_DRIVER}} volttron_instance.add_capabilities(agent.core.publickey, capabilities) agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_delete_store', + 'delete_store', PLATFORM_DRIVER).get(timeout=10) # Add test configurations. agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_store', + 'set_config', PLATFORM_DRIVER, "devices/campus/building/unit", driver_config_dict_string, "json").get(timeout=10) agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_store', + 'set_config', PLATFORM_DRIVER, "restful.csv", restful_csv_string, diff --git a/services/core/SQLHistorian/tests/test_sqlitehistorian.py b/services/core/SQLHistorian/tests/test_sqlitehistorian.py index a9b8333396..a5885d6007 100644 --- a/services/core/SQLHistorian/tests/test_sqlitehistorian.py +++ b/services/core/SQLHistorian/tests/test_sqlitehistorian.py @@ -43,7 +43,7 @@ import gevent import pytest from pytest import approx -from datetime import datetime, timedelta +from datetime import datetime from volttron.platform import get_services_core from volttron.platform.agent import utils @@ -237,7 +237,7 @@ def test_sqlite_timeout(request, publish_agent, volttron_instance, config): assert (len(result['values']) == 1) (now_date, now_time) = now.split("T") assert result['values'][0][0] == now_date + 'T' + now_time + '+00:00' - assert (result['values'][0][1] == approx(oat_reading)) + assert result['values'][0][1] == approx(oat_reading) assert set(result['metadata'].items()) == set(float_meta.items()) except Exception as e: print(e) diff --git a/services/core/VolttronCentral/tests/test_vc_autoregister.py b/services/core/VolttronCentral/tests/test_vc_autoregister.py index d50e752ba7..cf0bb838ca 100644 --- a/services/core/VolttronCentral/tests/test_vc_autoregister.py +++ b/services/core/VolttronCentral/tests/test_vc_autoregister.py @@ -32,12 +32,12 @@ def multi_messagebus_vc_vcp(volttron_multi_messagebus): # capabilities = {'edit_config_store': {'identity': VOLTTRON_CENTRAL_PLATFORM}} # vcp_instance.add_capabilities(vcp_instance.dynamic_agent.core.publickey, capabilities) vcp_instance.dynamic_agent.vip.rpc.call(CONFIGURATION_STORE, - "manage_store", + "set_config", VOLTTRON_CENTRAL_PLATFORM, "config", config, "json").get() - # "manage_store", opts.identity, opts.name, file_contents, config_type = opts.config_type + # "set_config", opts.identity, opts.name, file_contents, config_type = opts.config_type # Allows connections between platforms to be established. gevent.sleep(20) yield vcp_instance, vc_instance, vcp_uuid @@ -52,10 +52,10 @@ def test_able_to_register_unregister(multi_messagebus_vc_vcp): vcp_instance, vc_instance, vcp_uuid = multi_messagebus_vc_vcp if vcp_instance.param['sink'] == 'rmq_web' and vcp_instance.param['source'] != 'rmq': pytest.mark.xfail("Combination of rmq<-zmq is not valid") - pytest.fail("Combination of rmq<-zmq is not valid") + pytest.skip("Combination of rmq<-zmq is not valid") elif vcp_instance.param['sink'] == 'zmq_web' and vcp_instance.param['source'] != 'zmq': pytest.mark.xfail("Combination of zmq<-rmq does not work") - pytest.fail("Combination of rmq<-zmq is not valid") + pytest.skip("Combination of zmq<-rmq is not valid") apitester = APITester(vc_instance) diff --git a/services/core/VolttronCentral/tests/vctestutils.py b/services/core/VolttronCentral/tests/vctestutils.py index 7ead874c00..bdff3f9e7d 100644 --- a/services/core/VolttronCentral/tests/vctestutils.py +++ b/services/core/VolttronCentral/tests/vctestutils.py @@ -1,7 +1,5 @@ import requests -from volttron.platform import jsonapi - class APITester: def __init__(self, wrapper, username='admin', password='admin'): diff --git a/services/core/VolttronCentral/volttroncentral/platforms.py b/services/core/VolttronCentral/volttroncentral/platforms.py index 2e4a78d6cb..c3699b35f7 100644 --- a/services/core/VolttronCentral/volttroncentral/platforms.py +++ b/services/core/VolttronCentral/volttroncentral/platforms.py @@ -42,17 +42,14 @@ from collections import defaultdict import gevent -from copy import deepcopy from volttron.platform import jsonrpc from volttron.platform.agent.known_identities import VOLTTRON_CENTRAL_PLATFORM from volttron.platform.agent.utils import format_timestamp, get_aware_utc_now, \ get_utc_seconds_from_epoch -from volttron.platform.jsonrpc import INVALID_PARAMS, UNAVAILABLE_PLATFORM, \ - INTERNAL_ERROR, RemoteError +from volttron.platform.jsonrpc import INVALID_PARAMS, INTERNAL_ERROR, RemoteError from volttron.platform.messaging.health import Status, UNKNOWN_STATUS, \ GOOD_STATUS, BAD_STATUS -from volttron.platform.vip.agent import Unreachable from volttron.platform.vip.agent.utils import build_connection from volttron.platform import jsonapi diff --git a/services/core/VolttronCentralPlatform/tests/test_platform_agent_rpc.py b/services/core/VolttronCentralPlatform/tests/test_platform_agent_rpc.py index f52594a398..1ec429ff20 100644 --- a/services/core/VolttronCentralPlatform/tests/test_platform_agent_rpc.py +++ b/services/core/VolttronCentralPlatform/tests/test_platform_agent_rpc.py @@ -226,7 +226,7 @@ def test_can_change_topic_map(setup_platform, vc_agent): # now update the config store for vcp vc.vip.rpc.call(CONFIGURATION_STORE, - 'manage_store', + 'set_config', VOLTTRON_CENTRAL_PLATFORM, 'config', jsonapi.dumps(replace_map), @@ -247,7 +247,7 @@ def test_can_change_topic_map(setup_platform, vc_agent): # now update the config store for vcp vc.vip.rpc.call(CONFIGURATION_STORE, - 'manage_store', + 'set_config', VOLTTRON_CENTRAL_PLATFORM, 'config', jsonapi.dumps(replace_map), diff --git a/services/core/VolttronCentralPlatform/tests/test_platformagent.py b/services/core/VolttronCentralPlatform/tests/test_platformagent.py index 92a32a196b..e7ea10eafb 100644 --- a/services/core/VolttronCentralPlatform/tests/test_platformagent.py +++ b/services/core/VolttronCentralPlatform/tests/test_platformagent.py @@ -3,7 +3,6 @@ import tempfile import uuid -import gevent import pytest import requests from volttron.platform.agent.known_identities import ( @@ -13,7 +12,6 @@ from volttron.platform.keystore import KeyStore from volttron.platform.messaging.health import STATUS_GOOD from volttron.platform.vip.agent import Agent -from volttron.platform.vip.agent.connection import Connection from volttron.platform.web import DiscoveryInfo from volttrontesting.utils.agent_additions import \ add_volttron_central_platform diff --git a/services/core/VolttronCentralPlatform/vcplatform/agent.py b/services/core/VolttronCentralPlatform/vcplatform/agent.py index 546705913e..c8189cbeba 100644 --- a/services/core/VolttronCentralPlatform/vcplatform/agent.py +++ b/services/core/VolttronCentralPlatform/vcplatform/agent.py @@ -39,7 +39,6 @@ import base64 import datetime -import hashlib import logging import os import re @@ -69,7 +68,6 @@ from volttron.platform.agent.utils import (get_aware_utc_now) from volttron.platform.agent.utils import (get_utc_seconds_from_epoch, format_timestamp, normalize_identity) -from volttron.platform.auth.auth_entry import AuthEntry from volttron.platform.auth.auth_file import AuthFile from volttron.platform.jsonrpc import (INTERNAL_ERROR, INVALID_PARAMS) from volttron.platform.messaging import topics @@ -814,21 +812,21 @@ def list_agents(self): def store_agent_config(self, agent_identity, config_name, raw_contents, config_type='raw'): _log.debug("Storeing configuration file: {}".format(config_name)) - self.vip.rpc.call(CONFIGURATION_STORE, "manage_store", agent_identity, + self.vip.rpc.call(CONFIGURATION_STORE, "set_config", agent_identity, config_name, raw_contents, config_type) def list_agent_configs(self, agent_identity): - return self.vip.rpc.call(CONFIGURATION_STORE, "manage_list_configs", + return self.vip.rpc.call(CONFIGURATION_STORE, "list_configs", agent_identity).get(timeout=5) def get_agent_config(self, agent_identity, config_name, raw=True): - data = self.vip.rpc.call(CONFIGURATION_STORE, "manage_get", + data = self.vip.rpc.call(CONFIGURATION_STORE, "get_config", agent_identity, config_name, raw).get( timeout=5) return data or "" def delete_agent_config(self, agent_identity, config_name): - data = self.vip.rpc.call(CONFIGURATION_STORE, "manage_delete_config", + data = self.vip.rpc.call(CONFIGURATION_STORE, "delete_config", agent_identity, config_name).get( timeout=5) return data or "" @@ -979,7 +977,7 @@ def get_devices(self): devices = defaultdict(dict) for platform_driver_id in self._platform_driver_ids: config_list = self.vip.rpc.call(CONFIGURATION_STORE, - 'manage_list_configs', + 'list_configs', platform_driver_id).get(timeout=5) _log.debug('Config list is: {}'.format(config_list)) @@ -989,7 +987,7 @@ def get_devices(self): continue device_config = self.vip.rpc.call(CONFIGURATION_STORE, - 'manage_get', + 'get_config', platform_driver_id, cfg_name, raw=False).get(timeout=5) @@ -1001,7 +999,7 @@ def get_devices(self): reg_cfg_name )) registry_config = self.vip.rpc.call(CONFIGURATION_STORE, - 'manage_get', + 'get_config', platform_driver_id, reg_cfg_name, raw=False).get(timeout=5) diff --git a/services/core/WeatherDotGov/weatherdotgov/agent.py b/services/core/WeatherDotGov/weatherdotgov/agent.py index d309b88bbd..5c26e17014 100644 --- a/services/core/WeatherDotGov/weatherdotgov/agent.py +++ b/services/core/WeatherDotGov/weatherdotgov/agent.py @@ -43,7 +43,7 @@ import sys import grequests import datetime -import pkg_resources +from importlib.resources import files as get_resource_files from volttron.platform.agent.base_weather import BaseWeatherAgent from volttron.platform.agent import utils from volttron.utils.docs import doc_inherit @@ -52,7 +52,6 @@ # requests should be imported after grequests # as grequests monkey patches ssl and requests imports ssl -# TODO do we need the requests at all.. TODO test with RMQ import requests __version__ = "2.0.0" @@ -121,7 +120,7 @@ def get_point_name_defs_file(self): """ # returning resource file instead of stream, as csv.DictReader require file path or file like object opened in # text mode. - return pkg_resources.get_resource_filename(__name__, "data/name_mapping.csv") + return str(get_resource_files("weatherdotgov").joinpath("data/name_mapping.csv")) def get_location_string(self, location): """ @@ -382,13 +381,11 @@ def get_forecast_url(self, location): return url def make_web_request(self, url): - grequest = [grequests.get(url, verify=requests.certs.where(), - headers=self.headers, timeout=3)] - gresponse = grequests.map(grequest)[0] - if gresponse is None: + response = requests.get(url, headers=self.headers, verify=requests.certs.where()) + if response is None: raise RuntimeError("get request did not return any " "response") - return gresponse + return response def extract_forecast_data(self, url, gresponse): try: diff --git a/services/ops/EmailerAgent/emailer/agent.py b/services/ops/EmailerAgent/emailer/agent.py index 6dab6ef69c..bcf247a2b1 100644 --- a/services/ops/EmailerAgent/emailer/agent.py +++ b/services/ops/EmailerAgent/emailer/agent.py @@ -37,9 +37,6 @@ # }}} - -from collections import defaultdict - # Import the email modules we'll need from email.mime.text import MIMEText import logging @@ -51,7 +48,7 @@ import gevent from volttron.platform.agent.utils import get_utc_seconds_from_epoch -from volttron.platform.vip.agent import Agent, Core, PubSub, compat +from volttron.platform.vip.agent import Agent, PubSub, compat from volttron.platform.agent import utils from volttron.platform.messaging import topics from volttron.platform.messaging.health import ALERT_KEY, STATUS_BAD, Status, \ diff --git a/services/ops/LogStatisticsAgent/Tests/test_log_statistics.py b/services/ops/LogStatisticsAgent/Tests/test_log_statistics.py index 284cbd58c2..ab29db4883 100644 --- a/services/ops/LogStatisticsAgent/Tests/test_log_statistics.py +++ b/services/ops/LogStatisticsAgent/Tests/test_log_statistics.py @@ -43,7 +43,7 @@ from volttron.platform.messaging.health import STATUS_GOOD from volttron.platform.vip.agent import Agent -from volttron.platform import get_ops, get_home +from volttron.platform import get_ops test_config = { "analysis_interval_sec": 2, diff --git a/services/ops/LogStatisticsAgent/logstatisticsagent/agent.py b/services/ops/LogStatisticsAgent/logstatisticsagent/agent.py index 71e45913a1..05703adc5d 100644 --- a/services/ops/LogStatisticsAgent/logstatisticsagent/agent.py +++ b/services/ops/LogStatisticsAgent/logstatisticsagent/agent.py @@ -42,7 +42,7 @@ import sys import statistics -from volttron.platform.vip.agent import Agent, RPC, Core +from volttron.platform.vip.agent import Agent, Core from volttron.platform.agent import utils from volttron.platform.agent.utils import get_aware_utc_now diff --git a/services/ops/ThresholdDetectionAgent/tests/test_threshold_agent.py b/services/ops/ThresholdDetectionAgent/tests/test_threshold_agent.py index eb2816706e..4d71170200 100644 --- a/services/ops/ThresholdDetectionAgent/tests/test_threshold_agent.py +++ b/services/ops/ThresholdDetectionAgent/tests/test_threshold_agent.py @@ -42,7 +42,6 @@ import logging import sys -import uuid import unittest import mock from mock import Mock diff --git a/services/ops/ThresholdDetectionAgent/tests/test_threshold_detection.py b/services/ops/ThresholdDetectionAgent/tests/test_threshold_detection.py index ec99ac41df..4af809c103 100644 --- a/services/ops/ThresholdDetectionAgent/tests/test_threshold_detection.py +++ b/services/ops/ThresholdDetectionAgent/tests/test_threshold_detection.py @@ -109,7 +109,7 @@ def clear_keys(self): def reset_store(self): self.vip.rpc.call(CONFIGURATION_STORE, - 'manage_store', + 'set_config', 'platform.thresholddetection', 'config', jsonapi.dumps(_test_config), @@ -137,7 +137,7 @@ def threshold_tester_agent(volttron_instance): agent.reset_store() # agent.vip.rpc.call(CONFIGURATION_STORE, - # 'manage_store', + # 'set_config', # 'platform.thresholddetection', # 'config', # jsonapi.dumps(_test_config), @@ -146,7 +146,7 @@ def threshold_tester_agent(volttron_instance): yield agent agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_delete_store', + 'delete_store', 'platform.thresholddetection').get() volttron_instance.remove_agent(threshold_detection_uuid) @@ -211,7 +211,7 @@ def test_update_config(threshold_tester_agent): # threshold_tester_agent.vip.pubsub.publish('pubsub', topic="alerts/woot", headers={"foo": "bar"}) # threshold_tester_agent.vip .config.set('config', updated_config, True) threshold_tester_agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_store', + 'set_config', 'platform.thresholddetection', 'config', jsonapi.dumps(updated_config), @@ -254,7 +254,7 @@ def test_device_publish(threshold_tester_agent): def test_remove_from_config_store(threshold_tester_agent): threshold_tester_agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_delete_config', + 'delete_config', 'platform.thresholddetection', 'config').get() publish(threshold_tester_agent, _test_config, lambda x: x+1) diff --git a/services/ops/TopicWatcher/tests/test_topic_watcher.py b/services/ops/TopicWatcher/tests/test_topic_watcher.py index 51dd500fc5..5d68f7e4b2 100644 --- a/services/ops/TopicWatcher/tests/test_topic_watcher.py +++ b/services/ops/TopicWatcher/tests/test_topic_watcher.py @@ -135,7 +135,7 @@ def test_basic(agent): """ global alert_messages, db_connection publish_time = get_aware_utc_now() - print (f"publish time is {publish_time}") + print(f"publish time is {publish_time}") for _ in range(5): alert_messages.clear() agent.vip.pubsub.publish(peer='pubsub', diff --git a/services/unsupported/OpenADRVenAgent/openadrven/agent.py b/services/unsupported/OpenADRVenAgent/openadrven/agent.py new file mode 100644 index 0000000000..d925902268 --- /dev/null +++ b/services/unsupported/OpenADRVenAgent/openadrven/agent.py @@ -0,0 +1,1766 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright 2020, Battelle Memorial Institute. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This material was prepared as an account of work sponsored by an agency of +# the United States Government. Neither the United States Government nor the +# United States Department of Energy, nor Battelle, nor any of their +# employees, nor any jurisdiction or organization that has cooperated in the +# development of these materials, makes any warranty, express or +# implied, or assumes any legal liability or responsibility for the accuracy, +# completeness, or usefulness or any information, apparatus, product, +# software, or process disclosed, or represents that its use would not infringe +# privately owned rights. Reference herein to any specific commercial product, +# process, or service by trade name, trademark, manufacturer, or otherwise +# does not necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors expressed +# herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY operated by +# BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 +# }}} + + + +from collections import namedtuple +from datetime import datetime as dt +from datetime import timedelta +from dateutil import parser +import gevent +# OpenADR rule 1: use ISO8601 timestamp +import logging +import lxml.etree as etree_ +import os +import random +import requests +from requests.exceptions import ConnectionError +import signxml +import io +import sys + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from volttron.platform.agent import utils +# OpenADR rule 1: use ISO8601 timestamp +from volttron.platform.agent.utils import format_timestamp +from volttron.platform.messaging import topics, headers +from volttron.platform.vip.agent import Agent, Core, RPC +from volttron.platform.scheduling import periodic +from volttron.platform import jsonapi + +from .oadr_builder import * +from .oadr_extractor import * +from .oadr_20b import parseString, oadrSignedObject +from .oadr_common import * +from .models import ORMBase +from .models import EiEvent, EiReport, EiTelemetryValues + +utils.setup_logging() +_log = logging.getLogger(__name__) + +__version__ = '1.0' + +ENDPOINT_BASE = '/OpenADR2/Simple/2.0b/' +EIEVENT = ENDPOINT_BASE + 'EiEvent' +EIREPORT = ENDPOINT_BASE + 'EiReport' +EIREGISTERPARTY = ENDPOINT_BASE + 'EiRegisterParty' +POLL = ENDPOINT_BASE + 'OadrPoll' + +Endpoint = namedtuple('Endpoint', ['url', 'callback']) +OPENADR_ENDPOINTS = { + 'EiEvent': Endpoint(url=EIEVENT, callback='push_request'), + 'EiReport': Endpoint(url=EIREPORT, callback='push_request'), + 'EiRegisterParty': Endpoint(url=EIREGISTERPARTY, callback='push_request')} + +VTN_REQUESTS = { + 'oadrDistributeEvent': 'handle_oadr_distribute_event', + 'oadrRegisterReport': 'handle_oadr_register_report', + 'oadrRegisteredReport': 'handle_oadr_registered_report', + 'oadrCreateReport': 'handle_oadr_create_report', + 'oadrUpdatedReport': 'handle_oadr_updated_report', + 'oadrCancelReport': 'handle_oadr_cancel_report', + 'oadrResponse': 'handle_oadr_response', + 'oadrCreatedPartyRegistration': 'handle_oadr_created_party_registration'} + +PROCESS_LOOP_FREQUENCY_SECS = 5 +DEFAULT_REPORT_INTERVAL_SECS = 15 +DEFAULT_OPT_IN_TIMEOUT_SECS = 30 * 60 # If no optIn timeout was configured, use 30 minutes. + +# These parameters control behavior that is sometimes temporarily disabled during software development. +USE_REPORTS = True +SEND_POLL = True + +# Paths to sample X509 certificates, generated by Kyrio. These are needed when security_level = 'high'. +CERTS_DIRECTORY = '$VOLTTRON_ROOT/services/core/OpenADRVenAgent/certs/' +CERT_FILENAME = CERTS_DIRECTORY + 'TEST_RSA_VEN_171024145702_cert.pem' +KEY_FILENAME = CERTS_DIRECTORY + 'TEST_RSA_VEN_171024145702_privkey.pem' +VTN_CA_CERT_FILENAME = CERTS_DIRECTORY + 'TEST_OpenADR_RSA_BOTH0002_Cert.pem' + + +def ven_agent(config_path, **kwargs): + """ + Parse the OpenADRVenAgent configuration file and return an instance of + the agent that has been created using that configuration. + + See initialize_config() method documentation for a description of each configurable parameter. + + :param config_path: (str) Path to a configuration file. + :returns: OpenADRVenAgent instance + """ + try: + config = utils.load_config(config_path) + except Exception as err: + _log.error("Error loading configuration: {}".format(err)) + config = {} + db_path = config.get('db_path') + ven_id = config.get('ven_id') + ven_name = config.get('ven_name') + vtn_id = config.get('vtn_id') + vtn_address = config.get('vtn_address') + send_registration = config.get('send_registration') + security_level = config.get('security_level') + poll_interval_secs = config.get('poll_interval_secs') + log_xml = config.get('log_xml') + opt_in_timeout_secs = config.get('opt_in_timeout_secs') + opt_in_default_decision = config.get('opt_in_default_decision') + request_events_on_startup = config.get('request_events_on_startup') + report_parameters = config.get('report_parameters') + return OpenADRVenAgent(db_path, ven_id, ven_name, vtn_id, vtn_address, send_registration, security_level, + poll_interval_secs, log_xml, opt_in_timeout_secs, opt_in_default_decision, + request_events_on_startup, report_parameters, **kwargs) + + +class OpenADRVenAgent(Agent): + """ + OpenADR (Automated Demand Response) is a standard for alerting and responding + to the need to adjust electric power consumption in response to fluctuations + in grid demand. + + For further information about OpenADR and this agent, please see + the OpenADR documentation in VOLTTRON ReadTheDocs. + + OpenADR communications are conducted between Virtual Top Nodes (VTNs) and Virtual End Nodes (VENs). + In this implementation, a VOLTTRON agent is a VEN, implementing EiEvent and EiReport services + in conformance with a subset of the OpenADR 2.0b specification. + + The VEN receives VTN requests via the VOLTTRON web service. + + The VTN can 'call an event', indicating that a load-shed event should occur. + The VEN responds with an 'optIn' acknowledgment. + + In conjunction with an event (or independent of events), the VEN reports device status + and usage telemetry, relying on data received periodically from other VOLTTRON agents. + + Events: + The VEN agent maintains a persistent record of DR events. + Event updates (including creation) trigger publication of event JSON on the VOLTTRON message bus. + Other VOLTTRON agents can also call a get_events() RPC to retrieve the current status + of particular events, or of all active events. + + Reporting: + The VEN agent configuration defines telemetry values (data points) to be reported to the VTN. + The VEN agent maintains a persistent record of reportable/reported telemetry values over time. + Other VOLTTRON agents are expected to call a report_telemetry() RPC to supply the VEN agent + with a regular stream of telemetry values for reporting. + Other VOLTTRON agents can receive notification of changes in telemetry reporting requirements + by subscribing to publication of telemetry parameters. + + Pub/Sub (see method documentation): + publish_event() + publish_telemetry_parameters_for_report() + + RPC calls (see method documentation): + respond_to_event(event_id, opt_in=True): + get_events(in_progress_only=True, started_after=None, end_time_before=None) + get_telemetry_parameters() + set_telemetry_status(online, manual_override) + report_telemetry(telemetry_values) + + Supported requests/responses in the OpenADR VTN interface: + VTN: + oadrDistributeEvent (needed for event cancellation) + oadrResponse + oadrRegisteredReport + oadrCreateReport + oadrUpdatedReport + oadrCancelReport + oadrCreatedPartyRegistration + VEN: + oadrPoll + oadrRequestEvent + oadrCreatedEvent + oadrResponse + oadrRegisterReport + oadrCreatedReport + oadrUpdateReport + oadrCanceledReport + oadrCreatePartyRegistration + oadrQueryRegistration + """ + + _db_session = None + _last_poll = None + _active_events = {} + _active_reports = {} + + def __init__(self, db_path, ven_id, ven_name, vtn_id, vtn_address, send_registration, security_level, + poll_interval_secs, log_xml, opt_in_timeout_secs, opt_in_default_decision, + request_events_on_startup, report_parameters, + **kwargs): + super(OpenADRVenAgent, self).__init__(enable_web=True, **kwargs) + + self.db_path = None + self.ven_id = None + self.ven_name = None + self.vtn_id = None + self.vtn_address = None + self.send_registration = False + self.security_level = None + self.poll_interval_secs = None + self.log_xml = True + self.opt_in_timeout_secs = None + self.opt_in_default_decision = 'optIn' + self.request_events_on_startup = None + self.report_parameters = {} + self.default_config = {"db_path": db_path, + "ven_id": ven_id, + "ven_name": ven_name, + "vtn_id": vtn_id, + "vtn_address": vtn_address, + "send_registration": send_registration, + "security_level": security_level, + "poll_interval_secs": poll_interval_secs, + "log_xml": log_xml, + "opt_in_timeout_secs": opt_in_timeout_secs, + "opt_in_default_decision": opt_in_default_decision, + "request_events_on_startup": request_events_on_startup, + "report_parameters": report_parameters} + self.vip.config.set_default("config", self.default_config) + self.vip.config.subscribe(self._configure, actions=["NEW", "UPDATE"], pattern="config") + self.initialize_config(self.default_config) + # State variables for VTN request/response processing + self.oadr_current_service = None + self.oadr_current_request_id = None + # The following parameters can be adjusted by issuing a set_telemetry_status() RPC call. + self.ven_online = 'false' + self.ven_manual_override = 'false' + + def _configure(self, config_name, action, contents): + """The agent's config may have changed. Re-initialize it.""" + config = self.default_config.copy() + config.update(contents) + self.initialize_config(config) + + def initialize_config(self, config): + """ + Initialize the agent's configuration. + + Configuration parameters (see config for a sample config file): + + db_path: Pathname of the agent's sqlite database. + ~ and shell variables will be expanded if present. + ven_id: (string) OpenADR ID of this virtual end node. Identifies this VEN to the VTN. + ven_name: Name of this virtual end node. Identifies this VEN during registration, + before its ID is known. + vtn_id: (string) OpenADR ID of the VTN with which this VEN communicates. + vtn_address: URL and port number of the VTN. + send_registration: ('True' or 'False') If 'True', send a one-time registration request to the VTN, + obtaining the VEN ID. The agent should be run in this mode initially, + then shut down and run with this parameter set to 'False' thereafter. + security_level: If 'high', the VTN and VEN use a third-party signing authority to sign + and authenticate each request. + Default is 'standard' (XML payloads do not contain Signature elements). + poll_interval_secs: (integer) How often the VEN should send an OadrPoll to the VTN. + log_xml: ('True' or 'False') Whether to write inbound/outbound XML to the agent's log. + opt_in_timeout_secs: (integer) How long to wait before making a default optIn/optOut decision. + opt_in_default_decision: ('True' or 'False') What optIn/optOut choice to make by default. + request_events_on_startup: ('True' or 'False') Whether to send oadrRequestEvent to the VTN on startup. + report_parameters: A dictionary of definitions of reporting/telemetry parameters. + """ + _log.debug("Configuring agent") + self.db_path = config.get('db_path') + self.ven_id = config.get('ven_id') + self.ven_name = config.get('ven_name') + self.vtn_id = config.get('vtn_id') + self.vtn_address = config.get('vtn_address') + self.send_registration = (config.get('send_registration') == 'True') + self.security_level = config.get('security_level') + self.log_xml = (config.get('log_xml') != 'False') + opt_in_timeout = config.get('opt_in_timeout_secs') + self.opt_in_timeout_secs = int(opt_in_timeout if opt_in_timeout else DEFAULT_OPT_IN_TIMEOUT_SECS) + self.opt_in_default_decision = config.get('opt_in_default_decision') + loop_frequency = config.get('poll_interval_secs') + self.poll_interval_secs = int(loop_frequency if loop_frequency else PROCESS_LOOP_FREQUENCY_SECS) + self.request_events_on_startup = (config.get('request_events_on_startup') == 'True') + self.report_parameters = config.get('report_parameters') + + # Validate and adjust the configuration parameters. + if type(self.db_path) == str: + self.db_path = os.path.expanduser(self.db_path) + self.db_path = os.path.expandvars(self.db_path) + try: + self.opt_in_timeout_secs = int(self.opt_in_timeout_secs) + except ValueError: + # If opt_in_timeout_secs was not supplied or was not an integer, default to a 10-minute timeout. + self.opt_in_timeout_secs = 600 + + if self.poll_interval_secs < PROCESS_LOOP_FREQUENCY_SECS: + _log.warning('Poll interval is too frequent: resetting it to {}'.format(PROCESS_LOOP_FREQUENCY_SECS)) + self.poll_interval_secs = PROCESS_LOOP_FREQUENCY_SECS + + _log.info('Configuration parameters:') + _log.info('\tDatabase = {}'.format(self.db_path)) + _log.info('\tVEN ID = {}'.format(self.ven_id)) + _log.info('\tVEN name = {}'.format(self.ven_name)) + _log.info('\tVTN ID = {}'.format(self.vtn_id)) + _log.info('\tVTN address = {}'.format(self.vtn_address)) + _log.info('\tSend registration = {}'.format(self.send_registration)) + _log.info('\tSecurity level = {}'.format(self.security_level)) + _log.info('\tPoll interval = {} seconds'.format(self.poll_interval_secs)) + _log.info('\tLog XML = {}'.format(self.log_xml)) + _log.info('\toptIn timeout (secs) = {}'.format(self.opt_in_timeout_secs)) + _log.info('\toptIn default decision = {}'.format(self.opt_in_default_decision)) + _log.info('\tRequest events on startup = {}'.format(self.request_events_on_startup)) + _log.info("\treport parameters = {}".format(self.report_parameters)) + + @Core.receiver('onstart') + def onstart_method(self, sender): + """The agent has started. Perform initialization and spawn the main process loop.""" + _log.debug('Starting agent') + + self.register_endpoints() + + if self.send_registration: + # VEN registration with the VTN server. + # Register the VEN, obtaining the VEN ID. This is a one-time action. + self.send_oadr_create_party_registration() + else: + # Schedule an hourly database-cleanup task. + self.core.schedule(periodic(60 * 60), self.telemetry_cleanup) + + # Populate the caches with all of the database's events and reports that are active. + for event in self._get_events(): + _log.debug('Re-caching event with ID {}'.format(event.event_id)) + self._active_events[event.event_id] = event + for report in self._get_reports(): + _log.debug('Re-caching report with ID {}'.format(report.report_request_id)) + self._active_reports[report.report_request_id] = report + + try: + if self.request_events_on_startup: + # After a restart, the VEN asks the VTN for the status of all current events. + # When this is sent to the EPRI VTN server, it returns a 500 and logs a "method missing" traceback. + self.send_oadr_request_event() + + if USE_REPORTS: + # Send an initial report-registration request to the VTN. + self.send_oadr_register_report() + except Exception as err: + _log.error('Error in agent startup: {}'.format(err), exc_info=True) + self.core.schedule(periodic(PROCESS_LOOP_FREQUENCY_SECS), self.main_process_loop) + + def main_process_loop(self): + """ + gevent thread. Perform periodic tasks, executing them serially. + + Periodic tasks include: + Poll the VTN server. + Perform event-management tasks: + Force an optIn/optOut decision if too much time has elapsed. + Transition event state when appropriate. + Expire events that have become completed or canceled. + Perform report-management tasks: + Send telemetry to the VTN for any active report. + Transition report state when appropriate. + Expire reports that have become completed or canceled. + + This is intended to be a long-running gevent greenlet -- it should never crash. + If exceptions occur, they are logged, but no process failure occurs. + """ + try: + # If it's been poll_interval_secs since the last poll request, issue a new one. + if self._last_poll is None or \ + ((utils.get_aware_utc_now() - self._last_poll).total_seconds() > self.poll_interval_secs): + if SEND_POLL: + self.send_oadr_poll() + + for event in self.active_events(): + self.process_event(event) + + if USE_REPORTS: + for report in self.active_reports(): + self.process_report(report) + + except Exception as err: + _log.error('Error in main process loop: {}'.format(err), exc_info=True) + + def process_event(self, evt): + """ + Perform periodic maintenance for an event that's in the cache. + + Transition its state when appropriate. + Expire it from the cache if it has become completed or canceled. + + @param evt: An EiEvent instance. + """ + now = utils.get_aware_utc_now() + if evt.is_active(): + if evt.end_time is not None and now > evt.end_time: + _log.debug('Setting event {} to status {}'.format(evt.event_id, evt.STATUS_COMPLETED)) + self.set_event_status(evt, evt.STATUS_COMPLETED) + self.publish_event(evt) + else: + if evt.status == evt.STATUS_ACTIVE: + # It's an active event. Which is fine; nothing special needs to be done here. + pass + else: + if now > evt.start_time and evt.opt_type == evt.OPT_TYPE_OPT_IN: + _log.debug('Setting event {} to status {}'.format(evt.event_id, evt.STATUS_ACTIVE)) + self.set_event_status(evt, evt.STATUS_ACTIVE) + self.publish_event(evt) + else: + # Expire events from the cache if they're completed or canceled. + _log.debug('Expiring event {}'.format(evt.event_id)) + self.expire_event(evt) + + def process_report(self, rpt): + """ + Perform periodic maintenance for a report that's in the cache. + + Send telemetry to the VTN if the report is active. + Transition its state when appropriate. + Expire it from the cache if it has become completed or canceled. + + @param rpt: An EiReport instance. + """ + if rpt.is_active(): + now = utils.get_aware_utc_now() + if rpt.status == rpt.STATUS_ACTIVE: + if rpt.end_time is None or rpt.end_time > now: + rpt_interval = rpt.interval_secs if rpt.interval_secs is not None else DEFAULT_REPORT_INTERVAL_SECS + next_report_time = rpt.last_report + timedelta(seconds=rpt_interval) + if utils.get_aware_utc_now() > next_report_time: + # Possible enhancement: Use a periodic gevent instead of a timeout? + self.send_oadr_update_report(rpt) + if rpt_interval == 0: + # OADR rule 324: If rpt_interval == 0 it's a one-time report, so set status to COMPLETED. + rpt.status = rpt.STATUS_COMPLETED + self.commit() + else: + _log.debug('Setting report {} to status {}'.format(rpt.report_request_id, rpt.STATUS_COMPLETED)) + self.set_report_status(rpt, rpt.STATUS_COMPLETED) + self.publish_telemetry_parameters_for_report(rpt) + else: + if rpt.start_time < now and (rpt.end_time is None or now < rpt.end_time): + _log.debug('Setting report {} to status {}'.format(rpt.report_request_id, rpt.STATUS_ACTIVE)) + self.set_report_status(rpt, rpt.STATUS_ACTIVE) + self.publish_telemetry_parameters_for_report(rpt) + else: + # Expire reports from the cache if they're completed or canceled. + _log.debug('Expiring report {} from cache'.format(rpt.report_request_id)) + self.expire_event(rpt) + + def force_opt_type_decision(self, event_id): + """ + Force an optIn/optOut default decision if lots of time has elapsed with no decision from the control agent. + + Scheduled gevent thread, kicked off when an event is first published. + The default choice comes from "opt_in_default_decision" in the agent config. + + @param event_id: (String) ID of the event for which a decision will be made. + """ + event = self.get_event_for_id(event_id) + if event and event.is_active() and event.opt_type not in [EiEvent.OPT_TYPE_OPT_IN, + EiEvent.OPT_TYPE_OPT_OUT]: + event.opt_type = self.opt_in_default_decision + self.commit() + _log.info('Forcing an {} decision for event {}'.format(event.opt_type, event.event_id)) + if event.status == event.STATUS_ACTIVE: + # Odd exception scenario: If the event was already active, roll its status back to STATUS_FAR. + self.set_event_status(event, event.STATUS_FAR) + self.publish_event(event) # Tell the volttron message bus. + self.send_oadr_created_event(event) # Tell the VTN. + + # ***************** Methods for Servicing VTN Requests ******************** + + def push_request(self, env, request): + """Callback. The VTN pushed an http request. Service it.""" + _log.debug('Servicing a VTN push request') + self.core.spawn(self.service_vtn_request, request) + # Return an empty response. + return [HTTP_STATUS_CODES[204], '', [("Content-Length", "0")]] + + def service_vtn_request(self, request): + """ + An HTTP request/response was received. Handle it. + + Event workflow (see OpenADR Profile Specification section 8.1)... + + Event poll / creation: + (VEN) oadrPoll + (VTN) oadrDistributeEvent (all events are included; one oadrEvent element per event) + (VEN) oadrCreatedEvent with optIn/optOut (if events had oadrResponseRequired) + If "always", an oadrCreatedEvent must be sent for each event. + If "never", it was a "broadcast" event -- never create an event in response. + Otherwise, respond if event state (eventID, modificationNumber) has changed. + (VTN) oadrResponse + + Event change: + (VEN) oadrCreatedEvent (sent if the optIn/optOut status has changed) + (VTN) oadrResponse + + Sample oadrDistributeEvent use case from the OpenADR Program Guide: + + Event: + Notification: Day before event + Start Time: midnight + Duration: 24 hours + Randomization: None + Ramp Up: None + Recovery: None + Number of signals: 2 + Signal Name: simple + Signal Type: level + Units: LevN/A + Number of intervals: equal TOU Tier change in 24 hours (2 - 6) + Interval Duration(s): TOU tier active time frame (i.e. 6 hours) + Typical Interval Value(s): 0 - 4 mapped to TOU Tiers (0 - Cheapest Tier) + Signal Target: None + Signal Name: ELECTRICITY_PRICE + Signal Type: price + Units: USD per Kwh + Number of intervals: equal TOU Tier changes in 24 hours (2 - 6) + Interval Duration(s): TOU tier active time frame (i.e. 6 hours) + Typical Interval Value(s): $0.10 to $1.00 (current tier rate) + Signal Target: None + Event Targets: venID_1234 + Priority: 1 + VEN Response Required: always + VEN Expected Response: optIn + Reports: + None + + Report workflow (see OpenADR Profile Specification section 8.3)... + + Report registration interaction: + (VEN) oadrRegisterReport (METADATA report) + VEN sends its reporting capabilities to VTN. + Each report, identified by a reportSpecifierID, is described as elements and attributes. + (VTN) oadrRegisteredReport (with optional oadrReportRequests) + VTN acknowledges that capabilities have been registered. + VTN optionally requests one or more reports by reportSpecifierID. + Even if reports were previously requested, they should be requested again at this point. + (VEN) oadrCreatedReport (if report requested) + VEN acknowledges that it has received the report request and is generating the report. + If any reports were pending delivery, they are included in the payload. + (VTN) oadrResponse + Why?? + + Report creation interaction: + (VTN) oadrCreateReport + See above - this is like the "request" portion of oadrRegisteredReport + (VEN) oadrCreatedReport + See above. + + Report update interaction - this is the actual report: + (VEN) oadrUpdateReport (report with reportRequestID and reportSpecifierID) + Send a report update containing actual data values + (VTN) oadrUpdatedReport (optional oadrCancelReport) + Acknowledge report receipt, and optionally cancel the report + + Report cancellation: + (VTN) oadrCancelReport (reportRequestID) + This can be sent to cancel a report that is in progress. + It should also be sent if the VEN keeps sending oadrUpdateReport + after an oadrUpdatedReport cancellation. + If reportToFollow = True, the VEN is expected to send one final additional report. + (VEN) oadrCanceledReport + Acknowledge the cancellation. + If any reports were pending delivery, they are included in the payload. + + Key elements in the METADATA payload: + reportSpecifierID: Report identifier, used by subsequent oadrCreateReport requests + rid: Data point identifier + This VEN reports only two data points: baselinePower, actualPower + Duration: the amount of time that data can be collected + SamplingRate.oadrMinPeriod: maximum sampling frequency + SamplingRate.oadrMaxPeriod: minimum sampling frequency + SamplingRate.onChange: whether or not data is sampled as it changes + + For an oadrCreateReport example from the OpenADR Program Guide, see test/xml/sample_oadrCreateReport.xml. + + @param request: The request's XML payload. + """ + try: + if self.log_xml: + _log.debug('VTN PAYLOAD:') + _log.debug('\n{}'.format(etree_.tostring(etree_.fromstring(request), pretty_print=True))) + payload = parseString(request, silence=True) + signed_object = payload.oadrSignedObject + if signed_object is None: + raise OpenADRInterfaceException('No SignedObject in payload', OADR_BAD_DATA) + + if self.security_level == 'high': + # At high security, the request is accompanied by a Signature. + # (not implemented) The VEN should use a certificate authority to validate and decode the request. + pass + + # Call an appropriate method to handle the VTN request. + element_name = self.vtn_request_element_name(signed_object) + _log.debug('VTN: {}'.format(element_name)) + request_object = getattr(signed_object, element_name) + request_method = getattr(self, VTN_REQUESTS[element_name]) + request_method(request_object) + + if request_object.__class__.__name__ != 'oadrResponseType': + # A non-default response was received from the VTN. Issue a followup poll request. + self.send_oadr_poll() + + except OpenADRInternalException as err: + if err.error_code == OADR_EMPTY_DISTRIBUTE_EVENT: + _log.warning('Error handling VTN request: {}'.format(err)) # No need for a stack trace + else: + _log.warning('Error handling VTN request: {}'.format(err), exc_info=True) + except OpenADRInterfaceException as err: + _log.warning('Error handling VTN request: {}'.format(err), exc_info=True) + # OADR rule 48: Log the validation failure, send an oadrResponse.eiResponse with an error code. + self.send_oadr_response(err, err.error_code or OADR_BAD_DATA) + except Exception as err: + _log.error("Error handling VTN request: {}".format(err), exc_info=True) + self.send_oadr_response(err, OADR_BAD_DATA) + + @staticmethod + def vtn_request_element_name(signed_object): + """Given a SignedObject from the VTN, return the element name of the request that it wraps.""" + non_null_elements = [name for name in VTN_REQUESTS.keys() if getattr(signed_object, name)] + element_count = len(non_null_elements) + if element_count == 1: + return non_null_elements[0] + + if element_count == 0: + error_msg = 'Bad request {}, supported types are {}'.format(signed_object, VTN_REQUESTS.keys()) + else: + error_msg = 'Bad request {}, too many signedObject elements'.format(signed_object) + raise OpenADRInterfaceException(error_msg, None) + + # ***************** Handle Requests from the VTN to the VEN ******************** + + def handle_oadr_created_party_registration(self, oadr_created_party_registration): + """ + The VTN has responded to an oadrCreatePartyRegistration by sending an oadrCreatedPartyRegistration. + + @param oadr_created_party_registration: The VTN's request. + """ + self.oadr_current_service = EIREGISTERPARTY + self.check_ei_response(oadr_created_party_registration.eiResponse) + extractor = OadrCreatedPartyRegistrationExtractor(registration=oadr_created_party_registration) + _log.info('***********') + ven_id = extractor.extract_ven_id() + if ven_id: + _log.info('The VTN supplied {} as the ID of this VEN (ven_id).'.format(ven_id)) + poll_freq = extractor.extract_poll_freq() + if poll_freq: + _log.info('The VTN requested a poll frequency of {} (poll_interval_secs).'.format(poll_freq)) + vtn_id = extractor.extract_vtn_id() + if vtn_id: + _log.info('The VTN supplied {} as its ID (vtn_id).'.format(vtn_id)) + _log.info('Please set these values in the VEN agent config.') + _log.info('Registration is complete. Set send_registration to False in the VEN config and restart the agent.') + _log.info('***********') + + def handle_oadr_distribute_event(self, oadr_distribute_event): + """ + The VTN has responded to an oadrPoll by sending an oadrDistributeEvent. + + Create or update an event, then respond with oadrCreatedEvent. + + For sample XML, see test/xml/sample_oadrDistributeEvent.xml. + + @param oadr_distribute_event: (OadrDistributeEventType) The VTN's request. + """ + self.oadr_current_service = EIEVENT + self.oadr_current_request_id = None + if getattr(oadr_distribute_event, 'eiResponse'): + self.check_ei_response(oadr_distribute_event.eiResponse) + + # OADR rule 41: requestID does not need to be unique. + self.oadr_current_request_id = oadr_distribute_event.requestID + + vtn_id = oadr_distribute_event.vtnID + if vtn_id is not None and vtn_id != self.vtn_id: + raise OpenADRInterfaceException('vtnID failed to match agent config: {}'.format(vtn_id), OADR_BAD_DATA) + + oadr_event_list = oadr_distribute_event.oadrEvent + if len(oadr_event_list) == 0: + raise OpenADRInternalException('oadrDistributeEvent received with no events', OADR_EMPTY_DISTRIBUTE_EVENT) + + oadr_event_ids = [] + for oadr_event in oadr_event_list: + try: + event = self.handle_oadr_event(oadr_event) + if event: + oadr_event_ids.append(event.event_id) + except OpenADRInterfaceException as err: + # OADR rule 19: If a VTN message contains a mix of valid and invalid events, + # respond to the valid ones. Don't reject the entire message due to invalid events. + # OADR rule 48: Log the validation failure and send the error code in oadrCreatedEvent.eventResponse. + # (The oadrCreatedEvent's eiResponse should contain a 200 -- normal -- status code.) + _log.warning('Event error: {}'.format(err), exc_info=True) + # Construct a temporary EIEvent to hold data that will be reported in the error return. + if oadr_event.eiEvent and oadr_event.eiEvent.eventDescriptor: + event_id = oadr_event.eiEvent.eventDescriptor.eventID + modification_number = oadr_event.eiEvent.eventDescriptor.modificationNumber + else: + event_id = None + modification_number = None + error_event = EiEvent(self.oadr_current_request_id, event_id) + error_event.modification_number = modification_number + self.send_oadr_created_event(error_event, + error_code=err.error_code or OADR_BAD_DATA, + error_message=err) + except Exception as err: + _log.warning('Unanticipated error during event processing: {}'.format(err), exc_info=True) + self.send_oadr_response(err, OADR_BAD_DATA) + + for agent_event in self._get_events(): + if agent_event.event_id not in oadr_event_ids: + # "Implied cancel:" + # OADR rule 61: If the VTN request omitted an active event, cancel it. + # Also, think about whether to alert the VTN about this cancellation by sending it an oadrCreatedEvent. + _log.debug('Event ID {} not in distributeEvent: canceling it.'.format(agent_event.event_id)) + self.handle_event_cancellation(agent_event, 'never') + + def handle_oadr_event(self, oadr_event): + """ + An oadrEvent was received, usually as part of an oadrDistributeEvent. Handle the event creation/update. + + Respond with oadrCreatedEvent. + + For sample XML, see test/xml/sample_oadrDistributeEvent.xml. + + @param oadr_event: (OadrEventType) The VTN's request. + @return: (EiEvent) The event that was created or updated. + """ + + def create_temp_event(received_ei_event): + """Create a temporary EiEvent in preparation for an event creation or update.""" + event_descriptor = received_ei_event.eventDescriptor + if event_descriptor is None: + raise OpenADRInterfaceException('Missing eiEvent.eventDescriptor', OADR_BAD_DATA) + event_id = event_descriptor.eventID + if event_id is None: + raise OpenADRInterfaceException('Missing eiEvent.eventDescriptor.eventID', OADR_BAD_DATA) + _log.debug('Processing received event, ID = {}'.format(event_id)) + tmp_event = EiEvent(self.oadr_current_request_id, event_id) + extractor = OadrEventExtractor(event=tmp_event, ei_event=received_ei_event) + extractor.extract_event_descriptor() + extractor.extract_active_period() + extractor.extract_signals() + return tmp_event + + def update_event(temp_event, event): + """Update the current event based on the contents of temp_event.""" + _log.debug('Modification number has changed: {}'.format(temp_event.modification_number)) + # OADR rule 57: If modificationNumber increments, replace the event with the modified version. + if event.opt_type == EiEvent.OPT_TYPE_OPT_OUT: + # OADR rule 50: The VTN may continue to send events that the VEN has opted out of. + pass # Take no action, other than responding to the VTN. + else: + if temp_event.status == EiEvent.STATUS_CANCELED: + if event.status != EiEvent.STATUS_CANCELED: + # OADR rule 59: The event was just canceled. Process an event cancellation. + self.handle_event_cancellation(event, response_required) + else: + event.copy_from_event(temp_event) + # A VEN may ignore the received event status, calculating it based on the time. + # OADR rule 66: Do not treat status == completed as a cancellation. + if event.status == EiEvent.STATUS_CANCELED and temp_event.status != EiEvent.STATUS_CANCELED: + # If the VEN thinks the event is canceled and the VTN doesn't think that, un-cancel it. + event.status = temp_event.status + self.commit() + # Tell the VOLTTRON world about the event update. + self.publish_event(event) + + def create_event(event): + self.add_event(event) + if event.status == EiEvent.STATUS_CANCELED: + # OADR rule 60: Ignore a new event if it's cancelled - this is NOT a validation error. + pass + else: + opt_deadline = utils.get_aware_utc_now() + timedelta(seconds=self.opt_in_timeout_secs) + self.core.schedule(opt_deadline, self.force_opt_type_decision, event.event_id) + _log.debug('Scheduled a default optIn/optOut decision for {}'.format(opt_deadline)) + self.publish_event(event) # Tell the VOLTTRON world about the event creation. + + # Create a temporary EiEvent, constructed from the OadrDistributeEventType. + ei_event = oadr_event.eiEvent + response_required = oadr_event.oadrResponseRequired + + if ei_event.eiTarget and ei_event.eiTarget.venID and self.ven_id not in ei_event.eiTarget.venID: + # Rule 22: If an eiTarget is furnished, handle the event only if this venID is in the target list. + event = None + else: + temp_event = create_temp_event(ei_event) + event = self.get_event_for_id(temp_event.event_id) + if event: + if temp_event.modification_number < event.modification_number: + _log.debug('Out-of-order modification number: {}'.format(temp_event.modification_number)) + # OADR rule 58: Respond with error code 450. + raise OpenADRInterfaceException('Invalid modification number (too low)', + OADR_MOD_NUMBER_OUT_OF_ORDER) + elif temp_event.modification_number > event.modification_number: + update_event(temp_event, event) + else: + _log.debug('No modification number change, taking no action') + else: + # OADR rule 56: If the received event has an unrecognized event_id, create a new event. + _log.debug('Creating event for ID {}'.format(temp_event.event_id)) + event = temp_event + create_event(event) + + if response_required == 'always': + # OADR rule 12, 62: Send an oadrCreatedEvent if response_required == 'always'. + # OADR rule 12, 62: If response_required == 'never', do not send an oadrCreatedEvent. + self.send_oadr_created_event(event) + + return event + + def handle_event_cancellation(self, event, response_required): + """ + An event was canceled by the VTN. Update local state and publish the news. + + @param event: (EiEvent) The event that was canceled. + @param response_required: (string) Indicates when the VTN expects a confirmation/response to its request. + """ + if event.start_after: + # OADR rule 65: If the event has a startAfter value, + # schedule cancellation for a random future time between now and (now + startAfter). + max_delay = isodate.parse_duration(event.start_after) + cancel_time = utils.get_aware_utc_now() + timedelta(seconds=(max_delay.seconds * random.random())) + self.core.schedule(cancel_time, self._handle_event_cancellation, event, response_required) + else: + self._handle_event_cancellation(event, response_required) + + def _handle_event_cancellation(self, event, response_required): + """ + (Internal) An event was canceled by the VTN. Update local state and publish the news. + + @param event: (EiEvent) The event that was canceled. + @param response_required: (string) Indicates when the VTN expects a confirmation/response to its request. + """ + event.status = EiEvent.STATUS_CANCELED + if response_required != 'never': + # OADR rule 36: If response_required != never, confirm cancellation with optType = optIn. + event.optType = event.OPT_TYPE_OPT_IN + self.commit() + self.publish_event(event) # Tell VOLTTRON agents about the cancellation. + + def handle_oadr_register_report(self, request): + """ + The VTN is sending METADATA, registering the reports that it can send to the VEN. + + Send no response -- the VEN doesn't want any of the VTN's crumby reports. + + @param request: The VTN's request. + """ + self.oadr_current_service = EIREPORT + self.oadr_current_request_id = None + # OADR rule 301: Sent when the VTN wakes up. + pass + + def handle_oadr_registered_report(self, oadr_registered_report): + """ + The VTN acknowledged receipt of the METADATA in oadrRegisterReport. + + If the VTN requested any reports (by specifier ID), create them. + Send an oadrCreatedReport acknowledgment for each request. + + @param oadr_registered_report: (oadrRegisteredReportType) The VTN's request. + """ + self.oadr_current_service = EIREPORT + self.check_ei_response(oadr_registered_report.eiResponse) + self.create_or_update_reports(oadr_registered_report.oadrReportRequest) + + def handle_oadr_create_report(self, oadr_create_report): + """ + Handle an oadrCreateReport request from the VTN. + + The request could have arrived in response to a poll, + or it could have been part of an oadrRegisteredReport response. + + Create a report for each oadrReportRequest in the list, sending an oadrCreatedReport in response. + + @param oadr_create_report: The VTN's oadrCreateReport request. + """ + self.oadr_current_service = EIREPORT + self.oadr_current_request_id = None + self.create_or_update_reports(oadr_create_report.oadrReportRequest) + + def handle_oadr_updated_report(self, oadr_updated_report): + """ + The VTN acknowledged receipt of an oadrUpdatedReport, and may have sent a report cancellation. + + Check for report cancellation, and cancel the report if necessary. No need to send a response to the VTN. + + @param oadr_updated_report: The VTN's request. + """ + self.oadr_current_service = EIREPORT + self.check_ei_response(oadr_updated_report.eiResponse) + oadr_cancel_report = oadr_updated_report.oadrCancelReport + if oadr_cancel_report: + self.cancel_report(oadr_cancel_report.reportRequestID, acknowledge=False) + + def handle_oadr_cancel_report(self, oadr_cancel_report): + """ + The VTN responded to an oadrPoll by requesting a report cancellation. + + Respond by canceling the report, then send oadrCanceledReport to the VTN. + + @param oadr_cancel_report: (oadrCancelReportType) The VTN's request. + """ + self.oadr_current_service = EIREPORT + self.oadr_current_request_id = oadr_cancel_report.requestID + self.cancel_report(oadr_cancel_report.reportRequestID, acknowledge=True) + + def handle_oadr_response(self, oadr_response): + """ + The VTN has acknowledged a VEN request such as oadrCreatedReport. + + No response is needed. + + @param oadr_response: The VTN's request. + """ + self.check_ei_response(oadr_response.eiResponse) + + def check_ei_response(self, ei_response): + """ + An eiResponse can appear in multiple kinds of VTN requests. + + If an eiResponse has been received, check for a '200' (OK) response code. + If any other code is received, the VTN is reporting an error -- log it and raise an exception. + + @param ei_response: (eiResponseType) The VTN's eiResponse. + """ + self.oadr_current_request_id = ei_response.requestID + response_code, response_description = OadrResponseExtractor(ei_response=ei_response).extract() + if response_code != OADR_VALID_RESPONSE: + error_text = 'Error response from VTN, code={}, description={}'.format(response_code, response_description) + _log.error(error_text) + raise OpenADRInternalException(error_text, response_code) + + def create_or_update_reports(self, report_list): + """ + Process report creation/update requests from the VTN (which could have arrived in different payloads). + + The requests could have arrived in response to a poll, + or they could have been part of an oadrRegisteredReport response. + + Create/Update reports, and publish info about them on the volttron message bus. + Send an oadrCreatedReport response to the VTN for each report. + + @param report_list: A list of oadrReportRequest. Can be None. + """ + + def create_temp_rpt(report_request): + """Validate the report request, creating a temporary EiReport instance in the process.""" + extractor = OadrReportExtractor(request=report_request) + tmp_report = EiReport(None, + extractor.extract_report_request_id(), + extractor.extract_specifier_id()) + rpt_params = self.report_parameters.get(tmp_report.report_specifier_id, None) + if rpt_params is None: + err_msg = 'No parameters found for report with specifier ID {}'.format(tmp_report.report_specifier_id) + _log.error(err_msg) + raise OpenADRInterfaceException(err_msg, OADR_BAD_DATA) + extractor.report_parameters = rpt_params + extractor.report = tmp_report + extractor.extract_report() + return tmp_report + + def update_rpt(tmp_rpt, rpt): + """If the report changed, update its parameters in the database, and publish them on the message bus.""" + if rpt.report_specifier_id != tmp_rpt.report_specifier_id \ + or rpt.start_time != tmp_rpt.start_time \ + or rpt.end_time != tmp_rpt.end_time \ + or rpt.interval_secs != tmp_rpt.interval_secs: + rpt.copy_from_report(tmp_rpt) + self.commit() + self.publish_telemetry_parameters_for_report(rpt) + + def create_rpt(tmp_rpt): + """Store the new report request in the database, and publish it on the message bus.""" + self.add_report(tmp_rpt) + self.publish_telemetry_parameters_for_report(tmp_rpt) + + def cancel_rpt(rpt): + """A report cancellation was received. Process it and notify interested parties.""" + rpt.status = rpt.STATUS_CANCELED + self.commit() + self.publish_telemetry_parameters_for_report(rpt) + + oadr_report_request_ids = [] + + try: + if report_list: + for oadr_report_request in report_list: + temp_report = create_temp_rpt(oadr_report_request) + existing_report = self.get_report_for_report_request_id(temp_report.report_request_id) + if temp_report.status == temp_report.STATUS_CANCELED: + if existing_report: + oadr_report_request_ids.append(temp_report.report_request_id) + cancel_rpt(existing_report) + self.send_oadr_created_report(oadr_report_request) + else: + # Received notification of a new report, but it's already canceled. Take no action. + pass + else: + oadr_report_request_ids.append(temp_report.report_request_id) + if temp_report.report_specifier_id == 'METADATA': + # Rule 301/327: If the request's specifierID is 'METADATA', send an oadrRegisterReport. + self.send_oadr_created_report(oadr_report_request) + self.send_oadr_register_report() + elif existing_report: + update_rpt(temp_report, existing_report) + self.send_oadr_created_report(oadr_report_request) + else: + create_rpt(temp_report) + self.send_oadr_created_report(oadr_report_request) + except OpenADRInterfaceException as err: + # If a VTN message contains a mix of valid and invalid reports, respond to the valid ones. + # Don't reject the entire message due to an invalid report. + _log.warning('Report error: {}'.format(err), exc_info=True) + self.send_oadr_response(err, err.error_code or OADR_BAD_DATA) + except Exception as err: + _log.warning('Unanticipated error during report processing: {}'.format(err), exc_info=True) + self.send_oadr_response(err, OADR_BAD_DATA) + + all_active_reports = self._get_reports() + for agent_report in all_active_reports: + if agent_report.report_request_id not in oadr_report_request_ids: + # If the VTN's request omitted an active report, treat it as an implied cancellation. + report_request_id = agent_report.report_request_id + _log.debug('Report request ID {} not sent by VTN, canceling the report.'.format(report_request_id)) + self.cancel_report(report_request_id, acknowledge=True) + + def cancel_report(self, report_request_id, acknowledge=False): + """ + The VTN asked to cancel a report, in response to either report telemetry or an oadrPoll. Cancel it. + + @param report_request_id: (string) The report_request_id of the report to be canceled. + @param acknowledge: (boolean) If True, send an oadrCanceledReport acknowledgment to the VTN. + """ + if report_request_id is None: + raise OpenADRInterfaceException('Missing oadrCancelReport.reportRequestID', OADR_BAD_DATA) + report = self.get_report_for_report_request_id(report_request_id) + if report: + report.status = report.STATUS_CANCELED + self.commit() + self.publish_telemetry_parameters_for_report(report) + if acknowledge: + self.send_oadr_canceled_report(report_request_id) + else: + # The VEN got asked to cancel a report that it doesn't have. Do nothing. + pass + + # ***************** Send Requests from the VEN to the VTN ******************** + + def send_oadr_poll(self): + """Send oadrPoll to the VTN.""" + _log.debug('VEN: oadrPoll') + self.oadr_current_service = POLL + # OADR rule 37: The VEN must support the PULL implementation. + self._last_poll = utils.get_aware_utc_now() + self.send_vtn_request('oadrPoll', OadrPollBuilder(ven_id=self.ven_id).build()) + + def send_oadr_query_registration(self): + """Send oadrQueryRegistration to the VTN.""" + _log.debug('VEN: oadrQueryRegistration') + self.oadr_current_service = EIREGISTERPARTY + self.send_vtn_request('oadrQueryRegistration', OadrQueryRegistrationBuilder().build()) + + def send_oadr_create_party_registration(self): + """Send oadrCreatePartyRegistration to the VTN.""" + _log.debug('VEN: oadrCreatePartyRegistration') + self.oadr_current_service = EIREGISTERPARTY + send_signature = (self.security_level == 'high') + # OADR rule 404: If the VEN hasn't registered before, venID and registrationID should be empty. + builder = OadrCreatePartyRegistrationBuilder(ven_id=None, xml_signature=send_signature, ven_name=self.ven_name) + self.send_vtn_request('oadrCreatePartyRegistration', builder.build()) + + def send_oadr_request_event(self): + """Send oadrRequestEvent to the VTN.""" + _log.debug('VEN: oadrRequestEvent') + self.oadr_current_service = EIEVENT + self.send_vtn_request('oadrRequestEvent', OadrRequestEventBuilder(ven_id=self.ven_id).build()) + + def send_oadr_created_event(self, event, error_code=None, error_message=None): + """ + Send oadrCreatedEvent to the VTN. + + @param event: (EiEvent) The event that is the subject of the request. + @param error_code: (string) eventResponse error code. Used when reporting event protocol errors. + @param error_message: (string) eventResponse error message. Used when reporting event protocol errors. + """ + _log.debug('VEN: oadrCreatedEvent') + self.oadr_current_service = EIEVENT + builder = OadrCreatedEventBuilder(event=event, ven_id=self.ven_id, + error_code=error_code, error_message=error_message) + self.send_vtn_request('oadrCreatedEvent', builder.build()) + + def send_oadr_register_report(self): + """ + Send oadrRegisterReport (METADATA) to the VTN. + + Sample oadrRegisterReport from the OpenADR Program Guide: + + + RegReq120615_122508_975 + + --- See oadr_report() --- + + ec27de207837e1048fd3 + + """ + _log.debug('VEN: oadrRegisterReport') + self.oadr_current_service = EIREPORT + # The VEN is currently hard-coded to support the 'telemetry' report, which sends baseline and measured power, + # and the 'telemetry_status' report, which sends online and manual_override status. + # In order to support additional reports and telemetry types, the VEN would need to store other data elements + # as additional columns in its SQLite database. + builder = OadrRegisterReportBuilder(reports=self.metadata_reports(), ven_id=self.ven_id) + # The EPRI VTN server responds to this request with "452: Invalid ID". Why? + self.send_vtn_request('oadrRegisterReport', builder.build()) + + def send_oadr_update_report(self, report): + """ + Send oadrUpdateReport to the VTN. + + Sample oadrUpdateReport from the OpenADR Program Guide: + + + ReportUpdReqID130615_192730_445 + + --- See OadrUpdateReportBuilder --- + + VEN130615_192312_582 + + + @param report: (EiReport) The report for which telemetry should be sent. + """ + _log.debug('VEN: oadrUpdateReport (report {})'.format(report.report_request_id)) + self.oadr_current_service = EIREPORT + telemetry = self.get_new_telemetry_for_report(report) if report.report_specifier_id == 'telemetry' else [] + builder = OadrUpdateReportBuilder(report=report, + telemetry=telemetry, + online=self.ven_online, + manual_override=self.ven_manual_override, + ven_id=self.ven_id) + self.send_vtn_request('oadrUpdateReport', builder.build()) + report.last_report = utils.get_aware_utc_now() + self.commit() + + def send_oadr_created_report(self, report_request): + """ + Send oadrCreatedReport to the VTN. + + @param report_request: (oadrReportRequestType) The VTN's report request. + """ + _log.debug('VEN: oadrCreatedReport') + self.oadr_current_service = EIREPORT + builder = OadrCreatedReportBuilder(report_request_id=report_request.reportRequestID, + ven_id=self.ven_id, + pending_report_request_ids=self.get_pending_report_request_ids()) + self.send_vtn_request('oadrCreatedReport', builder.build()) + + def send_oadr_canceled_report(self, report_request_id): + """ + Send oadrCanceledReport to the VTN. + + @param report_request_id: (string) The reportRequestID of the report that has been canceled. + """ + _log.debug('VEN: oadrCanceledReport') + self.oadr_current_service = EIREPORT + builder = OadrCanceledReportBuilder(request_id=self.oadr_current_request_id, + report_request_id=report_request_id, + ven_id=self.ven_id, + pending_report_request_ids=self.get_pending_report_request_ids()) + self.send_vtn_request('oadrCanceledReport', builder.build()) + + def send_oadr_response(self, response_description, response_code): + """ + Send an oadrResponse to the VTN. + + @param response_description: (string The response description. + @param response_code: (string) The response code, 200 if OK. + """ + _log.debug('VEN: oadrResponse') + builder = OadrResponseBuilder(response_code=response_code, + response_description=response_description, + request_id=self.oadr_current_request_id or '0', + ven_id=self.ven_id) + self.send_vtn_request('oadrResponse', builder.build()) + + def send_vtn_request(self, request_name, request_object): + """ + Send a request to the VTN. If the VTN returns a non-empty response, service that request. + + Wrap the request in a SignedObject and then in Payload XML, and post it to the VTN via HTTP. + If using high security, calculate a digital signature and include it in the request payload. + + @param request_name: (string) The name of the SignedObject attribute where the request is attached. + @param request_object: (various oadr object types) The request to send. + """ + signed_object = oadrSignedObject(**{request_name: request_object}) + try: + # Export the SignedObject as an XML string. + buff = io.StringIO() + signed_object.export(buff, 1, pretty_print=True) + signed_object_xml = buff.getvalue() + except Exception as err: + raise OpenADRInterfaceException('Error exporting the SignedObject: {}'.format(err), None) + + if self.security_level == 'high': + try: + signature_lxml, signed_object_lxml = self.calculate_signature(signed_object_xml) + except Exception as err: + raise OpenADRInterfaceException('Error signing the SignedObject: {}'.format(err), None) + payload_lxml = self.payload_element(signature_lxml, signed_object_lxml) + try: + # Verify that the payload, with signature, is well-formed and can be validated. + signxml.XMLVerifier().verify(payload_lxml, ca_pem_file=VTN_CA_CERT_FILENAME) + except Exception as err: + raise OpenADRInterfaceException('Error verifying the SignedObject: {}'.format(err), None) + else: + signed_object_lxml = etree_.fromstring(signed_object_xml) + payload_lxml = self.payload_element(None, signed_object_lxml) + + if self.log_xml: + _log.debug('VEN PAYLOAD:') + _log.debug('\n{}'.format(etree_.tostring(payload_lxml, pretty_print=True))) + + # Post payload XML to the VTN as an HTTP request. Return the VTN's response, if any. + endpoint = self.vtn_address + (self.oadr_current_service or POLL) + try: + payload_xml = etree_.tostring(payload_lxml) + # OADR rule 53: If simple HTTP mode is used, send the following headers: Host, Content-Length, Content-Type. + # The EPRI VTN server responds with a 400 "bad request" if a "Host" header is sent. + _log.debug('Posting VEN request to {}'.format(endpoint)) + response = requests.post(endpoint, data=payload_xml, headers={ + # "Host": endpoint, + "Content-Length": str(len(payload_xml)), + "Content-Type": "application/xml"}) + http_code = response.status_code + if http_code == 200: + if len(response.content) > 0: + self.core.spawn(self.service_vtn_request, response.content) + else: + _log.warning('Received zero-length request from VTN') + elif http_code == 204: + # Empty response received. Take no action. + _log.debug('Empty response received from {}'.format(endpoint)) + else: + _log.error('Error in http request to {}: response={}'.format(endpoint, http_code), exc_info=True) + raise OpenADRInterfaceException('Error in VTN request: {}'.format(http_code), None) + except ConnectionError: + _log.warning('ConnectionError in http request to {} (is the VTN offline?)'.format(endpoint)) + return None + except Exception as err: + raise OpenADRInterfaceException('Error posting OADR XML: {}'.format(err), None) + + # ***************** VOLTTRON RPCs ******************** + + @RPC.export + def respond_to_event(self, event_id, opt_in_choice=None): + """ + Respond to an event, opting in or opting out. + + If an event's status=unresponded, it is awaiting this call. + When this RPC is received, the VENAgent sends an eventResponse to + the VTN, indicating whether optIn or optOut has been chosen. + If an event remains unresponded for a set period of time, + it times out and automatically optsIn to the event. + + Since this call causes a change in the event's status, it triggers + a PubSub call for the event update, as described above. + + @param event_id: (String) ID of an event. + @param opt_in_choice: (String) 'OptIn' to opt into the event, anything else is treated as 'OptOut'. + """ + event = self.get_event_for_id(event_id) + if event: + if opt_in_choice == event.OPT_TYPE_OPT_IN: + event.opt_type = opt_in_choice + else: + event.opt_type = event.OPT_TYPE_OPT_OUT + self.commit() + _log.debug('RPC respond_to_event: Sending {} for event ID {}'.format(event.opt_type, event_id)) + self.send_oadr_created_event(event) + else: + raise OpenADRInterfaceException('No event found for event_id {}'.format(event_id), None) + + @RPC.export + def add_event_for_test(self, event_id, request_id, start_time): + """Add an event to the database and cache. Used during regression testing only.""" + _log.debug('RPC add_event_for_test: Creating event with ID {}'.format(event_id)) + event = EiEvent(event_id, request_id) + event.start_time = parser.parse(start_time) + self.add_event(event) + + @RPC.export + def get_events(self, **kwargs): + """ + Return a list of events as a JSON string. + + See _get_eievents() for a list of parameters and a description of method behavior. + + Sample request: + self.get_events(started_after=utils.get_aware_utc_now() - timedelta(hours=1), + end_time_before=utils.get_aware_utc_now()) + + @return: (JSON) A list of EiEvents -- see 'PubSub: event update'. + """ + _log.debug('RPC get_events') + events = self._get_events(**kwargs) + return None if events is None else self.json_object([e.as_json_compatible_object() for e in events]) + + @RPC.export + def get_telemetry_parameters(self): + """ + Return the VENAgent's current set of telemetry parameters. + + @return: (JSON) Current telemetry parameters -- see 'PubSub: telemetry parameters update'. + """ + _log.debug('RPC get_telemetry_parameters') + # If there is an active report, return its telemetry parameters. + # Otherwise return the telemetry report parameters in agent config. + rpts = self.active_reports() + report = rpts[0] if len(rpts) > 0 else self.metadata_report('telemetry') + # Extend what's reported to include parameters other than just telemetry parameters. + return {'online': self.ven_online, + 'manual_override': self.ven_manual_override, + 'telemetry': report.telemetry_parameters, + 'report parameters': self.json_object(report.as_json_compatible_object())} + + @RPC.export + def set_telemetry_status(self, online, manual_override): + """ + Update the VENAgent's reporting status. + + To be compliant with the OADR profile spec, set these properties to either 'TRUE' or 'FALSE'. + + @param online: (Boolean) Whether the VENAgent's resource is online. + @param manual_override: (Boolean) Whether resource control has been overridden. + """ + _log.debug('RPC set_telemetry_status: online={}, manual_override={}'.format(online, manual_override)) + # OADR rule 510: Provide a TELEMETRY_STATUS report that includes oadrOnline and oadrManualOverride values. + self.ven_online = online + self.ven_manual_override = manual_override + + @RPC.export + def report_telemetry(self, telemetry): + """ + Receive an update of the VENAgent's report metrics, and store them in the agent's database. + + Examples of telemetry are: + { + 'baseline_power_kw': '15.2', + 'current_power_kw': '371.1', + 'start_time': '2017-11-21T23:41:46.051405', + 'end_time': '2017-11-21T23:42:45.951405' + } + + @param telemetry: (JSON) Current value of each report metric, with reporting-interval start/end timestamps. + """ + _log.debug('RPC report_telemetry: {}'.format(telemetry)) + baseline_power_kw = telemetry.get('baseline_power_kw') + current_power_kw = telemetry.get('current_power_kw') + start_time = utils.parse_timestamp_string(telemetry.get('start_time')) + end_time = utils.parse_timestamp_string(telemetry.get('end_time')) + for report in self.active_reports(): + self.add_telemetry(EiTelemetryValues(report_request_id=report.report_request_id, + baseline_power_kw=baseline_power_kw, + current_power_kw=current_power_kw, + start_time=start_time, + end_time=end_time)) + + # ***************** VOLTTRON Pub/Sub Requests ******************** + + def publish_event(self, an_event): + """ + Publish an event. + + When an event is created/updated, it is published to the VOLTTRON bus + with a topic that includes 'openadr/event_update'. + + Event JSON structure: + { + "event_id" : String, + "creation_time" : DateTime, + "start_time" : DateTime, + "end_time" : DateTime or None, + "priority" : Integer, # Values: 0, 1, 2, 3. Usually expected to be 1. + "signals" : String, # Values: json string describing one or more signals. + "status" : String, # Values: unresponded, far, near, active, + # completed, canceled. + "opt_type" : String # Values: optIn, optOut, none. + } + + If an event status is 'unresponded', the VEN agent is awaiting a decision on + whether to optIn or optOut. The downstream agent that subscribes to this PubSub + message should communicate that choice to the VEN agent by calling respond_to_event() + (see below). The VEN agent then relays the choice to the VTN. + + @param an_event: an EiEvent. + """ + if an_event.test_event != 'false': + # OADR rule 6: If testEvent is present and != "false", handle the event as a test event. + _log.debug('Suppressing publication of test event {}'.format(an_event)) + else: + _log.debug('Publishing event {}'.format(an_event)) + request_headers = {headers.TIMESTAMP: format_timestamp(utils.get_aware_utc_now())} + self.vip.pubsub.publish(peer='pubsub', + topic=topics.OPENADR_EVENT+'/'+self.ven_id, + headers=request_headers, + message=self.json_object(an_event.as_json_compatible_object())) + + def publish_telemetry_parameters_for_report(self, report): + """ + Publish telemetry parameters. + + When the VEN agent telemetry reporting parameters have been updated (by the VTN), + they are published with a topic that includes 'openadr/telemetry_parameters'. + If a particular report has been updated, the reported parameters are for that report. + + Telemetry parameters JSON example: + { + "telemetry": { + "baseline_power_kw": { + "r_id": "baseline_power", + "min_frequency": "30", + "max_frequency": "60", + "report_type": "baseline", + "reading_type": "Direct Read", + "units": "powerReal", + "method_name": "get_baseline_power" + } + "current_power_kw": { + "r_id": "actual_power", + "min_frequency": "30", + "max_frequency": "60", + "report_type": "reading", + "reading_type": "Direct Read", + "units": "powerReal", + "method_name": "get_current_power" + } + "manual_override": "False", + "report_status": "active", + "online": "False", + } + } + + The above example indicates that, for reporting purposes, telemetry values + for baseline_power and actual_power should be updated -- via report_telemetry() -- at + least once every 30 seconds. + + Telemetry value definitions such as baseline_power and actual_power come from the + agent configuration. + + @param report: (EiReport) The report whose parameters should be published. + """ + _log.debug('Publishing telemetry parameters') + request_headers = {headers.TIMESTAMP: format_timestamp(utils.get_aware_utc_now())} + self.vip.pubsub.publish(peer='pubsub', + topic=topics.OPENADR_STATUS+'/'+self.ven_id, + headers=request_headers, + message=report.telemetry_parameters) + + # ***************** Database Requests ******************** + + def active_events(self): + """Return a list of events that are neither COMPLETED nor CANCELED.""" + return self._get_events() + + def get_event_for_id(self, event_id): + """Return the event with ID event_id, or None if not found.""" + event_list = self._get_events(event_id=event_id, in_progress_only=False) + return event_list[0] if len(event_list) == 1 else None + + def _get_events(self, event_id=None, in_progress_only=True, started_after=None, end_time_before=None): + """ + Return a list of EiEvents. (internal method) + + By default, return only event requests with status=active or status=unresponded. + + If an event's status=active, a DR event is currently in progress. + + @param event_id: (String) Default None. + @param in_progress_only: (Boolean) Default True. + @param started_after: (DateTime) Default None. + @param end_time_before: (DateTime) Default None. + @return: A list of EiEvents. + """ + # For requests by event ID, query the cache first before querying the database. + if event_id: + event = self._active_events.get(event_id, None) + if event: + return [event] + + db_event = globals()['EiEvent'] + events = self.get_db_session().query(db_event) + if event_id is not None: + events = events.filter(db_event.event_id == event_id) + if in_progress_only: + events = events.filter(~db_event.status.in_([EiEvent.STATUS_COMPLETED, EiEvent.STATUS_CANCELED])) + if started_after: + events = events.filter(db_event.start_time > started_after) + if end_time_before and db_event.end_time: + # An event's end_time can be None, indicating that it doesn't expire until Canceled. + # If the event's end_time is None, don't apply this filter to it. + events = events.filter(db_event.end_time < end_time_before) + return events.all() + + def add_event(self, event): + """A new event has been created. Add it to the event cache, and also to the database.""" + self._active_events[event.event_id] = event + self.get_db_session().add(event) + self.commit() + + def set_event_status(self, event, status): + _log.debug('Transitioning status to {} for event ID {}'.format(status, event.event_id)) + event.status = status + self.commit() + + def expire_event(self, event): + """Remove the event from the event cache. (It remains in the SQLite database.)""" + self._active_events.pop(event.event_id) + + def active_reports(self): + """Return a list of reports that are neither COMPLETED nor CANCELED.""" + return self._get_reports() + + def add_report(self, report): + """A new report has been created. Add it to the report cache, and also to the database.""" + self._active_reports[report.report_request_id] = report + self.get_db_session().add(report) + self.commit() + + def set_report_status(self, report, status): + _log.debug('Transitioning status to {} for report request ID {}'.format(status, report.report_request_id)) + report.status = status + self.commit() + + def expire_report(self, report): + """Remove the report from the report cache. (It remains in the SQLite database.)""" + self._active_reports.pop(report.report_request_id) + + def get_report_for_report_request_id(self, report_request_id): + """Return the EiReport with request ID report_request_id, or None if not found.""" + report_list = self._get_reports(report_request_id=report_request_id, active_only=False) + return report_list[0] if len(report_list) == 1 else None + + def get_reports_for_report_specifier_id(self, report_specifier_id): + """Return the EiReport with request ID report_request_id, or None if not found.""" + return self._get_reports(report_specifier_id=report_specifier_id, active_only=True) + + def get_pending_report_request_ids(self): + """Return a list of reportRequestIDs for each active report.""" + # OpenADR rule 329: Include all current report request IDs in the oadrPendingReports list. + return [r.report_request_id for r in self._get_reports()] + + def _get_reports(self, report_request_id=None, report_specifier_id=None, active_only=True, + started_after=None, end_time_before=None): + """ + Return a list of EiReport. + + By default, return only report requests with status=active. + + @param report_request_id: (String) Default None. + @param report_specifier_id: (String) Default None. + @param active_only: (Boolean) Default True. + @param started_after: (DateTime) Default None. + @param end_time_before: (DateTime) Default None. + @return: A list of EiReports. + """ + # For requests by report ID, query the cache first before querying the database. + if report_request_id: + report = self._active_reports.get(report_request_id, None) + if report: + return [report] + + db_report = globals()['EiReport'] + reports = self.get_db_session().query(db_report) + if report_request_id is not None: + reports = reports.filter(db_report.report_request_id == report_request_id) + if report_specifier_id is not None: + reports = reports.filter(db_report.report_specifier_id == report_specifier_id) + if active_only: + reports = reports.filter(~db_report.status.in_([EiReport.STATUS_COMPLETED, EiReport.STATUS_CANCELED])) + if started_after: + reports = reports.filter(db_report.start_time > started_after) + if end_time_before and db_report.end_time: + # A report's end_time can be None, indicating that it doesn't expire until Canceled. + # If the report's end_time is None, don't apply this filter to it. + reports = reports.filter(db_report.end_time < end_time_before) + return reports.all() + + def metadata_reports(self): + """Return an EiReport instance containing telemetry metadata for each report definition in agent config.""" + return [self.metadata_report(rpt_name) for rpt_name in self.report_parameters.keys()] + + def metadata_report(self, specifier_id): + """Return an EiReport instance for the indicated specifier_id, or None if its' not in agent config.""" + params = self.report_parameters.get(specifier_id, None) + report = EiReport('', '', specifier_id) # No requestID, no reportRequestID + report.name = params.get('report_name_metadata', None) + try: + interval_secs = int(params.get('report_interval_secs_default', None)) + except ValueError: + error_msg = 'Default report interval {} is not an integer number of seconds'.format(default) + raise OpenADRInternalException(error_msg, OADR_BAD_DATA) + report.interval_secs = interval_secs + report.telemetry_parameters = jsonapi.dumps(params.get('telemetry_parameters', None)) + report.report_specifier_id = specifier_id + report.status = report.STATUS_INACTIVE + return report + + def get_new_telemetry_for_report(self, report): + """Query for relevant telemetry that's arrived since the report was last sent to the VTN.""" + db_telemetry_values = globals()['EiTelemetryValues'] + telemetry = self.get_db_session().query(db_telemetry_values) + telemetry = telemetry.filter(db_telemetry_values.report_request_id == report.report_request_id) + telemetry = telemetry.filter(db_telemetry_values.created_on > report.last_report) + return telemetry.all() + + def add_telemetry(self, telemetry): + """New telemetry has been received. Add it to the database.""" + self.get_db_session().add(telemetry) + self.commit() + + def telemetry_cleanup(self): + """gevent thread for periodically deleting week-old telemetry from the database.""" + db_telemetry_values = globals()['EiTelemetryValues'] + telemetry = self.get_db_session().query(db_telemetry_values) + total_rows = telemetry.count() + telemetry = telemetry.filter(db_telemetry_values.created_on < utils.get_aware_utc_now() - timedelta(days=7)) + deleted_row_count = telemetry.delete() + if deleted_row_count: + _log.debug('Deleting {} outdated of {} total telemetry rows in db'.format(deleted_row_count, total_rows)) + self.commit() + + def commit(self): + """Flush any modified objects to the SQLite database.""" + self.get_db_session().commit() + + def get_db_session(self): + """Return the SQLite database session. Initialize the session if this is the first time in.""" + if not self._db_session: + # First time: create a SQLAlchemy engine and session. + try: + database_dir = os.path.dirname(self.db_path) + if not os.path.exists(database_dir): + _log.debug('Creating sqlite database directory {}'.format(database_dir)) + os.makedirs(database_dir) + engine_path = 'sqlite:///' + self.db_path + _log.debug('Connecting to sqlite database {}'.format(engine_path)) + engine = create_engine(engine_path).connect() + ORMBase.metadata.create_all(engine) + self._db_session = sessionmaker(bind=engine)() + except AttributeError as err: + error_msg = 'Unable to open sqlite database named {}: {}'.format(self.db_path, err) + raise OpenADRInterfaceException(error_msg, None) + return self._db_session + + # ***************** Utility Methods ******************** + + @staticmethod + def payload_element(signature_lxml, signed_object_lxml): + """ + Construct and return an XML element for Payload. + + Append a child Signature element if one is provided. + Append a child SignedObject element. + + @param signature_lxml: (Element or None) Signature element. + @param signed_object_lxml: (Element) SignedObject element. + @return: (Element) Payload element. + """ + payload = etree_.Element("{http://openadr.org/oadr-2.0b/2012/07}oadrPayload", + nsmap=signed_object_lxml.nsmap) + if signature_lxml: + payload.append(signature_lxml) + payload.append(signed_object_lxml) + return payload + + @staticmethod + def calculate_signature(signed_object_xml): + """ + Calculate a digital signature for the SignedObject to be sent to the VTN. + + @param signed_object_xml: (xml string) A SignedObject. + @return: (lxml) A Signature and a SignedObject. + """ + signed_object_lxml = etree_.fromstring(signed_object_xml) + signed_object_lxml.set('Id', 'signedObject') + # Use XMLSigner to create a Signature. + # Use "detached method": the signature lives alonside the signed object in the XML element tree. + # Use c14n "exclusive canonicalization": the signature is independent of namespace inclusion/exclusion. + signer = signxml.XMLSigner(method=signxml.methods.detached, + c14n_algorithm='http://www.w3.org/2001/10/xml-exc-c14n#') + signature_lxml = signer.sign(signed_object_lxml, + key=open(KEY_FILENAME, 'rb').read(), + cert=open(CERT_FILENAME, 'rb').read(), + key_name='123') + # This generated Signature lacks the ReplayProtect property described in OpenADR profile spec section 10.6.3. + return signature_lxml, signed_object_lxml + + def register_endpoints(self): + """ + Register each endpoint URL and its callback. + + These endpoint definitions are used only by "PUSH" style VTN communications, + not by responses to VEN polls. + """ + _log.debug("Registering Endpoints: {}".format(self.__class__.__name__)) + for endpoint in OPENADR_ENDPOINTS.values(): + self.vip.web.register_endpoint(endpoint.url, getattr(self, endpoint.callback), "raw") + + def json_object(self, obj): + """Ensure that an object is valid JSON by dumping it with json_converter and then reloading it.""" + obj_string = jsonapi.dumps(obj, default=self.json_converter) + obj_json = jsonapi.loads(obj_string) + return obj_json + + @staticmethod + def json_converter(object_to_dump): + """When calling jsonapi.dumps, convert datetime instances to strings.""" + if isinstance(object_to_dump, dt): + return object_to_dump.__str__() + + +def main(): + """Start the agent.""" + utils.vip_main(ven_agent, identity='venagent', version=__version__) + + +if __name__ == '__main__': + try: + sys.exit(main()) + except KeyboardInterrupt: + pass diff --git a/volttron/platform/__init__.py b/volttron/platform/__init__.py index 375fdb6f31..d5b0a926d6 100644 --- a/volttron/platform/__init__.py +++ b/volttron/platform/__init__.py @@ -281,17 +281,19 @@ def build_vip_address_string(vip_root, serverkey, publickey, secretkey): :raises ValueError if one of the parameters is None. """ + from volttron.platform.agent.utils import is_auth_enabled + _log.debug("root: {}, serverkey: {}, publickey: {}, secretkey: {}".format( vip_root, serverkey, publickey, secretkey)) parsed = urlparse(vip_root) - if parsed.scheme == 'tcp': + if parsed.scheme == 'tcp' and is_auth_enabled(): if not (serverkey and publickey and secretkey and vip_root): raise ValueError("All parameters must be entered.") root = "{}?serverkey={}&publickey={}&secretkey={}".format( vip_root, serverkey, publickey, secretkey) - elif parsed.scheme == 'ipc': + elif parsed.scheme == 'ipc' or not is_auth_enabled(): root = vip_root else: raise ValueError('Invalid vip root specified!') diff --git a/volttron/platform/agent/base.py b/volttron/platform/agent/base.py index 76da49665f..6ad9b37026 100644 --- a/volttron/platform/agent/base.py +++ b/volttron/platform/agent/base.py @@ -41,7 +41,7 @@ import random -import string +import secrets import time as time_mod import zmq @@ -62,11 +62,9 @@ min_compatible_version = '1' max_compatible_version = '2' -_COOKIE_CHARS = string.ascii_letters + string.digits - -def random_cookie(length=40, choices=_COOKIE_CHARS): - return ''.join(random.choice(choices) for i in range(length)) +def random_cookie(length=40): + return secrets.token_hex(length) def remove_matching(test, items): diff --git a/volttron/platform/agent/base_historian.py b/volttron/platform/agent/base_historian.py index 84ce5ec0a1..34384b7657 100644 --- a/volttron/platform/agent/base_historian.py +++ b/volttron/platform/agent/base_historian.py @@ -2103,11 +2103,10 @@ def query(self, topic=None, start=None, end=None, agg_type=None, raise TypeError("You should provide both aggregation type" "(agg_type) and aggregation time period" "(agg_period) to query aggregate data") - else: - if agg_period: - raise TypeError("You should provide both aggregation type" - "(agg_type) and aggregation time period" - "(agg_period) to query aggregate data") + elif agg_period: + raise TypeError("You should provide both aggregation type" + "(agg_type) and aggregation time period" + "(agg_period) to query aggregate data") if agg_period: agg_period = AggregateHistorian.normalize_aggregation_time_period( diff --git a/volttron/platform/agent/base_weather.py b/volttron/platform/agent/base_weather.py index ac459e16e5..2efabb905b 100644 --- a/volttron/platform/agent/base_weather.py +++ b/volttron/platform/agent/base_weather.py @@ -41,13 +41,14 @@ import csv import sqlite3 import datetime +import os from functools import wraps from abc import abstractmethod from gevent import get_hub from volttron.platform.agent.utils import fix_sqlite3_datetime, \ get_aware_utc_now, format_timestamp, process_timestamp, \ parse_timestamp_string -from volttron.platform.vip.agent import * +from volttron.platform.vip.agent import Agent, RPC, Core from volttron.platform.async_ import AsyncCall from volttron.platform.messaging import headers from volttron.platform.messaging.health import (STATUS_BAD, diff --git a/volttron/platform/agent/utils.py b/volttron/platform/agent/utils.py index 5259b232c4..59fbc694a0 100644 --- a/volttron/platform/agent/utils.py +++ b/volttron/platform/agent/utils.py @@ -35,24 +35,26 @@ # BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY # under Contract DE-AC05-76RL01830 # }}} - """VOLTTRON platformâ„ĸ agent helper classes/functions.""" - import argparse import calendar import errno import logging -import warnings import os - import subprocess import sys +import warnings +from pathlib import Path +from typing import Callable + try: HAS_SYSLOG = True import syslog except ImportError: HAS_SYSLOG = False +import re +import stat import traceback from configparser import ConfigParser from datetime import datetime @@ -60,25 +62,23 @@ import gevent import psutil import pytz -import re -import stat import yaml from dateutil.parser import parse -from dateutil.tz import tzutc, tzoffset +from dateutil.tz import tzoffset, tzutc from tzlocal import get_localzone +from watchdog.events import FileClosedEvent, FileSystemEventHandler from watchdog_gevent import Observer -from volttron.platform import get_home, get_address -from volttron.platform import jsonapi -from volttron.utils import VolttronHomeFileReloader, AbsolutePathFileReloader +from volttron.platform import get_address, get_home, jsonapi +from volttron.utils import AbsolutePathFileReloader, VolttronHomeFileReloader from volttron.utils.prompt import prompt_response - -__all__ = ['load_config', 'run_agent', 'start_agent_thread', - 'is_valid_identity', 'load_platform_config', 'get_messagebus', - 'get_fq_identity', 'execute_command', 'get_aware_utc_now', - 'is_secure_mode', 'is_web_enabled', 'is_auth_enabled', - 'wait_for_volttron_shutdown', 'is_volttron_running'] +__all__ = [ + 'load_config', 'run_agent', 'start_agent_thread', 'is_valid_identity', + 'load_platform_config', 'get_messagebus', 'get_fq_identity', + 'execute_command', 'get_aware_utc_now', 'is_secure_mode', 'is_web_enabled', + 'is_auth_enabled', 'wait_for_volttron_shutdown', 'is_volttron_running' +] __author__ = 'Brandon Carpenter ' __copyright__ = 'Copyright (c) 2016, Battelle Memorial Institute' @@ -146,11 +146,15 @@ def strip_comments(string): def load_config(config_path): """Load a JSON-encoded configuration file.""" if config_path is None: - _log.info("AGENT_CONFIG does not exist in environment. load_config returning empty configuration.") + _log.info( + "AGENT_CONFIG does not exist in environment. load_config returning empty configuration." + ) return {} if not os.path.exists(config_path): - raise ValueError(f"Config file specified by AGENT_CONFIG path {config_path} does not exist.") + raise ValueError( + f"Config file specified by AGENT_CONFIG path {config_path} does not exist." + ) # First attempt parsing the file with a yaml parser (allows comments natively) # Then if that fails we fallback to our modified json parser. @@ -194,7 +198,8 @@ def get_platform_instance_name(vhome=None, prompt=False): if not instance_name: instance_name = 'volttron1' instance_name = prompt_response("Name of this volttron instance:", - mandatory=True, default=instance_name) + mandatory=True, + default=instance_name) else: if not instance_name: _log.warning("Using hostname as instance name.") @@ -237,6 +242,7 @@ def get_messagebus(): message_bus = config.get('message-bus', 'zmq') return message_bus + def is_auth_enabled(): """Get type of message bus - zeromq or rabbbitmq.""" allow_auth = os.environ.get('AUTH_ENABLED') @@ -246,6 +252,7 @@ def is_auth_enabled(): allow_auth = False if allow_auth == 'False' else True return allow_auth + def is_web_enabled(): """Returns True if web enabled, False otherwise""" is_web = os.environ.get('BIND_WEB_ADDRESS') @@ -294,7 +301,7 @@ def store_message_bus_config(message_bus, instance_name): config = ConfigParser() config.read(config_path) config.set('volttron', 'message-bus', message_bus) - config.set('volttron','instance-name', instance_name) + config.set('volttron', 'instance-name', instance_name) with open(config_path, 'w') as configfile: config.write(configfile) else: @@ -354,8 +361,11 @@ def parse_json_config(config_str): return jsonapi.loads(strip_comments(config_str)) -def run_agent(cls, subscribe_address=None, publish_address=None, - config_path=None, **kwargs): +def run_agent(cls, + subscribe_address=None, + publish_address=None, + config_path=None, + **kwargs): """Instantiate an agent and run it in the current thread. Attempts to get keyword parameters from the environment if they @@ -395,8 +405,11 @@ def isapipe(fd): return stat.S_ISFIFO(os.fstat(fd).st_mode) -def default_main(agent_class, description=None, argv=sys.argv, - parser_class=argparse.ArgumentParser, **kwargs): +def default_main(agent_class, + description=None, + argv=sys.argv, + parser_class=argparse.ArgumentParser, + **kwargs): """Default main entry point implementation for legacy agents. description and parser_class are depricated. Please avoid using them. @@ -413,8 +426,8 @@ def default_main(agent_class, description=None, argv=sys.argv, sub_addr = os.environ['AGENT_SUB_ADDR'] pub_addr = os.environ['AGENT_PUB_ADDR'] except KeyError as exc: - sys.stderr.write( - 'missing environment variable: {}\n'.format(exc.args[0])) + sys.stderr.write('missing environment variable: {}\n'.format( + exc.args[0])) sys.exit(1) if sub_addr.startswith('ipc://') and sub_addr[6:7] != '@': if not os.path.exists(sub_addr[6:]): @@ -427,7 +440,8 @@ def default_main(agent_class, description=None, argv=sys.argv, config = os.environ.get('AGENT_CONFIG') agent = agent_class(subscribe_address=sub_addr, publish_address=pub_addr, - config_path=config, **kwargs) + config_path=config, + **kwargs) agent.run() except KeyboardInterrupt: pass @@ -445,7 +459,7 @@ def vip_main(agent_class, identity=None, version='0.1', **kwargs): # Quiet printing of KeyboardInterrupt by greenlets Hub = gevent.hub.Hub - Hub.NOT_ERROR = Hub.NOT_ERROR + (KeyboardInterrupt,) + Hub.NOT_ERROR = Hub.NOT_ERROR + (KeyboardInterrupt, ) config = os.environ.get('AGENT_CONFIG') identity = os.environ.get('AGENT_VIP_IDENTITY', identity) @@ -454,8 +468,7 @@ def vip_main(agent_class, identity=None, version='0.1', **kwargs): if not is_valid_identity(identity): _log.warning('Deprecation warining') _log.warning( - 'All characters in {identity} are not in the valid set.' - .format(idenity=identity)) + f'All characters in {identity} are not in the valid set.') address = get_address() agent_uuid = os.environ.get('AGENT_UUID') @@ -463,12 +476,15 @@ def vip_main(agent_class, identity=None, version='0.1', **kwargs): from volttron.platform.auth.certs import Certs certs = Certs() - agent = agent_class(config_path=config, identity=identity, - address=address, agent_uuid=agent_uuid, + agent = agent_class(config_path=config, + identity=identity, + address=address, + agent_uuid=agent_uuid, volttron_home=volttron_home, version=version, - message_bus=message_bus, **kwargs) - + message_bus=message_bus, + **kwargs) + try: run = agent.run except AttributeError: @@ -485,20 +501,24 @@ def vip_main(agent_class, identity=None, version='0.1', **kwargs): # Keep the ability to have system log output for linux # this will fail on windows because no syslog. if HAS_SYSLOG: + class SyslogFormatter(logging.Formatter): - _level_map = {logging.DEBUG: syslog.LOG_DEBUG, - logging.INFO: syslog.LOG_INFO, - logging.WARNING: syslog.LOG_WARNING, - logging.ERROR: syslog.LOG_ERR, - logging.CRITICAL: syslog.LOG_CRIT} + _level_map = { + logging.DEBUG: syslog.LOG_DEBUG, + logging.INFO: syslog.LOG_INFO, + logging.WARNING: syslog.LOG_WARNING, + logging.ERROR: syslog.LOG_ERR, + logging.CRITICAL: syslog.LOG_CRIT + } def format(self, record): level = self._level_map.get(record.levelno, syslog.LOG_INFO) - return '<{}>'.format(level) + super(SyslogFormatter, self).format( - record) + return '<{}>'.format(level) + super(SyslogFormatter, + self).format(record) class JsonFormatter(logging.Formatter): + def format(self, record): dct = record.__dict__.copy() dct["msg"] = record.getMessage() @@ -510,6 +530,7 @@ def format(self, record): class AgentFormatter(logging.Formatter): + def __init__(self, fmt=None, datefmt=None): if fmt is None: fmt = '%(asctime)s %(composite_name)s %(levelname)s: %(message)s' @@ -517,12 +538,12 @@ def __init__(self, fmt=None, datefmt=None): def composite_name(self, record): if record.name == 'agents.log': - cname = '(%(processName)s %(process)d) %(remote_name)s' + cname = '(%(processName)s %(process)d [%(lineno)d]) %(remote_name)s' elif record.name.startswith('agents.std'): - cname = '(%(processName)s %(process)d) <{}>'.format( + cname = '(%(processName)s %(process)d [%(lineno)d]) <{}>'.format( record.name.split('.', 2)[1]) else: - cname = '() %(name)s' + cname = '() %(name)s [%(lineno)d]' return cname % record.__dict__ def format(self, record): @@ -544,9 +565,10 @@ def setup_logging(level=logging.DEBUG, console=False): handler.setFormatter(JsonFormatter()) elif console: # Below format is more readable for console - handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) + handler.setFormatter( + logging.Formatter('%(levelname)s: %(message)s')) else: - fmt = '%(asctime)s %(name)s %(levelname)s: %(message)s' + fmt = '%(asctime)s %(name)s %(lineno)d %(levelname)s: %(message)s' handler.setFormatter(logging.Formatter(fmt)) if level != logging.DEBUG: # import it here so that when urllib3 imports the requests package, ssl would already got @@ -615,7 +637,8 @@ def parse_timestamp_string(time_stamp_str): try: base_time_stamp_str = time_stamp_str[:26] time_zone_str = time_stamp_str[26:] - time_stamp = datetime.strptime(base_time_stamp_str, "%Y-%m-%dT%H:%M:%S.%f") + time_stamp = datetime.strptime(base_time_stamp_str, + "%Y-%m-%dT%H:%M:%S.%f") # Handle most common case. if time_zone_str == "+00:00": return time_stamp.replace(tzinfo=pytz.UTC) @@ -685,8 +708,9 @@ def process_timestamp(timestamp_string, topic=''): try: timestamp = parse_timestamp_string(timestamp_string) except (ValueError, TypeError): - _log.error("message for {topic} bad timetamp string: {ts_string}" - .format(topic=topic, ts_string=timestamp_string)) + _log.error( + "message for {topic} bad timetamp string: {ts_string}".format( + topic=topic, ts_string=timestamp_string)) return if timestamp.tzinfo is None: @@ -698,21 +722,34 @@ def process_timestamp(timestamp_string, topic=''): return timestamp, original_tz -def watch_file(fullpath, callback): - """Run callback method whenever the file changes - - Not available on OS X/MacOS. +def watch_file(path: str, callback: Callable): + """Run callback method whenever `path` changes. + + If `path` is not rooted the function assumes relative to the $VOLTTRON_HOME + environmental variable + + The watch_file will create a watchdog event handler and will trigger when + the close event happens for writing to the file. + + Not available on OS X/MacOS. """ + file_path = Path(path) + if not file_path.is_absolute(): + file_path = Path(get_home()) / file_path - dirname, filename = os.path.split(fullpath) - _log.info("Adding file watch for %s dirname=%s, filename=%s", fullpath, get_home(), filename) + class Reloader(FileSystemEventHandler): + + def on_closed(self, event): + """ Only called after a write to file has been closed + """ + callback() + + _log.debug(f"Watch file added for filename {file_path}") observer = Observer() - observer.schedule( - VolttronHomeFileReloader(filename, callback), - path=get_home() - ) + + observer.schedule(Reloader(), str(file_path)) observer.start() - _log.info("Added file watch for %s", fullpath) + _log.debug("Added file watch for %s", path) def watch_file_with_fullpath(fullpath, callback): @@ -723,10 +760,7 @@ def watch_file_with_fullpath(fullpath, callback): dirname, filename = os.path.split(fullpath) _log.info("Adding file watch for %s", fullpath) _observer = Observer() - _observer.schedule( - AbsolutePathFileReloader(fullpath, callback), - dirname - ) + _observer.schedule(AbsolutePathFileReloader(fullpath, callback), dirname) _log.info("Added file watch for %s", fullpath) _observer.start() @@ -751,7 +785,8 @@ def create_file_if_missing(path, permission=0o660, contents=None): success = False try: if contents: - contents = contents if isinstance(contents, bytes) else contents.encode("utf-8") + contents = contents if isinstance( + contents, bytes) else contents.encode("utf-8") os.write(fd, contents) success = True except Exception as e: @@ -777,11 +812,17 @@ def fix_sqlite3_datetime(sql=None): def parse(time_stamp_bytes): return parse_timestamp_string(time_stamp_bytes.decode("utf-8")) + sql.register_adapter(datetime, format_timestamp) sql.register_converter("timestamp", parse) -def execute_command(cmds, env=None, cwd=None, logger=None, err_prefix=None, use_shell=False) -> str: +def execute_command(cmds, + env=None, + cwd=None, + logger=None, + err_prefix=None, + use_shell=False) -> str: """ Executes a command as a subprocess If the return code of the call is 0 then return stdout otherwise @@ -798,8 +839,12 @@ def execute_command(cmds, env=None, cwd=None, logger=None, err_prefix=None, use_ :raises RuntimeError: if the return code is not 0 from suprocess.run """ - results = subprocess.run(cmds, env=env, cwd=cwd, - stderr=subprocess.PIPE, stdout=subprocess.PIPE, shell=use_shell) + results = subprocess.run(cmds, + env=env, + cwd=cwd, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + shell=use_shell) if results.returncode != 0: err_prefix = err_prefix if err_prefix is not None else "Error executing command" err_message = "\n{}: Below Command failed with non zero exit code.\n" \ @@ -814,6 +859,7 @@ def execute_command(cmds, env=None, cwd=None, logger=None, err_prefix=None, use_ return results.stdout.decode('utf-8') + # # def execute_command_p(cmds, env=None, cwd=None, logger=None, err_prefix=None): # """ Executes a given command using a subprocess. @@ -876,7 +922,9 @@ def wait_for_volttron_startup(vhome, timeout): gevent.sleep(3) sleep_time += 3 if sleep_time >= timeout: - raise Exception("Platform startup failed. Please check volttron.log in {}".format(vhome)) + raise Exception( + "Platform startup failed. Please check volttron.log in {}".format( + vhome)) def wait_for_volttron_shutdown(vhome, timeout): @@ -886,4 +934,6 @@ def wait_for_volttron_shutdown(vhome, timeout): gevent.sleep(1) sleep_time += 1 if sleep_time >= timeout: - raise Exception("Platform shutdown failed. Please check volttron.cfg.log in {}".format(vhome)) + raise Exception( + "Platform shutdown failed. Please check volttron.cfg.log in {}". + format(vhome)) diff --git a/volttron/platform/aip.py b/volttron/platform/aip.py index 0af5fb7f7c..2290c3a7fd 100644 --- a/volttron/platform/aip.py +++ b/volttron/platform/aip.py @@ -66,8 +66,7 @@ get_messagebus, get_platform_instance_name) from volttron.platform import get_home -from volttron.platform.agent.utils import load_platform_config, \ - get_utc_seconds_from_epoch +from volttron.platform.agent.utils import get_utc_seconds_from_epoch from volttron.platform.packages import UnpackedPackage from volttron.platform.vip.agent import Agent from volttron.platform.auth.auth_entry import AuthEntry diff --git a/volttron/platform/auth/auth.py b/volttron/platform/auth/auth.py index d837da565e..a8b2535edf 100644 --- a/volttron/platform/auth/auth.py +++ b/volttron/platform/auth/auth.py @@ -36,45 +36,31 @@ # under Contract DE-AC05-76RL01830 # }}} - +import copy import logging import os -import copy import gevent import gevent.core from gevent.fileobject import FileObject -from volttron.platform.agent.known_identities import ( - CONTROL_CONNECTION, - PROCESS_IDENTITIES, -) -from volttron.platform.auth.auth_utils import load_user +from volttron.platform.agent.known_identities import CONTROL_CONNECTION, PROCESS_IDENTITIES +from volttron.platform.agent.utils import create_file_if_missing, get_messagebus, watch_file from volttron.platform.auth.auth_entry import AuthEntry from volttron.platform.auth.auth_file import AuthFile +from volttron.platform.auth.auth_utils import load_user from volttron.platform.jsonrpc import RemoteError +from volttron.platform.vip.agent import RPC, Agent, Core from volttron.platform.vip.agent.errors import Unreachable from volttron.platform.vip.pubsubservice import ProtectedPubSubTopics -from volttron.platform.agent.utils import ( - create_file_if_missing, - watch_file, - get_messagebus, -) -from volttron.platform.vip.agent import Agent, Core, RPC _log = logging.getLogger(__name__) class AuthService(Agent): - def __init__( - self, - auth_file, - protected_topics_file, - setup_mode, - aip, - *args, - **kwargs - ): + + def __init__(self, auth_file, protected_topics_file, setup_mode, aip, + *args, **kwargs): """Initializes AuthService, and prepares AuthFile.""" self.allow_any = kwargs.pop("allow_any", False) self.is_zap_required = kwargs.pop('zap_required', True) @@ -96,8 +82,7 @@ def __init__( self._is_connected = False self._protected_topics_file = protected_topics_file self._protected_topics_file_path = os.path.abspath( - protected_topics_file - ) + protected_topics_file) self._protected_topics = {} self._protected_topics_for_rmq = ProtectedPubSubTopics() self._setup_mode = setup_mode @@ -137,28 +122,23 @@ def auth_file_update_by_index(auth_entry, index, is_allow=True): :params: auth_entry, index, is_allow :return: None """ - self.auth_file.update_by_index( - AuthEntry(**auth_entry), index, is_allow - ) + self.auth_file.update_by_index(AuthEntry(**auth_entry), index, + is_allow) self.vip.rpc.export(auth_file_read, "auth_file.read") - self.vip.rpc.export( - self.auth_file.find_by_credentials, "auth_file.find_by_credentials" - ) + self.vip.rpc.export(self.auth_file.find_by_credentials, + "auth_file.find_by_credentials") self.vip.rpc.export(auth_file_add, "auth_file.add") - self.vip.rpc.export( - auth_file_update_by_index, "auth_file.update_by_index" - ) + self.vip.rpc.export(auth_file_update_by_index, + "auth_file.update_by_index") self.vip.rpc.export( self.auth_file.remove_by_credentials, "auth_file.remove_by_credentials", ) - self.vip.rpc.export( - self.auth_file.remove_by_index, "auth_file.remove_by_index" - ) - self.vip.rpc.export( - self.auth_file.remove_by_indices, "auth_file.remove_by_indices" - ) + self.vip.rpc.export(self.auth_file.remove_by_index, + "auth_file.remove_by_index") + self.vip.rpc.export(self.auth_file.remove_by_indices, + "auth_file.remove_by_indices") self.vip.rpc.export(self.auth_file.set_groups, "auth_file.set_groups") self.vip.rpc.export(self.auth_file.set_roles, "auth_file.set_roles") @@ -169,11 +149,13 @@ def setup_authentication_server(self, sender, **kwargs): self.read_auth_file() if get_messagebus() == "zmq": from volttron.platform.auth.auth_protocols.auth_zmq import ZMQAuthorization, ZMQServerAuthentication - self.authentication_server = ZMQServerAuthentication(auth_service=self) + self.authentication_server = ZMQServerAuthentication( + auth_service=self) self.authorization_server = ZMQAuthorization(auth_service=self) else: from volttron.platform.auth.auth_protocols.auth_rmq import RMQAuthorization, RMQServerAuthentication - self.authentication_server = RMQServerAuthentication(auth_service=self) + self.authentication_server = RMQServerAuthentication( + auth_service=self) self.authorization_server = RMQAuthorization(auth_service=self) self._read_protected_topics_file() self.core.spawn(watch_file, self.auth_file_path, self.read_auth_file) @@ -186,7 +168,8 @@ def setup_authentication_server(self, sender, **kwargs): @Core.receiver("onstart") def start_authentication_server(self, sender, **kwargs): - self.authentication_server.handle_authentication(self._protected_topics) + self.authentication_server.handle_authentication( + self._protected_topics) @Core.receiver("onstop") def stop_authentication_server(self, sender, **kwargs): @@ -220,27 +203,22 @@ def update_id_rpc_authorizations(self, identity, rpc_methods): # Create it and set it to have the provided # rpc capabilities entry.rpc_method_authorizations[method] = rpc_methods[ - method - ] + method] is_updated = True # Check if the rpc method does not have any # rpc capabilities if not entry.rpc_method_authorizations[method]: # Set it to have the provided rpc capabilities entry.rpc_method_authorizations[method] = rpc_methods[ - method - ] + method] is_updated = True # Check if the rpc method's capabilities match # what have been provided - if ( - entry.rpc_method_authorizations[method] - != rpc_methods[method] - ): + if (entry.rpc_method_authorizations[method] + != rpc_methods[method]): # Update rpc_methods based on auth entries updated_rpc_methods[ - method - ] = entry.rpc_method_authorizations[method] + method] = entry.rpc_method_authorizations[method] # Update auth file if changed and return rpc_methods if is_updated: self.auth_file.update_by_index(entry, entries.index(entry)) @@ -256,14 +234,11 @@ def get_entry_authorizations(self, identity): rpc_method_authorizations = {} try: rpc_method_authorizations = self.vip.rpc.call( - identity, "auth.get_all_rpc_authorizations" - ).get() + identity, "auth.get_all_rpc_authorizations").get() _log.debug(f"RPC Methods are: {rpc_method_authorizations}") except Unreachable: - _log.warning( - f"{identity} " - f"is unreachable while attempting to get rpc methods" - ) + _log.warning(f"{identity} " + f"is unreachable while attempting to get rpc methods") return rpc_method_authorizations @@ -276,11 +251,9 @@ def update_rpc_authorizations(self, entries): """ for entry in entries: # Skip if core agent - if ( - entry.identity is not None + if (entry.identity is not None and entry.identity not in PROCESS_IDENTITIES - and entry.identity != CONTROL_CONNECTION - ): + and entry.identity != CONTROL_CONNECTION): # Collect all modified methods modified_methods = {} for method in entry.rpc_method_authorizations: @@ -291,8 +264,7 @@ def update_rpc_authorizations(self, entries): # if no capabilities in auth file continue modified_methods[method] = entry.rpc_method_authorizations[ - method - ] + method] if modified_methods: method_error = True try: @@ -303,11 +275,9 @@ def update_rpc_authorizations(self, entries): ).wait(timeout=4) method_error = False except gevent.Timeout: - _log.error( - f"{entry.identity} " - f"has timed out while attempting " - f"to update rpc_method_authorizations" - ) + _log.error(f"{entry.identity} " + f"has timed out while attempting " + f"to update rpc_method_authorizations") method_error = False except RemoteError: method_error = True @@ -320,18 +290,14 @@ def update_rpc_authorizations(self, entries): entry.identity, "auth.set_rpc_authorizations", method_str=method, - capabilities= - entry.rpc_method_authorizations[ - method - ], + capabilities=entry. + rpc_method_authorizations[method], ) except gevent.Timeout: - _log.error( - f"{entry.identity} " - f"has timed out while attempting " - f"to update " - f"rpc_method_authorizations" - ) + _log.error(f"{entry.identity} " + f"has timed out while attempting " + f"to update " + f"rpc_method_authorizations") except RemoteError: _log.error(f"Method {method} does not exist.") @@ -357,27 +323,19 @@ def add_rpc_authorizations(self, identity, method, authorizations): elif not entry.rpc_method_authorizations[method]: entry.rpc_method_authorizations[method] = authorizations else: - entry.rpc_method_authorizations[method].extend( - [ - rpc_auth - for rpc_auth in authorizations - if rpc_auth in authorizations - and rpc_auth - not in entry.rpc_method_authorizations[method] - ] - ) + entry.rpc_method_authorizations[method].extend([ + rpc_auth for rpc_auth in authorizations + if rpc_auth in authorizations and rpc_auth not in + entry.rpc_method_authorizations[method] + ]) self.auth_file.update_by_index(entry, entries.index(entry)) return _log.error("Agent identity not found in auth file!") return @RPC.export - def delete_rpc_authorizations( - self, - identity, - method, - denied_authorizations - ): + def delete_rpc_authorizations(self, identity, method, + denied_authorizations): """ Removes authorizations to method in auth entry in auth file. @@ -396,44 +354,34 @@ def delete_rpc_authorizations( if method not in entry.rpc_method_authorizations: _log.error( f"{entry.identity} does not have a method called " - f"{method}" - ) + f"{method}") elif not entry.rpc_method_authorizations[method]: - _log.error( - f"{entry.identity}.{method} does not have any " - f"authorized capabilities." - ) + _log.error(f"{entry.identity}.{method} does not have any " + f"authorized capabilities.") else: any_match = False for rpc_auth in denied_authorizations: - if ( - rpc_auth - not in entry.rpc_method_authorizations[method] - ): + if (rpc_auth not in + entry.rpc_method_authorizations[method]): _log.error( f"{rpc_auth} is not an authorized capability " - f"for {method}" - ) + f"for {method}") else: any_match = True if any_match: entry.rpc_method_authorizations[method] = [ - rpc_auth - for rpc_auth in entry.rpc_method_authorizations[ - method - ] + rpc_auth for rpc_auth in + entry.rpc_method_authorizations[method] if rpc_auth not in denied_authorizations ] if not entry.rpc_method_authorizations[method]: entry.rpc_method_authorizations[method] = [""] - self.auth_file.update_by_index( - entry, entries.index(entry) - ) + self.auth_file.update_by_index(entry, + entries.index(entry)) else: _log.error( f"No matching authorized capabilities provided " - f"for {method}" - ) + f"for {method}") return _log.error("Agent identity not found in auth file!") return @@ -441,16 +389,14 @@ def delete_rpc_authorizations( def _update_auth_lists(self, entries, is_allow=True): auth_list = [] for entry in entries: - auth_list.append( - { - "domain": entry.domain, - "address": entry.address, - "mechanism": entry.mechanism, - "credentials": entry.credentials, - "user_id": entry.user_id, - "retries": 0, - } - ) + auth_list.append({ + "domain": entry.domain, + "address": entry.address, + "mechanism": entry.mechanism, + "credentials": entry.credentials, + "user_id": entry.user_id, + "retries": 0, + }) if is_allow: self._auth_approved = [ entry for entry in auth_list if entry["address"] is not None @@ -472,25 +418,21 @@ def _get_updated_entries(self, old_entries, new_entries): """ modified_entries = [] for entry in new_entries: - if ( - entry.identity is not None + if (entry.identity is not None and entry.identity not in PROCESS_IDENTITIES - and entry.identity != CONTROL_CONNECTION - ): + and entry.identity != CONTROL_CONNECTION): for old_entry in old_entries: if entry.identity == old_entry.identity: - if ( - entry.rpc_method_authorizations - != old_entry.rpc_method_authorizations - ): + if (entry.rpc_method_authorizations + != old_entry.rpc_method_authorizations): modified_entries.append(entry) else: pass else: pass if entry.identity not in [ - old_entry.identity for old_entry in old_entries + old_entry.identity for old_entry in old_entries ]: modified_entries.append(entry) else: @@ -498,7 +440,7 @@ def _get_updated_entries(self, old_entries, new_entries): return modified_entries def read_auth_file(self): - _log.info("loading auth file %s", self.auth_file_path) + _log.debug("loading auth file %s", self.auth_file_path) # Update from auth file into memory if self.auth_file.auth_data: old_entries = self.auth_file.read_allow_entries().copy() @@ -531,12 +473,9 @@ def read_auth_file(self): gevent.sleep(2) self._send_update(modified_entries) except BaseException as err: - _log.error( - "Exception sending auth updates to peer. %r", - err - ) + _log.error("Exception sending auth updates to peer. %r", err) raise err - _log.info("auth file %s loaded", self.auth_file_path) + _log.debug("auth file %s loaded", self.auth_file_path) def get_protected_topics(self): protected = self._protected_topics @@ -549,7 +488,8 @@ def _read_protected_topics_file(self): with open(self._protected_topics_file) as fil: # Use gevent FileObject to avoid blocking the thread data = FileObject(fil, close=False).read() - self._protected_topics = self.authorization_server.load_protected_topics(data) + self._protected_topics = self.authorization_server.load_protected_topics( + data) except Exception: _log.exception("error loading %s", self._protected_topics_file) @@ -576,7 +516,8 @@ def _send_update(self, modified_entries=None): peers = self.vip.peerlist().get(timeout=0.5) except BaseException as err: _log.warning( - "Attempt %i to get peerlist failed with " "exception %s", + "Attempt %i to get peerlist failed with " + "exception %s", i, err, ) @@ -596,12 +537,12 @@ def _send_update(self, modified_entries=None): # Update RPC method authorizations on agents if modified_entries: try: - gevent.spawn( - self.update_rpc_authorizations, modified_entries - ).join(timeout=15) + gevent.spawn(self.update_rpc_authorizations, + modified_entries).join(timeout=15) except gevent.Timeout: _log.error("Timed out updating methods from auth file!") - self.authorization_server.update_user_capabilites(self.get_user_to_capabilities()) + self.authorization_server.update_user_capabilites( + self.get_user_to_capabilities()) @RPC.export def get_user_to_capabilities(self): diff --git a/volttron/platform/auth/auth_entry.py b/volttron/platform/auth/auth_entry.py index 39c086d81e..59399d54da 100644 --- a/volttron/platform/auth/auth_entry.py +++ b/volttron/platform/auth/auth_entry.py @@ -39,7 +39,7 @@ import logging import re -from typing import Optional +from typing import Optional, Union import uuid from volttron.platform.vip.socket import BASE64_ENCODED_CURVE_KEY_LEN @@ -114,7 +114,7 @@ def __init__( identity=None, groups=None, roles=None, - capabilities: Optional[dict] = None, + capabilities=None, rpc_method_authorizations=None, comments=None, enabled=True, @@ -127,13 +127,10 @@ def __init__( self.credentials = AuthEntry._build_field(credentials) self.groups = AuthEntry._build_field(groups) or [] self.roles = AuthEntry._build_field(roles) or [] - self.capabilities = ( - AuthEntry.build_capabilities_field(capabilities) or {} - ) - self.rpc_method_authorizations = ( - AuthEntry.build_rpc_authorizations_field(rpc_method_authorizations) - or {} - ) + self.capabilities = AuthEntry.build_capabilities_field(capabilities) or {} + + self.rpc_method_authorizations = AuthEntry.build_rpc_authorizations_field(rpc_method_authorizations) or {} + self.comments = AuthEntry._build_field(comments) if user_id is None: user_id = str(uuid.uuid4()) @@ -165,7 +162,7 @@ def _build_field(value): return List(String(elem) for elem in value) @staticmethod - def build_capabilities_field(value: Optional[dict]): + def build_capabilities_field(value): # _log.debug("_build_capabilities {}".format(value)) if not value: diff --git a/volttron/platform/auth/auth_file.py b/volttron/platform/auth/auth_file.py index 2ac86f799d..d39b63be26 100644 --- a/volttron/platform/auth/auth_file.py +++ b/volttron/platform/auth/auth_file.py @@ -62,6 +62,7 @@ _log = logging.getLogger(__name__) + class AuthFile(object): def __init__(self, auth_file=None): self.auth_data = {} @@ -74,7 +75,7 @@ def __init__(self, auth_file=None): @property def version(self): - return {"major": 1, "minor": 3} + return {"major": 1, "minor": 4} def _check_for_upgrade(self): auth_data = self._read() @@ -268,6 +269,11 @@ def upgrade_1_2_to_1_3(allow_list): version["minor"] = 2 if version["major"] == 1 and version["minor"] == 2: allow_list = upgrade_1_2_to_1_3(allow_list) + version["minor"] = 3 + if version["major"] == 1 and version["minor"] == 3: + # on start a new entry for config.store should have got created automatically + # so just update version + version["minor"] = 4 allow_entries, deny_entries = self._get_entries(allow_list, deny_list) self._write(allow_entries, deny_entries, groups, roles) diff --git a/volttron/platform/auth/auth_protocols/auth_protocol.py b/volttron/platform/auth/auth_protocols/auth_protocol.py index 7de07a7814..63dc48f083 100644 --- a/volttron/platform/auth/auth_protocols/auth_protocol.py +++ b/volttron/platform/auth/auth_protocols/auth_protocol.py @@ -36,7 +36,6 @@ # under Contract DE-AC05-76RL01830 # }}} -import os import volttron.platform from volttron.platform import jsonapi diff --git a/volttron/platform/auth/auth_protocols/auth_rmq.py b/volttron/platform/auth/auth_protocols/auth_rmq.py index c178c6ea39..17f4efd0ff 100644 --- a/volttron/platform/auth/auth_protocols/auth_rmq.py +++ b/volttron/platform/auth/auth_protocols/auth_rmq.py @@ -3,19 +3,16 @@ import ssl import re import logging -import grequests from collections import defaultdict from urllib.parse import urlparse, urlsplit from dataclasses import dataclass from volttron.platform.auth import certs -from volttron.platform.auth.auth_protocols import * +from volttron.platform.auth.auth_protocols import ( + BaseAuthentication, BaseClientAuthorization, BaseServerAuthentication, BaseServerAuthorization) from volttron.platform.parameters import Parameters -from volttron.utils.rmq_config_params import RMQConfig from volttron.utils.rmq_mgmt import RabbitMQMgmt from volttron.platform import jsonapi -from volttron.platform.agent.utils import get_fq_identity, get_platform_instance_name -from volttron.platform.messaging.health import STATUS_BAD -from volttron.platform import get_home +from volttron.platform.agent.utils import get_fq_identity from volttron.platform import is_rabbitmq_available @@ -119,9 +116,7 @@ def build_remote_connection_param(self, cert_dir=None, retry_attempt=30, retry_d :param retry_attempt: pika connection parameter - number of connection retry attempts :return: instance of pika.ConnectionParameters """ - from urllib import parse - - parsed_addr = parse.urlparse(self.params.url_address) + parsed_addr = urlparse(self.params.url_address) _, virtual_host = parsed_addr.path.split('/') try: diff --git a/volttron/platform/auth/auth_protocols/auth_zmq.py b/volttron/platform/auth/auth_protocols/auth_zmq.py index dda4edf067..927b1cef93 100644 --- a/volttron/platform/auth/auth_protocols/auth_zmq.py +++ b/volttron/platform/auth/auth_protocols/auth_zmq.py @@ -42,23 +42,21 @@ import random import uuid import bisect -from urllib.parse import urlsplit, parse_qs, urlunsplit, urlparse +from urllib.parse import urlsplit, parse_qs, urlunsplit import gevent import gevent.time from zmq import green as zmq -from volttron.platform.auth.auth_protocols import * from volttron.platform import get_home from volttron.platform import jsonapi from volttron.platform.auth.auth_entry import AuthEntry from volttron.platform.auth.auth_exception import AuthException +from volttron.platform.auth.auth_protocols import ( + BaseAuthentication, BaseClientAuthorization, BaseServerAuthentication, BaseServerAuthorization) from volttron.platform.auth.auth_utils import dump_user from volttron.platform.keystore import KeyStore, KnownHostsStore from volttron.platform.parameters import Parameters from volttron.platform.vip.socket import encode_key -from volttron.platform.agent.utils import ( - get_platform_instance_name, - get_fq_identity, -) + _log = logging.getLogger(__name__) diff --git a/volttron/platform/auth/certs.py b/volttron/platform/auth/certs.py index ce47416e68..adc10a4984 100644 --- a/volttron/platform/auth/certs.py +++ b/volttron/platform/auth/certs.py @@ -43,7 +43,7 @@ import six import time from shutil import copyfile -from socket import gethostname, getfqdn, getaddrinfo, AI_CANONNAME +from socket import gethostname, getfqdn import subprocess from cryptography import x509 @@ -58,7 +58,6 @@ from volttron.platform import jsonapi from volttron.platform import get_home from volttron.platform.agent.utils import (get_platform_instance_name, - get_fq_identity, execute_command) _log = logging.getLogger(__name__) diff --git a/volttron/platform/control/control_config.py b/volttron/platform/control/control_config.py index 5a8c028101..3518ac93e4 100644 --- a/volttron/platform/control/control_config.py +++ b/volttron/platform/control/control_config.py @@ -57,7 +57,7 @@ def add_config_to_store(opts): file_contents = opts.infile.read() call( - "manage_store", + "set_config", opts.identity, opts.name, file_contents, @@ -69,7 +69,7 @@ def delete_config_from_store(opts): opts.connection.peer = CONFIGURATION_STORE call = opts.connection.call if opts.delete_store: - call("manage_delete_store", opts.identity) + call("delete_store", opts.identity) return if opts.name is None: @@ -79,7 +79,7 @@ def delete_config_from_store(opts): ) return - call("manage_delete_config", opts.identity, opts.name) + call("delete_config", opts.identity, opts.name) def list_store(opts): @@ -87,9 +87,9 @@ def list_store(opts): call = opts.connection.call results = [] if opts.identity is None: - results = call("manage_list_stores") + results = call("list_stores") else: - results = call("manage_list_configs", opts.identity) + results = call("list_configs", opts.identity) for item in results: _stdout.write(item + "\n") @@ -98,7 +98,7 @@ def list_store(opts): def get_config(opts): opts.connection.peer = CONFIGURATION_STORE call = opts.connection.call - results = call("manage_get", opts.identity, opts.name, raw=opts.raw) + results = call("get_config", opts.identity, opts.name, raw=opts.raw) if opts.raw: _stdout.write(results) @@ -119,7 +119,7 @@ def edit_config(opts): raw_data = "" else: try: - results = call("manage_get_metadata", opts.identity, opts.name) + results = call("get_metadata", opts.identity, opts.name) config_type = results["type"] raw_data = results["data"] except RemoteError as e: @@ -159,7 +159,7 @@ def edit_config(opts): return call( - "manage_store", + "set_config", opts.identity, opts.name, new_raw_data, diff --git a/volttron/platform/control/control_utils.py b/volttron/platform/control/control_utils.py index 232222ff5e..a4300e6603 100644 --- a/volttron/platform/control/control_utils.py +++ b/volttron/platform/control/control_utils.py @@ -36,7 +36,7 @@ # under Contract DE-AC05-76RL01830 # }}} import collections -import os +import itertools import sys import re from volttron.platform import jsonapi @@ -48,13 +48,10 @@ def _calc_min_uuid_length(agents): n = 0 - for agent1 in agents: - for agent2 in agents: - if agent1 is agent2: - continue - common_len = len(os.path.commonprefix([agent1.uuid, agent2.uuid])) - if common_len > n: - n = common_len + for agent1, agent2 in itertools.combinations(agents, 2): + common_len = sum(1 for a, b in zip(agent1.uuid, agent2.uuid) if a == b) + if common_len > n: + n = common_len return n + 1 diff --git a/volttron/platform/dbutils/postgresqlfuncts.py b/volttron/platform/dbutils/postgresqlfuncts.py index 55ceb74f35..8ce1ca2cca 100644 --- a/volttron/platform/dbutils/postgresqlfuncts.py +++ b/volttron/platform/dbutils/postgresqlfuncts.py @@ -45,6 +45,7 @@ """ class PostgreSqlFuncts(DbDriver): def __init__(self, connect_params, table_names): + self.db_name = connect_params.get('dbname') if table_names: self.data_table = table_names['data_table'] self.topics_table = table_names['topics_table'] @@ -147,7 +148,7 @@ def rollback(self): def setup_historian_tables(self): rows = self.select(f"""SELECT table_name FROM information_schema.tables - WHERE table_catalog = 'test_historian' and table_schema = 'public' + WHERE table_catalog = '{self.db_name}' and table_schema = 'public' AND table_name = '{self.data_table}'""") if rows: _log.debug("Found table {}. Historian table exists".format( @@ -350,7 +351,17 @@ def get_topic_meta_map(self): 'SELECT topic_id, metadata ' 'FROM {}').format(Identifier(self.meta_table)) rows = self.select(query) - meta_map = {tid: jsonapi.loads(meta) if meta else None for tid, meta in rows} + + meta_map = {} + for tid, meta in rows: + if meta: + if isinstance(meta, dict): + meta_map[tid] = meta + else: + meta_map[tid] = jsonapi.loads(meta) + else: + meta_map[tid] = None + return meta_map def get_agg_topics(self): diff --git a/volttron/platform/instance_setup.py b/volttron/platform/instance_setup.py index 984e696023..d8e91a3de1 100644 --- a/volttron/platform/instance_setup.py +++ b/volttron/platform/instance_setup.py @@ -155,7 +155,7 @@ def _is_bound_already(address): return already_bound -def fail_if_instance_running(): +def fail_if_instance_running(args): home = get_home() @@ -356,11 +356,10 @@ def _check_dependencies_met(requirement): pass else: return False + elif dependency.split("==")[0] in [r.split("==")[0] for r in current_dependencies]: + pass else: - if dependency.split("==")[0] in [r.split("==")[0] for r in current_dependencies]: - pass - else: - return False + return False return True @@ -376,15 +375,6 @@ def set_dependencies(requirement): subprocess.check_call(cmds) return - -def set_dependencies_rmq(): - install_rabbit(default_rmq_dir) - prompt = 'What OS are you running?' - user_os = prompt_response(prompt, default='debian') - prompt = 'Which distribution are you running?' - user_dist = prompt_response(prompt, default='bionic') - _cmd(["./scripts/rabbit_dependencies.sh", user_os, user_dist]) - def _create_web_certs(): global config_opts """ @@ -454,12 +444,6 @@ def do_message_bus(): print("Message type is not valid. Valid entries are zmq or rmq.") if bus_type == 'rmq': - if not is_rabbitmq_available(): - print("RabbitMQ has not been set up!") - print("Setting up now...") - set_dependencies_rmq() - print("Done!") - try: check_rmq_setup() except AssertionError: @@ -1111,7 +1095,7 @@ def main(): set_home(args.vhome) prompt_vhome = False # if not args.rabbitmq or args.rabbitmq[0] in ["single"]: - fail_if_instance_running() + fail_if_instance_running(args) fail_if_not_in_src_root() if use_active in n: atexit.register(_cleanup_on_exit) diff --git a/volttron/platform/lib/inotify/green.py b/volttron/platform/lib/inotify/green.py index 01047f6d57..e2d6b76ef7 100644 --- a/volttron/platform/lib/inotify/green.py +++ b/volttron/platform/lib/inotify/green.py @@ -44,8 +44,7 @@ import gevent from gevent.select import select -from . import _inotify, __all__, _main -from . import * +from . import IN_NONBLOCK, _inotify, _main import logging _log = logging.getLogger(__name__) diff --git a/volttron/platform/main.py b/volttron/platform/main.py index e3b8f13482..ed3b62415b 100644 --- a/volttron/platform/main.py +++ b/volttron/platform/main.py @@ -53,74 +53,71 @@ fn() import argparse -import errno import logging -from logging import handlers import logging.config -from typing import Optional -from urllib.parse import urlparse - import os import resource import stat import struct +import subprocess import sys import threading import uuid +from logging import handlers +from typing import Optional +from urllib.parse import urlparse import gevent +import zmq +from zmq import ZMQError, green, NOBLOCK +from volttron.platform.agent.utils import get_platform_instance_name +# Create a context common to the green and non-green zmq modules. +from volttron.platform.instance_setup import _update_config_file from volttron.platform.vip.healthservice import HealthService from volttron.platform.vip.servicepeer import ServicePeerNotifier from volttron.utils import get_random_key from volttron.utils.frame_serialization import deserialize_frames, serialize_frames -import zmq -from zmq import ZMQError -from zmq import green -import subprocess - -# Create a context common to the green and non-green zmq modules. -from volttron.platform.instance_setup import _update_config_file -from volttron.platform.agent.utils import get_platform_instance_name -green.Context._instance = green.Context.shadow(zmq.Context.instance().underlying) -from volttron.platform import jsonapi - -from volttron.platform import aip -from volttron.platform import __version__ -from volttron.platform import config - -from volttron.platform.vip.router import * -from volttron.platform.vip.socket import decode_key, encode_key, Address -from volttron.platform.vip.tracking import Tracker +green.Context._instance = green.Context.shadow( + zmq.Context.instance().underlying) +from volttron.platform import __version__, aip, config, jsonapi from volttron.platform.auth.auth import AuthService -from volttron.platform.auth.auth_file import AuthFile from volttron.platform.auth.auth_entry import AuthEntry +from volttron.platform.auth.auth_file import AuthFile from volttron.platform.control.control import ControlService +from volttron.platform.vip.router import BaseRouter, ERROR, INCOMING, UNROUTABLE +from volttron.platform.vip.socket import Address, decode_key, encode_key +from volttron.platform.vip.tracking import Tracker + try: from .web import PlatformWebService HAS_WEB = True except ImportError: HAS_WEB = False -from .store import ConfigStoreService +from zmq import green as _green + +from volttron.platform import is_rabbitmq_available +from volttron.platform.agent.utils import store_message_bus_config +from volttron.platform.vip.proxy_zmq_router import ZMQProxyRouter + +from ..utils.persistance import load_create_store from .agent import utils -from .agent.known_identities import PLATFORM_WEB, CONFIGURATION_STORE, AUTH, CONTROL, CONTROL_CONNECTION, \ - PLATFORM_HEALTH, KEY_DISCOVERY, PROXY_ROUTER, PLATFORM -from .vip.agent.subsystems.pubsub import ProtectedPubSubTopics +from .agent.known_identities import (AUTH, CONFIGURATION_STORE, CONTROL, + CONTROL_CONNECTION, KEY_DISCOVERY, + PLATFORM, PLATFORM_HEALTH, PLATFORM_WEB, + PROXY_ROUTER) from .keystore import KeyStore, KnownHostsStore -from .vip.pubsubservice import PubSubService -from .vip.routingservice import RoutingService +from .store import ConfigStoreService from .vip.externalrpcservice import ExternalRPCService from .vip.keydiscovery import KeyDiscoveryAgent -from ..utils.persistance import load_create_store +from .vip.pubsubservice import PubSubService from .vip.rmq_router import RMQRouter -from volttron.platform.agent.utils import store_message_bus_config -from zmq import green as _green -from volttron.platform.vip.proxy_zmq_router import ZMQProxyRouter -from volttron.platform import is_rabbitmq_available +from .vip.routingservice import RoutingService + if is_rabbitmq_available(): - from volttron.utils.rmq_setup import start_rabbit from volttron.utils.rmq_config_params import RMQConfig + from volttron.utils.rmq_setup import start_rabbit try: import volttron.restricted @@ -131,14 +128,13 @@ HAVE_RESTRICTED = True -_log = logging.getLogger(os.path.basename(sys.argv[0]) - if __name__ == '__main__' else __name__) +_log = logging.getLogger( + os.path.basename(sys.argv[0]) if __name__ == '__main__' else __name__) # Only show debug on the platform when really necessary! log_level_info = ( #'volttron.platform.main', 'volttron.platform.vip.zmq_connection', - 'urllib3.connectionpool', 'watchdog.observers.inotify_buffer', 'volttron.platform.auth', 'volttron.platform.store', @@ -151,11 +147,13 @@ for log_name in log_level_info: logging.getLogger(log_name).setLevel(logging.INFO) +logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING) VOLTTRON_INSTANCES = '~/.volttron_instances' -def log_to_file(file_, level=logging.WARNING, +def log_to_file(file_, + level=logging.WARNING, handler_class=logging.StreamHandler): '''Direct log output to a file (or something like one).''' handler = handler_class(file_) @@ -232,9 +230,9 @@ def configure_logging(conf_path): import yaml except ImportError: return (conf_path, 'PyYAML must be installed before ' - 'loading logging configuration from a YAML file.') + 'loading logging configuration from a YAML file.') try: - conf_dict = yaml.load(conf_file) + conf_dict = yaml.safe_load(conf_file) except yaml.YAMLError as exc: return conf_path, exc try: @@ -276,8 +274,11 @@ def __init__(self, sock): self.sock = sock def run(self): - events = {value: name[6:] for name, value in vars(zmq).items() - if name.startswith('EVENT_') and name != 'EVENT_ALL'} + events = { + value: name[6:] + for name, value in vars(zmq).items() + if name.startswith('EVENT_') and name != 'EVENT_ALL' + } log = logging.getLogger('vip.monitor') if log.level == logging.NOTSET: log.setLevel(logging.INFO) @@ -290,6 +291,7 @@ def run(self): class FramesFormatter: + def __init__(self, frames): self.frames = frames @@ -301,18 +303,30 @@ def __repr__(self): class Router(BaseRouter): '''Concrete VIP router.''' + # Add ZMQClientAuthentication - for building address using public/secretkey ? - def __init__(self, local_address, addresses=(), - context=None, secretkey=None, publickey=None, - default_user_id=None, monitor=False, tracker=None, - volttron_central_address=None, instance_name=None, - bind_web_address=None, volttron_central_serverkey=None, - protected_topics={}, external_address_file='', - msgdebug=None, agent_monitor_frequency=600, + def __init__(self, + local_address, + addresses=(), + context=None, + secretkey=None, + publickey=None, + default_user_id=None, + monitor=False, + tracker=None, + volttron_central_address=None, + instance_name=None, + bind_web_address=None, + volttron_central_serverkey=None, + protected_topics={}, + external_address_file='', + msgdebug=None, + agent_monitor_frequency=600, service_notifier=Optional[ServicePeerNotifier]): - super(Router, self).__init__( - context=context, default_user_id=default_user_id, service_notifier=service_notifier) + super(Router, self).__init__(context=context, + default_user_id=default_user_id, + service_notifier=service_notifier) self.local_address = Address(local_address) self._addr = addresses self.addresses = addresses = [Address(addr) for addr in set(addresses)] @@ -370,9 +384,9 @@ def setup(self): for address in self.addresses: if not address.identity: address.identity = identity - if (address.secretkey is None and - address.server not in ['NULL', 'PLAIN'] and - self._secretkey): + if (address.secretkey is None + and address.server not in ['NULL', 'PLAIN'] + and self._secretkey): address.server = 'CURVE' address.secretkey = self._secretkey if not address.domain: @@ -385,11 +399,9 @@ def setup(self): self._socket_class, self._poller, self._addr, self._instance_name) - self.pubsub = PubSubService(self.socket, - self._protected_topics, + self.pubsub = PubSubService(self.socket, self._protected_topics, self._ext_routing) - self.ext_rpc = ExternalRPCService(self.socket, - self._ext_routing) + self.ext_rpc = ExternalRPCService(self.socket, self._ext_routing) self._poller.register(sock, zmq.POLLIN) _log.debug("ZMQ version: {}".format(zmq.zmq_version())) @@ -402,25 +414,30 @@ def issue(self, topic, frames, extra=None): elif topic == UNROUTABLE: log('unroutable: %s: %s', extra, formatter) else: - log('%s: %s', - ('incoming' if topic == INCOMING else 'outgoing'), formatter) + log('%s: %s', ('incoming' if topic == INCOMING else 'outgoing'), + formatter) if self._tracker: self._tracker.hit(topic, frames, extra) if self._msgdebug: if not self._message_debugger_socket: # Initialize a ZMQ IPC socket on which to publish all messages to MessageDebuggerAgent. - socket_path = os.path.expandvars('$VOLTTRON_HOME/run/messagedebug') + socket_path = os.path.expandvars( + '$VOLTTRON_HOME/run/messagedebug') socket_path = os.path.expanduser(socket_path) - socket_path = 'ipc://{}'.format('@' if sys.platform.startswith('linux') else '') + socket_path + socket_path = 'ipc://{}'.format('@' if sys.platform.startswith( + 'linux') else '') + socket_path self._message_debugger_socket = zmq.Context().socket(zmq.PUB) self._message_debugger_socket.connect(socket_path) # Publish the routed message, including the "topic" (status/direction), for use by MessageDebuggerAgent. frame_bytes = [topic] - frame_bytes.extend(frames) # [frame if type(frame) is bytes else frame.bytes for frame in frames]) + frame_bytes.extend( + frames + ) # [frame if type(frame) is bytes else frame.bytes for frame in frames]) frame_bytes = serialize_frames(frames) # TODO we need to fix the msgdebugger socket if we need it to be connected #frame_bytes = [f.bytes for f in frame_bytes] #self._message_debugger_socket.send_pyobj(frame_bytes) + # This is currently not being used e.g once fixed we won't use it. #def extract_bytes(self, frame_bytes): # result = [] @@ -432,7 +449,8 @@ def issue(self, topic, frames, extra=None): # return result def handle_subsystem(self, frames, user_id): - _log.debug(f"Handling subsystem with frames: {frames} user_id: {user_id}") + _log.debug( + f"Handling subsystem with frames: {frames} user_id: {user_id}") subsystem = frames[5] if subsystem == 'quit': @@ -446,8 +464,9 @@ def handle_subsystem(self, frames, user_id): self.stop() raise KeyboardInterrupt() else: - _log.error(f"Sender {sender} not authorized to shutdown platform") - elif subsystem =='agentstop': + _log.error( + f"Sender {sender} not authorized to shutdown platform") + elif subsystem == 'agentstop': try: drop = frames[6] self._drop_peer(drop) @@ -455,9 +474,13 @@ def handle_subsystem(self, frames, user_id): if self._service_notifier: self._service_notifier.peer_dropped(drop) - _log.debug("ROUTER received agent stop message. dropping peer: {}".format(drop)) + _log.debug( + "ROUTER received agent stop message. dropping peer: {}". + format(drop)) except IndexError: - _log.error(f"agentstop called but unable to determine agent from frames sent {frames}") + _log.error( + f"agentstop called but unable to determine agent from frames sent {frames}" + ) return False elif subsystem == 'query': try: @@ -494,7 +517,7 @@ def handle_subsystem(self, frames, user_id): value = None frames[6:] = ['', value] frames[3] = '' - + return frames elif subsystem == 'pubsub': result = self.pubsub.handle_subsystem(frames, user_id) @@ -564,7 +587,9 @@ def ext_route(self, socket): peer = msg_data['to_peer'] # Send to destionation agent/peer # Form new frame for local - frames[:9] = [peer, sender, proto, usr_id, msg_id, 'external_rpc', msg] + frames[:9] = [ + peer, sender, proto, usr_id, msg_id, 'external_rpc', msg + ] try: self.socket.send_multipart(frames, flags=NOBLOCK, copy=False) except ZMQError as ex: @@ -594,25 +619,44 @@ class GreenRouter(Router): Greenlet friendly Router """ - def __init__(self, local_address, addresses=(), - context=None, secretkey=None, publickey=None, - default_user_id=None, monitor=False, tracker=None, - volttron_central_address=None, instance_name=None, - bind_web_address=None, volttron_central_serverkey=None, - protected_topics={}, external_address_file='', - msgdebug=None, volttron_central_rmq_address=None, + def __init__(self, + local_address, + addresses=(), + context=None, + secretkey=None, + publickey=None, + default_user_id=None, + monitor=False, + tracker=None, + volttron_central_address=None, + instance_name=None, + bind_web_address=None, + volttron_central_serverkey=None, + protected_topics={}, + external_address_file='', + msgdebug=None, + volttron_central_rmq_address=None, service_notifier=Optional[ServicePeerNotifier]): self._context_class = _green.Context self._socket_class = _green.Socket self._poller_class = _green.Poller super(GreenRouter, self).__init__( - local_address, addresses=addresses, - context=context, secretkey=secretkey, publickey=publickey, - default_user_id=default_user_id, monitor=monitor, tracker=tracker, - volttron_central_address=volttron_central_address, instance_name=instance_name, - bind_web_address=bind_web_address, volttron_central_serverkey=volttron_central_address, - protected_topics=protected_topics, external_address_file=external_address_file, - msgdebug=msgdebug, service_notifier=service_notifier) + local_address, + addresses=addresses, + context=context, + secretkey=secretkey, + publickey=publickey, + default_user_id=default_user_id, + monitor=monitor, + tracker=tracker, + volttron_central_address=volttron_central_address, + instance_name=instance_name, + bind_web_address=bind_web_address, + volttron_central_serverkey=volttron_central_address, + protected_topics=protected_topics, + external_address_file=external_address_file, + msgdebug=msgdebug, + service_notifier=service_notifier) def start(self): '''Create the socket and call setup(). @@ -690,9 +734,11 @@ def start_volttron_process(opts): opts.web_ssl_cert = config.expandall(opts.web_ssl_cert) if opts.web_ssl_key and not opts.web_ssl_cert: - raise Exception("If web-ssl-key is specified web-ssl-cert MUST be specified.") + raise Exception( + "If web-ssl-key is specified web-ssl-cert MUST be specified.") if opts.web_ssl_cert and not opts.web_ssl_key: - raise Exception("If web-ssl-cert is specified web-ssl-key MUST be specified.") + raise Exception( + "If web-ssl-cert is specified web-ssl-key MUST be specified.") if opts.web_ca_cert: assert os.path.isfile(opts.web_ca_cert), "web_ca_cert does not exist!" @@ -721,26 +767,31 @@ def start_volttron_process(opts): os.environ['BIND_WEB_ADDRESS'] = opts.bind_web_address parsed = urlparse(opts.bind_web_address) if parsed.scheme not in ('http', 'https'): - raise Exception( - 'bind-web-address must begin with http or https.') + raise Exception('bind-web-address must begin with http or https.') opts.bind_web_address = config.expandall(opts.bind_web_address) # zmq with tls is supported if opts.message_bus == 'zmq' and parsed.scheme == 'https': if not opts.web_ssl_key or not opts.web_ssl_cert: - raise Exception("zmq https requires a web-ssl-key and a web-ssl-cert file.") - if not os.path.isfile(opts.web_ssl_key) or not os.path.isfile(opts.web_ssl_cert): - raise Exception("zmq https requires a web-ssl-key and a web-ssl-cert file.") + raise Exception( + "zmq https requires a web-ssl-key and a web-ssl-cert file." + ) + if not os.path.isfile(opts.web_ssl_key) or not os.path.isfile( + opts.web_ssl_cert): + raise Exception( + "zmq https requires a web-ssl-key and a web-ssl-cert file." + ) # zmq without tls is supported through the use of a secret key, if it's None then # we want to generate a secret key and set it in the config file. elif opts.message_bus == 'zmq' and opts.web_secret_key is None: opts.web_secret_key = get_random_key() - _update_config_file(web_secret_key = opts.web_secret_key) + _update_config_file(web_secret_key=opts.web_secret_key) if opts.volttron_central_address: parsed = urlparse(opts.volttron_central_address) if parsed.scheme not in ('http', 'https', 'tcp', 'amqp', 'amqps'): raise Exception( - 'volttron-central-address must begin with tcp, amqp, amqps, http or https.') + 'volttron-central-address must begin with tcp, amqp, amqps, http or https.' + ) opts.volttron_central_address = config.expandall( opts.volttron_central_address) opts.volttron_central_serverkey = opts.volttron_central_serverkey @@ -767,8 +818,7 @@ def start_volttron_process(opts): else: _log.debug('open file resource limit increased from %d to %d', soft, limit) - _log.debug('open file resource limit %d to %d', - soft, hard) + _log.debug('open file resource limit %d to %d', soft, hard) # Set configuration if HAVE_RESTRICTED: if opts.verify_agents: @@ -782,13 +832,15 @@ def start_volttron_process(opts): # Check for agent isolation mode/permissions on VOLTTRON_HOME directory mode = os.stat(opts.volttron_home).st_mode if mode & (stat.S_IWGRP | stat.S_IWOTH): - _log.warning('insecure access control on directory: %s', opts.volttron_home) - + _log.warning('insecure access control on directory: %s', + opts.volttron_home) + # Initialize public and secret keys for Non-auth. publickey = None secretkey = None # auth entries for agents if opts.allow_auth: + _log.info("Loading auth entries from auth.json") # Get or generate encryption key keystore = KeyStore() _log.debug('using key-store file %s', keystore.filename) @@ -800,10 +852,15 @@ def start_volttron_process(opts): publickey = decode_key(keystore.public) if publickey: # Authorize the platform key: - entry = AuthEntry(credentials=encode_key(publickey), - user_id=PLATFORM, - capabilities=[{'edit_config_store': {'identity': '/.*/'}}], - comments='Automatically added by platform on start') + entry = AuthEntry( + credentials=encode_key(publickey), + user_id=PLATFORM, + capabilities=[{ + 'edit_config_store': { + 'identity': '/.*/' + } + }], + comments='Automatically added by platform on start') AuthFile().add(entry, overwrite=True) # Add platform key to known-hosts file: known_hosts = KnownHostsStore() @@ -813,14 +870,20 @@ def start_volttron_process(opts): secretkey = decode_key(keystore.secret) # Add the control.connection so that volttron-ctl can access the bus - control_conn_path = KeyStore.get_agent_keystore_path(CONTROL_CONNECTION) + control_conn_path = KeyStore.get_agent_keystore_path( + CONTROL_CONNECTION) os.makedirs(os.path.dirname(control_conn_path), exist_ok=True) - ks_control_conn = KeyStore(KeyStore.get_agent_keystore_path(CONTROL_CONNECTION)) - entry = AuthEntry(credentials=encode_key(decode_key(ks_control_conn.public)), + ks_control_conn = KeyStore( + KeyStore.get_agent_keystore_path(CONTROL_CONNECTION)) + entry = AuthEntry(credentials=encode_key( + decode_key(ks_control_conn.public)), user_id=CONTROL_CONNECTION, identity=CONTROL_CONNECTION, - capabilities=[{'edit_config_store': {'identity': '/.*/'}}, - 'modify_rpc_method_allowance', + capabilities=[{ + 'edit_config_store': { + 'identity': '/.*/' + } + }, 'modify_rpc_method_allowance', 'allow_auth_modifications'], comments='Automatically added by platform on start') AuthFile().add(entry, overwrite=True) @@ -831,9 +894,11 @@ def start_volttron_process(opts): # zmq.Context.instance().set(zmq.MAX_SOCKETS, 2046) tracker = Tracker() - protected_topics_file = os.path.join(opts.volttron_home, 'protected_topics.json') + protected_topics_file = os.path.join(opts.volttron_home, + 'protected_topics.json') _log.debug('protected topics file %s', protected_topics_file) - external_address_file = os.path.join(opts.volttron_home, 'external_address.json') + external_address_file = os.path.join(opts.volttron_home, + 'external_address.json') _log.debug('external_address_file file %s', external_address_file) protected_topics = {} if opts.agent_monitor_frequency: @@ -852,9 +917,12 @@ def start_volttron_process(opts): def zmq_router(stop): try: _log.debug("Running zmq router") - Router(opts.vip_local_address, opts.vip_address, - secretkey=secretkey, publickey=publickey, - default_user_id='vip.service', monitor=opts.monitor, + Router(opts.vip_local_address, + opts.vip_address, + secretkey=secretkey, + publickey=publickey, + default_user_id='vip.service', + monitor=opts.monitor, tracker=tracker, volttron_central_address=opts.volttron_central_address, volttron_central_serverkey=opts.volttron_central_serverkey, @@ -876,12 +944,14 @@ def zmq_router(stop): # RMQ router def rmq_router(stop): try: - RMQRouter(opts.vip_address, opts.vip_local_address, opts.instance_name, opts.vip_address, + RMQRouter(opts.vip_address, + opts.vip_local_address, + opts.instance_name, + opts.vip_address, volttron_central_address=opts.volttron_central_address, bind_web_address=opts.bind_web_address, enable_auth=opts.allow_auth, - service_notifier=notifier - ).run() + service_notifier=notifier).run() except Exception: _log.exception('Unhandled exception in rmq router loop') except KeyboardInterrupt: @@ -902,26 +972,44 @@ def rmq_router(stop): proxy_router = None proxy_router_task = None - _log.debug("********************************************************************") - _log.debug("VOLTTRON PLATFORM RUNNING ON {} MESSAGEBUS".format(opts.message_bus)) - _log.debug("********************************************************************") + _log.debug( + "********************************************************************" + ) + _log.debug("VOLTTRON PLATFORM RUNNING ON {} MESSAGEBUS".format( + opts.message_bus)) + _log.debug( + "********************************************************************" + ) # Start the config store before auth so we may one day have auth use it. config_store = ConfigStoreService(address=address, - identity=CONFIGURATION_STORE, - message_bus=opts.message_bus, - enable_auth=opts.allow_auth) + identity=CONFIGURATION_STORE, + message_bus=opts.message_bus, + enable_auth=opts.allow_auth, + enable_store=False) + + if opts.allow_auth: + entry = AuthEntry(credentials=config_store.core.publickey, + user_id=CONFIGURATION_STORE, + identity=CONFIGURATION_STORE, + capabilities='sync_agent_config', + comments='Automatically added by platform on start') + AuthFile().add(entry, overwrite=True) # Launch additional services and wait for them to start before # auto-starting agents services = [ - ControlService(opts.aip, address=address, identity=CONTROL, - tracker=tracker, heartbeat_autostart=True, - enable_store=False, enable_channel=True, - message_bus=opts.message_bus, - agent_monitor_frequency=opts.agent_monitor_frequency, - enable_auth=opts.allow_auth), - + ControlService( + opts.aip, + address=address, + identity=CONTROL, + tracker=tracker, + heartbeat_autostart=True, + enable_store=False, + enable_channel=True, + message_bus=opts.message_bus, + agent_monitor_frequency=opts.agent_monitor_frequency, + enable_auth=opts.allow_auth), KeyDiscoveryAgent(address=address, identity=KEY_DISCOVERY, external_address_config=external_address_file, @@ -933,19 +1021,27 @@ def rmq_router(stop): ] health_service = HealthService(address=address, - identity=PLATFORM_HEALTH, heartbeat_autostart=True, + identity=PLATFORM_HEALTH, + heartbeat_autostart=True, enable_store=False, message_bus=opts.message_bus, enable_auth=opts.allow_auth) - notifier.register_peer_callback(health_service.peer_added, health_service.peer_dropped) + notifier.register_peer_callback(health_service.peer_added, + health_service.peer_dropped) services.append(health_service) # Begin the webserver based options here. if opts.bind_web_address is not None: if not HAS_WEB: - _log.info(f"Web libraries not installed, but bind web address specified\n") - sys.stderr.write("Web libraries not installed, but bind web address specified\n") - sys.stderr.write("Please install web libraries using python3 bootstrap.py --web\n") + _log.info( + f"Web libraries not installed, but bind web address specified\n" + ) + sys.stderr.write( + "Web libraries not installed, but bind web address specified\n" + ) + sys.stderr.write( + "Please install web libraries using python3 bootstrap.py --web\n" + ) sys.exit(-1) if opts.instance_name is None: @@ -960,25 +1056,28 @@ def rmq_router(stop): base_webserver_name = PLATFORM_WEB + "-server" from volttron.platform.auth.certs import Certs certs = Certs() - certs.create_signed_cert_files(base_webserver_name, cert_type='server') - opts.web_ssl_key = certs.private_key_file(base_webserver_name) + certs.create_signed_cert_files(base_webserver_name, + cert_type='server') + opts.web_ssl_key = certs.private_key_file( + base_webserver_name) opts.web_ssl_cert = certs.cert_file(base_webserver_name) - + _log.info("Starting platform web service") - services.append(PlatformWebService( - serverkey=publickey, - identity=PLATFORM_WEB, - address=address, - bind_web_address=opts.bind_web_address, - volttron_central_address=opts.volttron_central_address, - enable_store=False, - message_bus=opts.message_bus, - volttron_central_rmq_address=opts.volttron_central_rmq_address, - web_ssl_key=opts.web_ssl_key, - web_ssl_cert=opts.web_ssl_cert, - web_secret_key=opts.web_secret_key, - enable_auth=opts.allow_auth - )) + services.append( + PlatformWebService( + serverkey=publickey, + identity=PLATFORM_WEB, + address=address, + bind_web_address=opts.bind_web_address, + volttron_central_address=opts.volttron_central_address, + enable_store=False, + message_bus=opts.message_bus, + volttron_central_rmq_address=opts. + volttron_central_rmq_address, + web_ssl_key=opts.web_ssl_key, + web_ssl_cert=opts.web_ssl_cert, + web_secret_key=opts.web_secret_key, + enable_auth=opts.allow_auth)) if opts.message_bus == 'zmq': # starting sequence is different for zmq and rmq @@ -993,7 +1092,8 @@ def rmq_router(stop): del event # Start ZMQ router in separate thread to remain responsive - thread = threading.Thread(target=zmq_router, args=(config_store.core.stop,)) + thread = threading.Thread(target=zmq_router, + args=(config_store.core.stop, )) thread.daemon = True thread.start() @@ -1004,7 +1104,9 @@ def rmq_router(stop): # Start RabbitMQ server if not running rmq_config = RMQConfig() if rmq_config is None: - _log.error("DEBUG: Exiting due to error in rabbitmq config file. Please check.") + _log.error( + "DEBUG: Exiting due to error in rabbitmq config file. Please check." + ) sys.exit() # If RabbitMQ is started as service, don't start it through the code @@ -1012,14 +1114,17 @@ def rmq_router(stop): try: start_rabbit(rmq_config.rmq_home) except AttributeError as exc: - _log.error("Exception while starting RabbitMQ. Check the path in the config file.") + _log.error( + "Exception while starting RabbitMQ. Check the path in the config file." + ) sys.exit() except subprocess.CalledProcessError as exc: _log.error("Unable to start rabbitmq server. " "Check rabbitmq log for errors") sys.exit() - thread = threading.Thread(target=rmq_router, args=(config_store.core.stop,)) + thread = threading.Thread(target=rmq_router, + args=(config_store.core.stop, )) thread.daemon = True thread.start() @@ -1039,17 +1144,21 @@ def rmq_router(stop): # Spawn Greenlet friendly ZMQ router # Necessary for backward compatibility with ZMQ message bus - green_router = GreenRouter(opts.vip_local_address, opts.vip_address, - secretkey=secretkey, publickey=publickey, - default_user_id='vip.service', monitor=opts.monitor, - tracker=tracker, - volttron_central_address=opts.volttron_central_address, - instance_name=opts.instance_name, - bind_web_address=opts.bind_web_address, - protected_topics=protected_topics, - external_address_file=external_address_file, - msgdebug=opts.msgdebug, - service_notifier=notifier) + green_router = GreenRouter( + opts.vip_local_address, + opts.vip_address, + secretkey=secretkey, + publickey=publickey, + default_user_id='vip.service', + monitor=opts.monitor, + tracker=tracker, + volttron_central_address=opts.volttron_central_address, + instance_name=opts.instance_name, + bind_web_address=opts.bind_web_address, + protected_topics=protected_topics, + external_address_file=external_address_file, + msgdebug=opts.msgdebug, + service_notifier=notifier) proxy_router = ZMQProxyRouter(address=address, identity=PROXY_ROUTER, @@ -1079,10 +1188,11 @@ def rmq_router(stop): instances[opts.volttron_home] = this_instance instances.async_sync() - events = [gevent.event.Event() for service in services] - tasks = [gevent.spawn(service.core.run, event) - for service, event in zip(services, events)] + tasks = [ + gevent.spawn(service.core.run, event) + for service, event in zip(services, events) + ] tasks.append(config_store_task) tasks.append(auth_task) tasks = [task for task in tasks if task] @@ -1130,19 +1240,28 @@ def rmq_router(stop): os.remove(pid_file) except Exception: _log.warning("Unable to load {}".format(VOLTTRON_INSTANCES)) - _log.debug("********************************************************************") + _log.debug( + "********************************************************************" + ) _log.debug("VOLTTRON PLATFORM HAS SHUTDOWN") - _log.debug("********************************************************************") + _log.debug( + "********************************************************************" + ) def setup_auth_service(opts, address, services): - protected_topics_file = os.path.join(opts.volttron_home, 'protected_topics.json') + protected_topics_file = os.path.join(opts.volttron_home, + 'protected_topics.json') _log.debug('protected topics file %s', protected_topics_file) auth_file = os.path.join(opts.volttron_home, 'auth.json') - auth = AuthService(auth_file, protected_topics_file, - opts.setup_mode, opts.aip, - address=address, identity=AUTH, - enable_store=False, message_bus=opts.message_bus, + auth = AuthService(auth_file, + protected_topics_file, + opts.setup_mode, + opts.aip, + address=address, + identity=AUTH, + enable_store=False, + message_bus=opts.message_bus, enable_auth=opts.allow_auth) event = gevent.event.Event() @@ -1154,24 +1273,27 @@ def setup_auth_service(opts, address, services): _log.debug("MAIN: protected topics content {}".format(protected_topics)) ks_auth = KeyStore(KeyStore.get_agent_keystore_path(AUTH)) - entry = AuthEntry( - credentials=encode_key(decode_key(ks_auth.public)), - user_id=AUTH, - identity=AUTH, - capabilities=['modify_rpc_method_allowance'], - comments='Automatically added by platform on start') + entry = AuthEntry(credentials=encode_key(decode_key(ks_auth.public)), + user_id=AUTH, + identity=AUTH, + capabilities=['modify_rpc_method_allowance'], + comments='Automatically added by platform on start') AuthFile().add(entry, overwrite=True) - - external_address_file = os.path.join(opts.volttron_home, 'external_address.json') + external_address_file = os.path.join(opts.volttron_home, + 'external_address.json') _log.debug('external_address_file file %s', external_address_file) entry = AuthEntry(credentials=services[0].core.publickey, user_id=CONTROL, identity=CONTROL, - capabilities=[{'edit_config_store': {'identity': '/.*/'}}, - 'modify_rpc_method_allowance', - 'allow_auth_modifications'], + capabilities=[ + { + 'edit_config_store': { + 'identity': '/.*/'} + }, + 'modify_rpc_method_allowance', + 'allow_auth_modifications'], comments='Automatically added by platform on start') AuthFile().add(entry, overwrite=True) @@ -1197,126 +1319,168 @@ def main(argv=sys.argv): 'potential damage.\n' % os.path.basename(argv[0])) sys.exit(77) - volttron_home = os.path.normpath(config.expandall( - os.environ.get('VOLTTRON_HOME', '~/.volttron'))) + volttron_home = os.path.normpath( + config.expandall(os.environ.get('VOLTTRON_HOME', '~/.volttron'))) os.environ['VOLTTRON_HOME'] = volttron_home # Setup option parser parser = config.ArgumentParser( - prog=os.path.basename(argv[0]), add_help=False, + prog=os.path.basename(argv[0]), + add_help=False, description='VOLTTRON platform service', usage='%(prog)s [OPTION]...', argument_default=argparse.SUPPRESS, epilog='Boolean options, which take no argument, may be inversed by ' - 'prefixing the option with no- (e.g. --autostart may be ' - 'inversed using --no-autostart).' - ) + 'prefixing the option with no- (e.g. --autostart may be ' + 'inversed using --no-autostart).') + parser.add_argument('-c', + '--config', + metavar='FILE', + action='parse_config', + ignore_unknown=False, + sections=[None, 'volttron'], + help='read configuration from FILE') + parser.add_argument('-l', + '--log', + metavar='FILE', + default=None, + help='send log output to FILE instead of stderr') + parser.add_argument('-L', + '--log-config', + metavar='FILE', + help='read logging configuration from FILE') + parser.add_argument('--log-level', + metavar='LOGGER:LEVEL', + action=LogLevelAction, + help='override default logger logging level') + parser.add_argument('--monitor', + action='store_true', + help='monitor and log connections (implies -v)') parser.add_argument( - '-c', '--config', metavar='FILE', action='parse_config', - ignore_unknown=False, sections=[None, 'volttron'], - help='read configuration from FILE') - parser.add_argument( - '-l', '--log', metavar='FILE', default=None, - help='send log output to FILE instead of stderr') - parser.add_argument( - '-L', '--log-config', metavar='FILE', - help='read logging configuration from FILE') - parser.add_argument( - '--log-level', metavar='LOGGER:LEVEL', action=LogLevelAction, - help='override default logger logging level') - parser.add_argument( - '--monitor', action='store_true', - help='monitor and log connections (implies -v)') - parser.add_argument( - '-q', '--quiet', action='add_const', const=10, dest='verboseness', + '-q', + '--quiet', + action='add_const', + const=10, + dest='verboseness', help='decrease logger verboseness; may be used multiple times') parser.add_argument( - '-v', '--verbose', action='add_const', const=-10, dest='verboseness', + '-v', + '--verbose', + action='add_const', + const=-10, + dest='verboseness', help='increase logger verboseness; may be used multiple times') - parser.add_argument( - '--verboseness', type=int, metavar='LEVEL', default=logging.WARNING, - help='set logger verboseness') + parser.add_argument('--verboseness', + type=int, + metavar='LEVEL', + default=logging.WARNING, + help='set logger verboseness') # parser.add_argument( # '--volttron-home', env_var='VOLTTRON_HOME', metavar='PATH', # help='VOLTTRON configuration directory') - parser.add_argument( - '--show-config', action='store_true', - help=argparse.SUPPRESS) + parser.add_argument('--show-config', + action='store_true', + help=argparse.SUPPRESS) parser.add_help_argument() parser.add_version_argument(version='%(prog)s ' + __version__) agents = parser.add_argument_group('agent options') + agents.add_argument('--autostart', + action='store_true', + inverse='--no-autostart', + help='automatically start enabled agents and services') + agents.add_argument('--no-autostart', + action='store_false', + dest='autostart', + help=argparse.SUPPRESS) agents.add_argument( - '--autostart', action='store_true', inverse='--no-autostart', - help='automatically start enabled agents and services') - agents.add_argument( - '--no-autostart', action='store_false', dest='autostart', - help=argparse.SUPPRESS) - agents.add_argument( - '--publish-address', metavar='ZMQADDR', + '--publish-address', + metavar='ZMQADDR', help='ZeroMQ URL used for pre-3.x agent publishing (deprecated)') agents.add_argument( - '--subscribe-address', metavar='ZMQADDR', + '--subscribe-address', + metavar='ZMQADDR', help='ZeroMQ URL used for pre-3.x agent subscriptions (deprecated)') + agents.add_argument('--vip-address', + metavar='ZMQADDR', + action='append', + default=[], + help='ZeroMQ URL to bind for VIP connections') agents.add_argument( - '--vip-address', metavar='ZMQADDR', action='append', default=[], - help='ZeroMQ URL to bind for VIP connections') - agents.add_argument( - '--vip-local-address', metavar='ZMQADDR', + '--vip-local-address', + metavar='ZMQADDR', help='ZeroMQ URL to bind for local agent VIP connections') agents.add_argument( - '--bind-web-address', metavar='BINDWEBADDR', default=None, + '--bind-web-address', + metavar='BINDWEBADDR', + default=None, help='Bind a web server to the specified ip:port passed') agents.add_argument( - '--web-ca-cert', metavar='CAFILE', default=None, - help='If using self-signed certificates, this variable will be set globally to allow requests' - 'to be able to correctly reach the webserver without having to specify verify in all calls.' + '--web-ca-cert', + metavar='CAFILE', + default=None, + help= + 'If using self-signed certificates, this variable will be set globally to allow requests' + 'to be able to correctly reach the webserver without having to specify verify in all calls.' ) agents.add_argument( - "--web-secret-key", default=None, - help="Secret key to be used instead of https based authentication." - ) + "--web-secret-key", + default=None, + help="Secret key to be used instead of https based authentication.") agents.add_argument( - '--web-ssl-key', metavar='KEYFILE', default=None, - help='ssl key file for using https with the volttron server' - ) + '--web-ssl-key', + metavar='KEYFILE', + default=None, + help='ssl key file for using https with the volttron server') agents.add_argument( - '--web-ssl-cert', metavar='CERTFILE', default=None, - help='ssl certficate file for using https with the volttron server' - ) + '--web-ssl-cert', + metavar='CERTFILE', + default=None, + help='ssl certficate file for using https with the volttron server') agents.add_argument( - '--volttron-central-address', default=None, + '--volttron-central-address', + default=None, help='The web address of a volttron central install instance.') + agents.add_argument('--volttron-central-serverkey', + default=None, + help='The serverkey of volttron central.') agents.add_argument( - '--volttron-central-serverkey', default=None, - help='The serverkey of volttron central.') - agents.add_argument( - '--allow-auth', default='True', - help='Require authentication and authorization in VOLTTRON. Default=True' - ) + '--allow-auth', + default='True', + help= + 'Require authentication and authorization in VOLTTRON. Default=True') agents.add_argument( - '--instance-name', default=None, + '--instance-name', + default=None, help='The name of the instance that will be reported to ' - 'VOLTTRON central.') + 'VOLTTRON central.') + agents.add_argument('--msgdebug', + action='store_true', + help='Route all messages to an agent while debugging.') agents.add_argument( - '--msgdebug', action='store_true', - help='Route all messages to an agent while debugging.') - agents.add_argument( - '--setup-mode', action='store_true', - help='Setup mode flag for setting up authorization of external platforms.') + '--setup-mode', + action='store_true', + help= + 'Setup mode flag for setting up authorization of external platforms.') parser.add_argument( - '--message-bus', action='store', default='zmq', dest='message_bus', + '--message-bus', + action='store', + default='zmq', + dest='message_bus', help='set message to be used. valid values are zmq and rmq') agents.add_argument( - '--volttron-central-rmq-address', default=None, + '--volttron-central-rmq-address', + default=None, help='The AMQP address of a volttron central install instance') agents.add_argument( - '--agent-monitor-frequency', default=600, + '--agent-monitor-frequency', + default=600, help='How often should the platform check for crashed agents and ' - 'attempt to restart. Units=seconds. Default=600') + 'attempt to restart. Units=seconds. Default=600') agents.add_argument( - '--agent-isolation-mode', default=False, + '--agent-isolation-mode', + default=False, help='Require that agents run with their own users (this requires ' - 'running scripts/secure_user_permissions.sh as sudo)') + 'running scripts/secure_user_permissions.sh as sudo)') # XXX: re-implement control options # on @@ -1334,12 +1498,20 @@ def main(argv=sys.argv): # help='user groups allowed to connect to control socket') if HAVE_RESTRICTED: + class RestrictedAction(argparse.Action): - def __init__(self, option_strings, dest, - const=True, help=None, **kwargs): - super(RestrictedAction, self).__init__( - option_strings, dest=argparse.SUPPRESS, nargs=0, - const=const, help=help) + + def __init__(self, + option_strings, + dest, + const=True, + help=None, + **kwargs): + super(RestrictedAction, self).__init__(option_strings, + dest=argparse.SUPPRESS, + nargs=0, + const=const, + help=help) def __call__(self, parser, namespace, values, option_string=None): namespace.verify_agents = self.const @@ -1348,24 +1520,30 @@ def __call__(self, parser, namespace, values, option_string=None): restrict = parser.add_argument_group('restricted options') restrict.add_argument( - '--restricted', action=RestrictedAction, inverse='--no-restricted', + '--restricted', + action=RestrictedAction, + inverse='--no-restricted', help='shortcut to enable all restricted features') - restrict.add_argument( - '--no-restricted', action=RestrictedAction, const=False, - help=argparse.SUPPRESS) - restrict.add_argument( - '--verify', action='store_true', inverse='--no-verify', - help='verify agent integrity before execution') - restrict.add_argument( - '--no-verify', action='store_false', dest='verify_agents', - help=argparse.SUPPRESS) - restrict.add_argument( - '--resource-monitor', action='store_true', - inverse='--no-resource-monitor', - help='enable agent resource management') - restrict.add_argument( - '--no-resource-monitor', action='store_false', - dest='resource_monitor', help=argparse.SUPPRESS) + restrict.add_argument('--no-restricted', + action=RestrictedAction, + const=False, + help=argparse.SUPPRESS) + restrict.add_argument('--verify', + action='store_true', + inverse='--no-verify', + help='verify agent integrity before execution') + restrict.add_argument('--no-verify', + action='store_false', + dest='verify_agents', + help=argparse.SUPPRESS) + restrict.add_argument('--resource-monitor', + action='store_true', + inverse='--no-resource-monitor', + help='enable agent resource management') + restrict.add_argument('--no-resource-monitor', + action='store_false', + dest='resource_monitor', + help=argparse.SUPPRESS) # restrict.add_argument( # '--mobility', action='store_true', inverse='--no-mobility', # help='enable agent mobility') @@ -1410,8 +1588,7 @@ def __call__(self, parser, namespace, values, option_string=None): web_ca_cert=None, # If we aren't using ssl then we need a secret key available for us to use. web_secret_key=None, - allow_auth='True' - ) + allow_auth='True') # Parse and expand options args = argv[1:] diff --git a/volttron/platform/packaging.py b/volttron/platform/packaging.py index 459b4e6760..ce033dfb8a 100644 --- a/volttron/platform/packaging.py +++ b/volttron/platform/packaging.py @@ -50,9 +50,9 @@ import errno from wheel.install import WheelFile -from volttron.platform.packages import * from volttron.platform.agent import utils from volttron.platform import get_volttron_data, get_home +from volttron.platform.packages import UnpackedPackage, VolttronPackageWheelFileNoSign from volttron.utils.prompt import prompt_response from volttron.platform.auth import certs from volttron.platform import config @@ -692,33 +692,32 @@ def main(argv=sys.argv): init_agent(opts.directory, opts.module_name, opts.template, opts.silent, opts.identity) elif opts.subparser_name == 'create_ca': _create_ca() - else: - if auth is not None: - try: - if opts.subparser_name == 'verify': - if not os.path.exists(opts.package): - print(f'Invalid package name {opts.package}') - verifier = auth.SignedZipPackageVerifier(opts.package) - verifier.verify() - print("Package is verified") - else: - user_type = {'admin': opts.admin, - 'creator': opts.creator, - 'initiator': opts.initiator, - 'platform': opts.platform} - if opts.subparser_name == 'sign': - in_args = { - 'config_file': opts.config_file, - 'user_type': user_type, - 'contract': opts.contract, - 'certs_dir': opts.certs_dir - } - _sign_agent_package(opts.package, **in_args) - - elif opts.subparser_name == 'create_cert': - _create_cert(name=opts.name, **user_type) - except auth.AuthError as e: - _log.error(e.message) + elif auth is not None: + try: + if opts.subparser_name == 'verify': + if not os.path.exists(opts.package): + print(f'Invalid package name {opts.package}') + verifier = auth.SignedZipPackageVerifier(opts.package) + verifier.verify() + print("Package is verified") + else: + user_type = {'admin': opts.admin, + 'creator': opts.creator, + 'initiator': opts.initiator, + 'platform': opts.platform} + if opts.subparser_name == 'sign': + in_args = { + 'config_file': opts.config_file, + 'user_type': user_type, + 'contract': opts.contract, + 'certs_dir': opts.certs_dir + } + _sign_agent_package(opts.package, **in_args) + + elif opts.subparser_name == 'create_cert': + _create_cert(name=opts.name, **user_type) + except auth.AuthError as e: + _log.error(e.message) except AgentPackageError as e: print(e) diff --git a/volttron/platform/store.py b/volttron/platform/store.py index 95a2580abb..8949ee9504 100644 --- a/volttron/platform/store.py +++ b/volttron/platform/store.py @@ -44,8 +44,8 @@ import errno from csv import DictReader from io import StringIO - import gevent +from deprecated import deprecated from volttron.platform import jsonapi from gevent.lock import Semaphore @@ -54,7 +54,7 @@ from volttron.platform.agent.utils import parse_json_config from volttron.platform.vip.agent import errors from volttron.platform.jsonrpc import RemoteError, MethodNotFound -from volttron.platform.agent.utils import parse_timestamp_string, format_timestamp, get_aware_utc_now +from volttron.platform.agent.utils import format_timestamp, get_aware_utc_now from volttron.platform.storeutils import check_for_recursion, strip_config_name, store_ext from .vip.agent import Agent, Core, RPC @@ -162,19 +162,50 @@ def _onstart(self, sender, **kwargs): @RPC.export @RPC.allow('edit_config_store') - def manage_store(self, identity, config_name, raw_contents, config_type="raw"): + @deprecated(reason="Use set_config") + def manage_store(self, identity, config_name, raw_contents, config_type="raw", trigger_callback=True, + send_update=True): + """ + This method is deprecated and will be removed in VOLTTRON 10. Please use set_config instead + """ + contents = process_raw_config(raw_contents, config_type) + self._add_config_to_store(identity, config_name, raw_contents, contents, config_type, + trigger_callback=trigger_callback, send_update=send_update) + + @RPC.export + @RPC.allow('edit_config_store') + def set_config(self, identity, config_name, raw_contents, config_type="raw", trigger_callback=True, + send_update=True): contents = process_raw_config(raw_contents, config_type) self._add_config_to_store(identity, config_name, raw_contents, contents, config_type, - trigger_callback=True) + trigger_callback=trigger_callback, send_update=send_update) + + @RPC.export + @RPC.allow('edit_config_store') + @deprecated(reason="Use delete_config") + def manage_delete_config(self, identity, config_name, trigger_callback=True, send_update=True): + """ + This method is deprecated and will be removed in VOLTTRON 10. Please use delete_config instead + """ + self.delete(identity, config_name, trigger_callback=trigger_callback, send_update=send_update) @RPC.export @RPC.allow('edit_config_store') - def manage_delete_config(self, identity, config_name): - self.delete(identity, config_name, trigger_callback=True) + def delete_config(self, identity, config_name, trigger_callback=True, send_update=True): + self.delete(identity, config_name, trigger_callback=trigger_callback, send_update=send_update) @RPC.export @RPC.allow('edit_config_store') + @deprecated(reason="Use delete_store") def manage_delete_store(self, identity): + """ + This method is deprecated and will be removed in VOLTTRON 10. Please use delete_store instead + """ + self.delete_store(identity) + + @RPC.export + @RPC.allow('edit_config_store') + def delete_store(self, identity): agent_store = self.store.get(identity) if agent_store is None: return @@ -211,19 +242,43 @@ def manage_delete_store(self, identity): self.store.pop(identity, None) @RPC.export + @deprecated(reason="Use list_configs") def manage_list_configs(self, identity): + """ + This method is deprecated and will be removed in VOLTTRON 10. Use list_configs instead + """ + return self.list_configs(identity) + + @RPC.export + def list_configs(self, identity): result = list(self.store.get(identity, {}).get("store", {}).keys()) result.sort() return result @RPC.export + @deprecated(reason="Use list_stores") def manage_list_stores(self): + """ + This method is deprecated and will be removed in VOLTTRON 10. Use list_stores instead + """ + return self.list_stores() + + @RPC.export + def list_stores(self): result = list(self.store.keys()) result.sort() return result @RPC.export + @deprecated(reason="Use get_config") def manage_get(self, identity, config_name, raw=True): + """ + This method is deprecated and will be removed in VOLTTRON 10. Use get_config instead + """ + return self.get_config(identity, config_name, raw) + + @RPC.export + def get_config(self, identity, config_name, raw=True): agent_store = self.store.get(identity) if agent_store is None: raise KeyError('No configuration file "{}" for VIP IDENTIY {}'.format(config_name, identity)) @@ -246,7 +301,15 @@ def manage_get(self, identity, config_name, raw=True): return agent_configs[real_config_name] @RPC.export + @deprecated(reason="Use get_metadata") def manage_get_metadata(self, identity, config_name): + """ + This method is deprecated and will be removed in VOLTTRON 10. Please use get_metadata instead + """ + return self.get_metadata(identity, config_name) + + @RPC.export + def get_metadata(self, identity, config_name): agent_store = self.store.get(identity) if agent_store is None: raise KeyError('No configuration file "{}" for VIP IDENTIY {}'.format(config_name, identity)) @@ -264,27 +327,21 @@ def manage_get_metadata(self, identity, config_name): real_config = agent_disk_store[real_config_name] - #Set modified to none if we predate the modified flag. + # Set modified to none if we predate the modified flag. if real_config.get("modified") is None: real_config["modified"] = None return real_config + @RPC.allow('edit_config_store') @RPC.export - def set_config(self, config_name, contents, trigger_callback=False, send_update=True): - identity = self.vip.rpc.context.vip_message.peer - self.store_config(identity, config_name, contents, trigger_callback=trigger_callback, send_update=send_update) - - - @RPC.export - def get_configs(self): + def initialize_configs(self, identity): """ Called by an Agent at startup to trigger initial configuration state push. """ - identity = self.vip.rpc.context.vip_message.peer - #We need to create store and lock if it doesn't exist in case someone + # We need to create store and lock if it doesn't exist in case someone # tries to add a configuration while we are sending the initial state. agent_store = self.store.get(identity) @@ -321,13 +378,6 @@ def get_configs(self): if not agent_disk_store: self.store.pop(identity, None) - @RPC.export - def delete_config(self, config_name, trigger_callback=False, send_update=True): - """Called by an Agent to delete a configuration.""" - identity = self.vip.rpc.context.vip_message.peer - self.delete(identity, config_name, trigger_callback=trigger_callback, - send_update=send_update) - # Helper method to allow the local services to delete configs before message # bus in online. def delete(self, identity, config_name, trigger_callback=False, send_update=True): diff --git a/volttron/platform/vip/agent/connection.py b/volttron/platform/vip/agent/connection.py index aa87675a31..fcf4d176c1 100644 --- a/volttron/platform/vip/agent/connection.py +++ b/volttron/platform/vip/agent/connection.py @@ -38,7 +38,6 @@ import logging import urllib.parse -import uuid import os import gevent @@ -226,5 +225,5 @@ def notify(self, method, *args, **kwargs): def kill(self, *args, **kwargs): if self._greenlet is not None: self._greenlet.kill(*args, **kwargs) - del(self._greenlet) + del self._greenlet self._greenlet = None diff --git a/volttron/platform/vip/agent/core.py b/volttron/platform/vip/agent/core.py index 3cbd38a5ae..7276263af3 100644 --- a/volttron/platform/vip/agent/core.py +++ b/volttron/platform/vip/agent/core.py @@ -55,26 +55,29 @@ import gevent.event import grequests from gevent.queue import Queue -from volttron.platform.keystore import KnownHostsStore from zmq import green as zmq -from zmq.green import ZMQError, EAGAIN, ENOTSOCK +from zmq.green import EAGAIN, ENOTSOCK, ZMQError from zmq.utils.monitor import recv_monitor_message -from volttron.platform import get_address, get_home, jsonapi -from volttron.platform import is_rabbitmq_available +from volttron.platform import get_address, get_home, is_rabbitmq_available, jsonapi from volttron.platform.agent import utils -from volttron.platform.agent.utils import get_fq_identity, load_platform_config, get_platform_instance_name, is_auth_enabled +from volttron.platform.agent.utils import (get_fq_identity, + get_platform_instance_name, + is_auth_enabled, + load_platform_config) +from volttron.platform.keystore import KnownHostsStore from volttron.platform.messaging.health import STATUS_BAD from volttron.utils.rmq_config_params import RMQConfig from volttron.utils.rmq_mgmt import RabbitMQMgmt -from .decorators import annotate, annotations, dualmethod -from .dispatch import Signal -from .errors import VIPError + +from .... import platform from .. import router from ..rmq_connection import RMQConnection from ..socket import Message from ..zmq_connection import ZMQConnection -from .... import platform +from .decorators import annotate, annotations, dualmethod +from .dispatch import Signal +from .errors import VIPError if is_rabbitmq_available(): import pika @@ -160,7 +163,7 @@ def findsignal(obj, owner, name): signal = owner for part in parts: signal = getattr(signal, part) - assert isinstance(signal, Signal), 'bad signal name %r' % (name,) + assert isinstance(signal, Signal), 'bad signal name %r' % (name, ) return signal @@ -190,8 +193,8 @@ def __init__(self, owner): prev_int_signal = gevent.signal.getsignal(signal.SIGINT) # To avoid a child agent handler overwriting the parent agent handler if prev_int_signal in [None, signal.SIG_IGN, signal.SIG_DFL]: - self.oninterrupt = gevent.signal.signal(signal.SIGINT, - self._on_sigint_handler) + self.oninterrupt = gevent.signal.signal( + signal.SIGINT, self._on_sigint_handler) self._owner = owner def setup(self): @@ -206,9 +209,10 @@ def setup(self): def setup(member): # pylint: disable=redefined-outer-name periodics.extend( - periodic.get(member) for periodic in annotations( - member, list, 'core.periodics')) - for deadline, args, kwargs in annotations(member, list, 'core.schedule'): + periodic.get(member) + for periodic in annotations(member, list, 'core.periodics')) + for deadline, args, kwargs in annotations(member, list, + 'core.schedule'): self.schedule(deadline, member, *args, **kwargs) for name in annotations(member, set, 'core.signals'): findsignal(self, owner, name).connect(member, owner) @@ -397,8 +401,7 @@ def periodic(self, period, func, args=None, kwargs=None, wait=0): warnings.warn( 'Use of the periodic() method is deprecated in favor of the ' 'schedule() method with the periodic() generator. This ' - 'method will be removed in a future version.', - DeprecationWarning) + 'method will be removed in a future version.', DeprecationWarning) greenlet = Periodic(period, args, kwargs, wait).get(func) self.spawned_greenlets.add(greenlet) greenlet.start() @@ -415,6 +418,7 @@ def periodic(cls, period, args=None, kwargs=None, wait=0): # pylint: disable=no @classmethod def receiver(cls, signal): + def decorate(method): annotate(method, set, 'core.signals', signal) return method @@ -438,11 +442,13 @@ def get_tie_breaker(self): def _schedule_callback(self, deadline, callback): deadline = utils.get_utc_seconds_from_epoch(deadline) - heapq.heappush(self._schedule, (deadline, self.get_tie_breaker(), callback)) + heapq.heappush(self._schedule, + (deadline, self.get_tie_breaker(), callback)) if self._schedule_event: self._schedule_event.set() def _schedule_iter(self, it, event): + def wrapper(): if event.canceled: event.finished = True @@ -485,11 +491,20 @@ class Core(BasicCore): # to false to keep from blocking. AuthService does this. delay_running_event_set = True - def __init__(self, owner, address=None, identity=None, context=None, - publickey=None, secretkey=None, serverkey=None, + def __init__(self, + owner, + address=None, + identity=None, + context=None, + publickey=None, + secretkey=None, + serverkey=None, volttron_home=os.path.abspath(platform.get_home()), - agent_uuid=None, reconnect_interval=None, - version='0.1', instance_name=None, messagebus=None): + agent_uuid=None, + reconnect_interval=None, + version='0.1', + instance_name=None, + messagebus=None): self.volttron_home = volttron_home # These signals need to exist before calling super().__init__() @@ -500,7 +515,8 @@ def __init__(self, owner, address=None, identity=None, context=None, self.configuration = Signal() super(Core, self).__init__(owner) self.address = address if address is not None else get_address() - self.identity = str(identity) if identity is not None else str(uuid.uuid4()) + self.identity = str(identity) if identity is not None else str( + uuid.uuid4()) self.agent_uuid = agent_uuid self.publickey = publickey self.secretkey = secretkey @@ -530,8 +546,7 @@ def set_connected(self, value): self.__connected = value connected = property(fget=lambda self: self.get_connected(), - fset=lambda self, v: self.set_connected(v) - ) + fset=lambda self, v: self.set_connected(v)) def stop(self, timeout=None, platform_shutdown=False): # Send message to router that this agent is stopping @@ -584,7 +599,9 @@ def handle_error(self, message): error = VIPError.from_errno(*args) self.onviperror.send(self, error=error, message=message) - def create_event_handlers(self, state, hello_response_event, running_event): + def create_event_handlers(self, state, hello_response_event, + running_event): + def connection_failed_check(): # If we don't have a verified connection after 10.0 seconds # shut down. @@ -592,13 +609,13 @@ def connection_failed_check(): return _log.error("No response to hello message after 10 seconds.") _log.error("Type of message bus used {}".format(self.messagebus)) - _log.error("A common reason for this is a conflicting VIP IDENTITY.") + _log.error( + "A common reason for this is a conflicting VIP IDENTITY.") _log.error("Another common reason is not having an auth entry on" "the target instance.") _log.error("Shutting down agent.") _log.error("Possible conflicting identity is: {}".format( - self.identity - )) + self.identity)) self.stop(timeout=10.0) @@ -608,15 +625,16 @@ def hello(): state.ident = ident = 'connect.hello.%d' % state.count state.count += 1 self.spawn(connection_failed_check) - message = Message(peer='', subsystem='hello', - id=ident, args=['hello']) + message = Message(peer='', + subsystem='hello', + id=ident, + args=['hello']) self.connection.send_vip_object(message) - def hello_response(sender, version='', - router='', identity=''): + def hello_response(sender, version='', router='', identity=''): _log.info("Connected to platform: " "router: {} version: {} identity: {}".format( - router, version, identity)) + router, version, identity)) _log.debug("Running onstart methods.") hello_response_event.set() self.onstart.sendby(self.link_receiver, self) @@ -632,36 +650,50 @@ class ZMQCore(Core): Concrete Core class for ZeroMQ message bus """ - def __init__(self, owner, address=None, identity=None, context=None, - publickey=None, secretkey=None, serverkey=None, + def __init__(self, + owner, + address=None, + identity=None, + context=None, + publickey=None, + secretkey=None, + serverkey=None, volttron_home=os.path.abspath(platform.get_home()), - agent_uuid=None, reconnect_interval=None, - version='0.1', enable_fncs=False, - instance_name=None, messagebus='zmq', enable_auth=True): - super(ZMQCore, self).__init__(owner, address=address, identity=identity, - context=context, publickey=publickey, secretkey=secretkey, - serverkey=serverkey, volttron_home=volttron_home, - agent_uuid=agent_uuid, reconnect_interval=reconnect_interval, + agent_uuid=None, + reconnect_interval=None, + version='0.1', + enable_fncs=False, + instance_name=None, + messagebus='zmq', + enable_auth=True): + super(ZMQCore, self).__init__(owner, + address=address, + identity=identity, + context=context, + publickey=publickey, + secretkey=secretkey, + serverkey=serverkey, + volttron_home=volttron_home, + agent_uuid=agent_uuid, + reconnect_interval=reconnect_interval, version=version, - instance_name=instance_name, messagebus=messagebus) + instance_name=instance_name, + messagebus=messagebus) self.context = context or zmq.Context.instance() self._fncs_enabled = enable_fncs self.messagebus = messagebus self.enable_auth = enable_auth zmq_auth = None - if self.enable_auth: + if self.enable_auth: from volttron.platform.auth.auth_protocols.auth_zmq import ZMQClientAuthentication, ZMQClientParameters zmq_auth = ZMQClientAuthentication( - ZMQClientParameters( - address=self.address, - identity=self.identity, - agent_uuid=self.agent_uuid, - publickey=self.publickey, - secretkey=self.secretkey, - serverkey=self.serverkey, - volttron_home=self.volttron_home - ) - ) + ZMQClientParameters(address=self.address, + identity=self.identity, + agent_uuid=self.agent_uuid, + publickey=self.publickey, + secretkey=self.secretkey, + serverkey=self.serverkey, + volttron_home=self.volttron_home)) self.address = zmq_auth.create_authentication_parameters() self.publickey = zmq_auth.publickey self.secretkey = zmq_auth.secretkey @@ -681,7 +713,8 @@ def set_connected(self, value): def loop(self, running_event): # pre-setup # self.context.set(zmq.MAX_SOCKETS, 30690) - _log.info(f"CORE address:{self.address}") + _log.info( + f"Identity: {self.identity} connecting to address:{self.address}") self.connection = ZMQConnection(self.address, self.identity, self.instance_name, @@ -713,7 +746,7 @@ def monitor(): # get_monitor_socket() so we can use green sockets with # regular contexts (get_monitor_socket() uses # self.context.socket()). - addr = 'inproc://monitor.v-%d' % (id(self.socket),) + addr = 'inproc://monitor.v-%d' % (id(self.socket), ) sock = None if self.socket is not None: try: @@ -755,7 +788,8 @@ def monitor(): if self.socket is not None: self.socket.monitor(None, 0) except Exception as exc: - _log.debug("Error in closing the socket: {}".format(exc)) + _log.debug( + "Error in closing the socket: {}".format(exc)) self.onconnected.connect(hello_response) self.ondisconnected.connect(close_socket) @@ -789,14 +823,15 @@ def vip_loop(): # subsystem, message.id, len(message.args), message.args[0])) # Handle hellos sent by CONNECTED event - if (str(subsystem) == 'hello' and - message.id == state.ident and - len(message.args) > 3 and - message.args[0] == 'welcome'): + if (str(subsystem) == 'hello' and message.id == state.ident + and len(message.args) > 3 + and message.args[0] == 'welcome'): version, server, identity = message.args[1:4] self.connected = True - self.onconnected.send(self, version=version, - router=server, identity=identity) + self.onconnected.send(self, + version=version, + router=server, + identity=identity) continue try: @@ -829,13 +864,10 @@ def vip_loop(): self.socket = None yield - - def connect_remote_platform( - self, - address: str, - serverkey: typing.Optional[str]=None, - agent_class=None - ): + def connect_remote_platform(self, + address: str, + serverkey: typing.Optional[str] = None, + agent_class=None): """ Agent attempts to connect to a remote platform to exchange data. @@ -861,8 +893,8 @@ def connect_remote_platform( function. """ - from volttron.platform.vip.agent.utils import build_agent from volttron.platform.vip.agent import Agent + from volttron.platform.vip.agent.utils import build_agent if agent_class is None: agent_class = Agent @@ -879,8 +911,7 @@ def connect_remote_platform( if not temp_serverkey: _log.info( "Destination serverkey not found in known hosts file, " - "using config" - ) + "using config") destination_serverkey = serverkey elif not serverkey: destination_serverkey = temp_serverkey @@ -889,13 +920,10 @@ def connect_remote_platform( raise ValueError( "server_key passed and known hosts serverkey do not " "" - "match!" - ) + "match!") destination_serverkey = serverkey - _log.debug( - "Connecting using: %s", get_fq_identity(self.identity) - ) + _log.debug("Connecting using: %s", get_fq_identity(self.identity)) value = build_agent( agent_class=agent_class, @@ -907,8 +935,7 @@ def connect_remote_platform( address=address, ) elif parsed_address.scheme in ("https", "http"): - from volttron.platform.web import DiscoveryInfo - from volttron.platform.web import DiscoveryError + from volttron.platform.web import DiscoveryError, DiscoveryInfo try: # TODO: Use known host instead of looking up for discovery @@ -928,14 +955,12 @@ def connect_remote_platform( # version of the identity because there will be conflicts if # volttron central has more than one platform.agent connecting if not info.vip_address: - err = ( - "Discovery from {} did not return vip_address".format(address) - ) + err = ("Discovery from {} did not return vip_address". + format(address)) raise ValueError(err) if self.enable_auth and not info.serverkey: - err = ( - "Discovery from {} did not return serverkey".format(address) - ) + err = ("Discovery from {} did not return serverkey".format( + address)) raise ValueError(err) _log.debug( "Connecting using: %s", @@ -963,8 +988,7 @@ def connect_remote_platform( else: raise ValueError( "Invalid configuration found the address: {} has an invalid " - "scheme".format(address) - ) + "scheme".format(address)) return value @@ -990,18 +1014,36 @@ class RMQCore(Core): Concrete Core class for RabbitMQ message bus """ - def __init__(self, owner, address=None, identity=None, context=None, - publickey=None, secretkey=None, serverkey=None, + def __init__(self, + owner, + address=None, + identity=None, + context=None, + publickey=None, + secretkey=None, + serverkey=None, volttron_home=os.path.abspath(platform.get_home()), - agent_uuid=None, reconnect_interval=None, - version='0.1', instance_name=None, messagebus='rmq', + agent_uuid=None, + reconnect_interval=None, + version='0.1', + instance_name=None, + messagebus='rmq', volttron_central_address=None, - volttron_central_instance_name=None, enable_auth=True): - super(RMQCore, self).__init__(owner, address=address, identity=identity, - context=context, publickey=publickey, secretkey=secretkey, - serverkey=serverkey, volttron_home=volttron_home, - agent_uuid=agent_uuid, reconnect_interval=reconnect_interval, - version=version, instance_name=instance_name, messagebus=messagebus) + volttron_central_instance_name=None, + enable_auth=True): + super(RMQCore, self).__init__(owner, + address=address, + identity=identity, + context=context, + publickey=publickey, + secretkey=secretkey, + serverkey=serverkey, + volttron_home=volttron_home, + agent_uuid=agent_uuid, + reconnect_interval=reconnect_interval, + version=version, + instance_name=instance_name, + messagebus=messagebus) self.volttron_central_address = volttron_central_address self.enable_auth = enable_auth self.remote_certs_dir = None @@ -1033,16 +1075,13 @@ def __init__(self, owner, address=None, identity=None, context=None, if self.publickey is None or self.secretkey is None and self.enable_auth: from volttron.platform.auth.auth_protocols.auth_zmq import ZMQClientAuthentication, ZMQClientParameters zmq_auth = ZMQClientAuthentication( - ZMQClientParameters( - address=self.address, - identity=self.identity, - agent_uuid=self.agent_uuid, - publickey=self.publickey, - secretkey=self.secretkey, - serverkey=self.serverkey, - volttron_home=self.volttron_home - ) - ) + ZMQClientParameters(address=self.address, + identity=self.identity, + agent_uuid=self.agent_uuid, + publickey=self.publickey, + secretkey=self.secretkey, + serverkey=self.serverkey, + volttron_home=self.volttron_home)) zmq_auth._set_public_and_secret_keys() self.publickey = zmq_auth.publickey @@ -1058,6 +1097,7 @@ def set_connected(self, value): super(RMQCore, self).set_connected(value) connected = property(get_connected, set_connected) + # Replace with RMQConnectionParam (wraps around pika.Connection) # Passed into RMQClientConnection() def _build_connection_parameters(self): @@ -1072,24 +1112,28 @@ def _build_connection_parameters(self): try: if self.instance_name == get_platform_instance_name(): - param = connection_api.build_agent_connection(self.identity, self.instance_name) + param = connection_api.build_agent_connection( + self.identity, self.instance_name) else: param = connection_api.build_remote_connection_param() except AttributeError: - _log.error("RabbitMQ broker may not be running. Restart the broker first") + _log.error( + "RabbitMQ broker may not be running. Restart the broker first" + ) param = None return param def loop(self, running_event): - if not isinstance(self.rmq_address, pika.ConnectionParameters): + if not isinstance(self.rmq_address, pika.ConnectionParameters): self.rmq_address = self._build_connection_parameters() # pre-setup - self.connection = RMQConnection(self.rmq_address, - self.identity, - self.instance_name, - reconnect_delay=self.rmq_mgmt.rmq_config.reconnect_delay(), - vc_url=self.volttron_central_address) + self.connection = RMQConnection( + self.rmq_address, + self.identity, + self.instance_name, + reconnect_delay=self.rmq_mgmt.rmq_config.reconnect_delay(), + vc_url=self.volttron_central_address) yield # pre-start @@ -1115,8 +1159,8 @@ def connect_callback(): bindings = self.rmq_mgmt.get_bindings('volttron') except AttributeError: bindings = None - router_user = router_key = "{inst}.{ident}".format(inst=self.instance_name, - ident='router') + router_user = router_key = "{inst}.{ident}".format( + inst=self.instance_name, ident='router') if bindings: for binding in bindings: if binding['destination'] == router_user and \ @@ -1130,8 +1174,9 @@ def connect_callback(): if router_connected: hello() else: - _log.debug("Router not bound to RabbitMQ yet, waiting for 2 seconds before sending hello {}". - format(self.identity)) + _log.debug( + "Router not bound to RabbitMQ yet, waiting for 2 seconds before sending hello {}" + .format(self.identity)) self.spawn_later(2, hello) # Connect to RMQ broker. Register a callback to get notified when @@ -1158,21 +1203,23 @@ def vip_loop(): subsystem = message.subsystem if subsystem == 'hello': - if (subsystem == 'hello' and - message.id == state.ident and - len(message.args) > 3 and - message.args[0] == 'welcome'): + if (subsystem == 'hello' + and message.id == state.ident + and len(message.args) > 3 + and message.args[0] == 'welcome'): version, server, identity = message.args[1:4] self.connected = True - self.onconnected.send(self, version=version, + self.onconnected.send(self, + version=version, router=server, identity=identity) continue try: handle = self.subsystems[subsystem] except KeyError: - _log.error('peer %r requested unknown subsystem %r', - message.peer, subsystem) + _log.error( + 'peer %r requested unknown subsystem %r', + message.peer, subsystem) message.user = '' message.args = list(router._INVALID_SUBSYSTEM) message.args.append(message.subsystem) @@ -1193,13 +1240,10 @@ def vip_message_handler(self, message): # _log.debug("RMQ VIP Core {}".format(message)) self._event_queue.put(message) - - def connect_remote_platform( - self, - address, - serverkey=None, - agent_class=None - ): + def connect_remote_platform(self, + address, + serverkey=None, + agent_class=None): """ Agent attempts to connect to a remote platform to exchange data. @@ -1225,8 +1269,8 @@ def connect_remote_platform( function. """ - from volttron.platform.vip.agent.utils import build_agent from volttron.platform.vip.agent import Agent + from volttron.platform.vip.agent.utils import build_agent if agent_class is None: agent_class = Agent @@ -1237,15 +1281,16 @@ def connect_remote_platform( if parsed_address.scheme == "tcp": # ZMQ connection destination_serverkey = None - _log.debug(f"parsed address scheme is tcp. auth enabled = {self.enable_auth}") + _log.debug( + f"parsed address scheme is tcp. auth enabled = {self.enable_auth}" + ) if self.enable_auth: hosts = KnownHostsStore() temp_serverkey = hosts.serverkey(address) if not temp_serverkey: _log.info( "Destination serverkey not found in known hosts file, " - "using config" - ) + "using config") destination_serverkey = serverkey elif not serverkey: destination_serverkey = temp_serverkey @@ -1254,13 +1299,10 @@ def connect_remote_platform( raise ValueError( "server_key passed and known hosts serverkey do not " "" - "match!" - ) + "match!") destination_serverkey = serverkey - _log.debug( - "Connecting using: %s", get_fq_identity(self.identity) - ) + _log.debug("Connecting using: %s", get_fq_identity(self.identity)) value = build_agent( agent_class=agent_class, @@ -1272,9 +1314,8 @@ def connect_remote_platform( address=address, ) elif parsed_address.scheme in ("https", "http"): - from volttron.platform.web import DiscoveryInfo - from volttron.platform.web import DiscoveryError from volttron.platform.auth.auth_protocols.auth_rmq import RMQConnectionAPI + from volttron.platform.web import DiscoveryError, DiscoveryInfo try: # TODO: Use known host instead of looking up for discovery @@ -1294,28 +1335,22 @@ def connect_remote_platform( # Check if we already have the cert, if so use it # instead of requesting cert again remote_certs_dir = self.get_remote_certs_dir() - remote_cert_name = "{}.{}".format( - info.instance_name, fqid_local - ) - certfile = os.path.join( - remote_certs_dir, remote_cert_name + ".crt" - ) + remote_cert_name = "{}.{}".format(info.instance_name, + fqid_local) + certfile = os.path.join(remote_certs_dir, + remote_cert_name + ".crt") if os.path.exists(certfile): response = certfile else: - response = self.request_cert( - address, fqid_local, info - ) + response = self.request_cert(address, fqid_local, info) if response is None: _log.error("there was no response from the server") value = None elif isinstance(response, tuple): if response[0] == "PENDING": - _log.info( - "Waiting for administrator to accept a " - "CSR request." - ) + _log.info("Waiting for administrator to accept a " + "CSR request.") value = None # elif isinstance(response, dict): # response @@ -1329,14 +1364,13 @@ def connect_remote_platform( # pass to the build_remote_connection_params # for a successful - remote_rmq_user = get_fq_identity( - fqid_local, info.instance_name - ) - _log.debug( - "REMOTE RMQ USER IS: %s", remote_rmq_user - ) - connection_api = RMQConnectionAPI(rmq_user=remote_rmq_user, - url_address=info.rmq_address, ssl_auth=True) + remote_rmq_user = get_fq_identity(fqid_local, + info.instance_name) + _log.debug("REMOTE RMQ USER IS: %s", remote_rmq_user) + connection_api = RMQConnectionAPI( + rmq_user=remote_rmq_user, + url_address=info.rmq_address, + ssl_auth=True) remote_rmq_address = connection_api.build_remote_connection_param( cert_dir=self.get_remote_certs_dir()) @@ -1351,9 +1385,7 @@ def connect_remote_platform( agent_class=agent_class, ) else: - raise ValueError( - "Unknown path through discovery process!" - ) + raise ValueError("Unknown path through discovery process!") except DiscoveryError: _log.error( @@ -1366,17 +1398,12 @@ def connect_remote_platform( else: raise ValueError( "Invalid configuration found the address: {} has an invalid " - "scheme".format(address) - ) + "scheme".format(address)) return value - def request_cert( - self, - csr_server, - fully_qualified_local_identity, - discovery_info - ): + def request_cert(self, csr_server, fully_qualified_local_identity, + discovery_info): """ Get a signed csr from the csr_server endpoint @@ -1391,25 +1418,22 @@ def request_cert( if not config.is_ssl: raise ValueError( - "Only can create csr for rabbitmq based platform in ssl mode." - ) + "Only can create csr for rabbitmq based platform in ssl mode.") # info = discovery_info # if info is None: # info = DiscoveryInfo.request_discovery_info(csr_server) csr_request = self.rmq_mgmt.certs.create_csr( - fully_qualified_local_identity, discovery_info.instance_name - ) + fully_qualified_local_identity, discovery_info.instance_name) # The csr request requires the fully qualified identity that is # going to be connected to the external instance. # # The remote instance id is the instance name of the remote platform # concatenated with the identity of the local fully quallified # identity. - remote_cert_name = "{}.{}".format( - discovery_info.instance_name, fully_qualified_local_identity - ) + remote_cert_name = "{}.{}".format(discovery_info.instance_name, + fully_qualified_local_identity) remote_ca_name = discovery_info.instance_name + "_ca" # if certs.cert_exists(remote_cert_name, True): @@ -1452,8 +1476,7 @@ def request_cert( discovery_info.rmq_ca_cert.encode("utf-8"), ) os.environ["REQUESTS_CA_BUNDLE"] = os.path.join( - remote_certs_dir, "requests_ca_bundle" - ) + remote_certs_dir, "requests_ca_bundle") _log.debug( "Set os.environ requests ca bundle to %s", os.environ["REQUESTS_CA_BUNDLE"], @@ -1470,15 +1493,13 @@ def request_cert( "Shutting down", ) self._owner.vip.health.set_status(status.status, status.context) - self._owner.vip.health.send_alert( - self.identity + "_DENIED", status - ) + self._owner.vip.health.send_alert(self.identity + "_DENIED", + status) self.stop() return None elif status == "ERROR": err = "Error retrieving certificate from {}\n".format( - config.hostname - ) + config.hostname) err += "{}".format(message) raise ValueError(err) else: # No resposne @@ -1492,9 +1513,7 @@ def request_cert( def get_remote_certs_dir(self): if not self.remote_certs_dir: - install_dir = os.path.join( - get_home(), "agents", self.agent_uuid - ) + install_dir = os.path.join(get_home(), "agents", self.agent_uuid) files = os.listdir(install_dir) for f in files: agent_dir = os.path.join(install_dir, f) diff --git a/volttron/platform/vip/agent/example.py b/volttron/platform/vip/agent/example.py index 297d14a8be..337c881807 100644 --- a/volttron/platform/vip/agent/example.py +++ b/volttron/platform/vip/agent/example.py @@ -1,9 +1,9 @@ - - - import gevent -from volttron.platform.vip.agent import * +from volttron.platform.vip.agent import Agent +from volttron.platform.vip.agent.core import Core +from volttron.platform.vip.agent.errors import Unreachable +from volttron.platform.vip.agent.subsystems import RPC from volttron.platform.scheduling import periodic diff --git a/volttron/platform/vip/agent/subsystems/configstore.py b/volttron/platform/vip/agent/subsystems/configstore.py index 4ba08ed72c..5a0bba29bd 100644 --- a/volttron/platform/vip/agent/subsystems/configstore.py +++ b/volttron/platform/vip/agent/subsystems/configstore.py @@ -40,13 +40,15 @@ import os import weakref import fnmatch -import greenlet import inspect from .base import SubsystemBase from volttron.platform.storeutils import list_unique_links, check_for_config_link from volttron.platform.vip.agent import errors from volttron.platform.agent.known_identities import CONFIGURATION_STORE +from volttron.platform import jsonapi +from volttron.platform.agent.utils import is_auth_enabled + from collections import defaultdict from copy import deepcopy @@ -61,14 +63,15 @@ _log = logging.getLogger(__name__) -VALID_ACTIONS = set(["NEW", "UPDATE", "DELETE"]) +VALID_ACTIONS = ("NEW", "UPDATE", "DELETE") + class ConfigStore(SubsystemBase): def __init__(self, owner, core, rpc): self._core = weakref.ref(core) self._rpc = weakref.ref(rpc) - self._ref_map = {} #For triggering callbacks. + self._ref_map = {} # For triggering callbacks. self._reverse_ref_map = defaultdict(set) # For triggering callbacks. self._store = {} self._default_store = {} @@ -80,6 +83,7 @@ def __init__(self, owner, core, rpc): self._initial_callbacks_called = False self._process_callbacks_code_object = self._process_callbacks.__code__ + self.vip_identity = self._core().identity def sub_factory(): return defaultdict(set) @@ -89,6 +93,9 @@ def sub_factory(): def onsetup(sender, **kwargs): rpc.export(self._update_config, 'config.update') rpc.export(self._initial_update, 'config.initial_update') + if is_auth_enabled(): + rpc.allow('config.update', 'sync_agent_config') + rpc.allow('config.initial_update', 'sync_agent_config') core.onsetup.connect(onsetup, self) core.configuration.connect(self._onconfig, self) @@ -96,7 +103,7 @@ def onsetup(sender, **kwargs): def _onconfig(self, sender, **kwargs): if not self._initialized: try: - self._rpc().call(CONFIGURATION_STORE, "get_configs").get() + self._rpc().call(CONFIGURATION_STORE, "initialize_configs", self.vip_identity).get() except errors.Unreachable as e: _log.error("Connected platform does not support the Configuration Store feature.") return @@ -104,7 +111,6 @@ def _onconfig(self, sender, **kwargs): _log.error("Error retrieving agent configurations: {}".format(e)) return - affected_configs = {} for config_name in self._store: affected_configs[config_name] = "NEW" @@ -126,9 +132,8 @@ def _update_refs(self, config_name, contents): self._add_refs(config_name, contents) - def _delete_refs(self, config_name): - #Delete refs if they exist. + # Delete refs if they exist. old_refs = self._ref_map.pop(config_name, set()) for ref in old_refs: @@ -137,7 +142,6 @@ def _delete_refs(self, config_name): if not reverse_ref_set: del self._reverse_ref_map[ref] - def _initial_update(self, configs, reset_name_map=True): self._initialized = True self._store = {key.lower(): value for (key, value) in configs.items()} @@ -151,7 +155,6 @@ def _initial_update(self, configs, reset_name_map=True): if config_name not in self._store: self._add_refs(config_name, config_contents) - def _process_links(self, config_contents, already_gathered): if isinstance(config_contents, dict): for key, value in config_contents.items(): @@ -185,7 +188,6 @@ def _gather_child_configs(self, config_name, already_gathered): return config_contents - def _gather_config(self, config_name): config_contents = self._store.get(config_name) if config_contents is None: @@ -198,8 +200,6 @@ def _gather_config(self, config_name): return self._gather_child_configs(config_name, already_configured) - - def _gather_affected(self, config_name, seen_dict): reverse_refs = self._reverse_ref_map[config_name] for ref in reverse_refs: @@ -207,16 +207,15 @@ def _gather_affected(self, config_name, seen_dict): seen_dict[ref] = "UPDATE" self._gather_affected(ref, seen_dict) - def _update_config(self, action, config_name, contents=None, trigger_callback=False): """Called by the platform to push out configuration changes.""" - #If we haven't yet grabbed the initial callback state we just bail. + # If we haven't yet grabbed the initial callback state we just bail. if not self._initialized: return affected_configs = {} - #Update local store. + # Update local store. if action == "DELETE": config_name_lower = config_name.lower() if config_name_lower in self._store: @@ -234,7 +233,7 @@ def _update_config(self, action, config_name, contents=None, trigger_callback=Fa if action == "DELETE_ALL": for name in self._store: affected_configs[name] = "DELETE" - #Just assume all default stores updated. + # Just assume all default stores updated. for name in self._default_store: affected_configs[name] = "UPDATE" self._ref_map = {} @@ -251,7 +250,6 @@ def _update_config(self, action, config_name, contents=None, trigger_callback=Fa self._update_refs(config_name_lower, self._store[config_name_lower]) self._gather_affected(config_name_lower, affected_configs) - if trigger_callback and self._initial_callbacks_called: self._process_callbacks(affected_configs) @@ -261,13 +259,11 @@ def _update_config(self, action, config_name, contents=None, trigger_callback=Fa if action == "DELETE_ALL": self._name_map.clear() - - def _process_callbacks(self, affected_configs): _log.debug("Processing callbacks for affected files: {}".format(affected_configs)) all_map = self._default_name_map.copy() all_map.update(self._name_map) - #Always process "config" first. + # Always process "config" first. if "config" in affected_configs: self._process_callbacks_one_config("config", affected_configs["config"], all_map) @@ -276,7 +272,6 @@ def _process_callbacks(self, affected_configs): continue self._process_callbacks_one_config(config_name, action, all_map) - def _process_callbacks_one_config(self, config_name, action, name_map): callbacks = set() for pattern, actions in self._subscriptions.items(): @@ -307,7 +302,7 @@ def list(self): # Handle case were we are called during "onstart". if not self._initialized: try: - self._rpc().call(CONFIGURATION_STORE, "get_configs").get() + self._rpc().call(CONFIGURATION_STORE, "initialize_configs", self.vip_identity).get() except errors.Unreachable as e: _log.error("Connected platform does not support the Configuration Store feature.") except errors.VIPError as e: @@ -333,13 +328,13 @@ def get(self, config_name="config"): :Return Values: The contents of the configuration specified. """ - #Handle case were we are called during "onstart". + # Handle case were we are called during "onstart". - #If we fail to initialize we don't raise an exception as there still - #may be a default configuration to grab. + # If we fail to initialize we don't raise an exception as there still + # may be a default configuration to grab. if not self._initialized: try: - self._rpc().call(CONFIGURATION_STORE, "get_configs").get() + self._rpc().call(CONFIGURATION_STORE, "initialize_configs", self.vip_identity).get() except errors.Unreachable as e: _log.error("Connected platform does not support the Configuration Store feature.") except errors.VIPError as e: @@ -352,14 +347,13 @@ def get(self, config_name="config"): def _check_call_from_process_callbacks(self): frame_records = inspect.stack() try: - #Don't create any unneeded references to frame objects. + # Don't create any unneeded references to frame objects. for frame, *_ in frame_records: if self._process_callbacks_code_object is frame.f_code: raise RuntimeError("Cannot request changes to the config store from a configuration callback.") finally: del frame_records - def set(self, config_name, contents, trigger_callback=False, send_update=True): """Called to set the contents of a configuration. @@ -370,6 +364,8 @@ def set(self, config_name, contents, trigger_callback=False, send_update=True): :param config_name: Name of configuration to add to store. :param contents: Contents of the configuration. May be a string, dictionary, or list. :param trigger_callback: Tell the platform to trigger callbacks on the agent for this change. + :param send_update: Boolean flag to tell the server if it should call config.update on this agent + after server side update is done :type config_name: str :type contents: str, dict, list @@ -377,9 +373,17 @@ def set(self, config_name, contents, trigger_callback=False, send_update=True): """ self._check_call_from_process_callbacks() - self._rpc().call(CONFIGURATION_STORE, "set_config", config_name, contents, - trigger_callback=trigger_callback, - send_update=send_update).get(timeout=10.0) + if isinstance(contents, (dict, list)): + config_type = 'json' + raw_data = jsonapi.dumps(contents) + elif isinstance(contents, str): + config_type = 'raw' + raw_data = contents + else: + raise ValueError("Unsupported configuration content type: {}".format(str(type(contents)))) + + self._rpc().call(CONFIGURATION_STORE, "set_config", self.vip_identity, config_name, raw_data, + config_type, trigger_callback=trigger_callback, send_update=send_update).get(timeout=10.0) def set_default(self, config_name, contents): """Called to set the contents of a default configuration file. Default configurations are used if the @@ -427,7 +431,6 @@ def delete_default(self, config_name): self._update_refs(config_name_lower, self._store[config_name_lower]) - def delete(self, config_name, trigger_callback=False, send_update=True): """Delete a configuration by name. May not be called from a callback as this will cause deadlock with the platform. Will produce a runtime error if done so. @@ -439,7 +442,7 @@ def delete(self, config_name, trigger_callback=False, send_update=True): """ self._check_call_from_process_callbacks() - self._rpc().call(CONFIGURATION_STORE, "delete_config", config_name, + self._rpc().call(CONFIGURATION_STORE, "delete_config", self.vip_identity, config_name, trigger_callback=trigger_callback, send_update=send_update).get(timeout=10.0) @@ -447,7 +450,8 @@ def subscribe(self, callback, actions=VALID_ACTIONS, pattern="*"): """Subscribe to changes to a configuration. :param callback: Function to call in response to changes to a configuration. - :param actions: Change actions to respond to. Valid values are "NEW", "UPDATE", and "DELETE". May be a single action or a list of actions. + :param actions: Change actions to respond to. Valid values are "NEW", "UPDATE", and "DELETE". + May be a single action or a list of actions. :param pattern: Configuration name pattern to match to. Uses Unix style filename pattern matching. :type callback: str @@ -459,9 +463,9 @@ def subscribe(self, callback, actions=VALID_ACTIONS, pattern="*"): actions = set(action.upper() for action in actions) - invalid_actions = actions - VALID_ACTIONS - if (invalid_actions): - raise ValueError("Invalid actions: " + list(invalid_actions)) + invalid_actions = actions - set(VALID_ACTIONS) + if invalid_actions: + raise ValueError(f"Invalid actions: {invalid_actions}") pattern = pattern.lower() diff --git a/volttron/platform/vip/agent/subsystems/heartbeat.py b/volttron/platform/vip/agent/subsystems/heartbeat.py index c8d9688fea..48fb4542da 100644 --- a/volttron/platform/vip/agent/subsystems/heartbeat.py +++ b/volttron/platform/vip/agent/subsystems/heartbeat.py @@ -45,7 +45,7 @@ from volttron.platform.agent.utils import (get_aware_utc_now, format_timestamp) from volttron.platform.scheduling import periodic -from ..errors import Unreachable, VIPError +from ..errors import Unreachable """The heartbeat subsystem adds an optional periodic publish to all agents. Heartbeats can be started with agents and toggled on and off at runtime. diff --git a/volttron/platform/vip/agent/subsystems/hello.py b/volttron/platform/vip/agent/subsystems/hello.py index d515c7b7c9..7a594c29d6 100644 --- a/volttron/platform/vip/agent/subsystems/hello.py +++ b/volttron/platform/vip/agent/subsystems/hello.py @@ -42,7 +42,6 @@ import weakref from .base import SubsystemBase -from ..errors import VIPError from ..results import ResultsDictionary from zmq import ZMQError from zmq.green import ENOTSOCK diff --git a/volttron/platform/vip/agent/subsystems/pubsub.py b/volttron/platform/vip/agent/subsystems/pubsub.py index 186b7fe776..ed558f4cb9 100644 --- a/volttron/platform/vip/agent/subsystems/pubsub.py +++ b/volttron/platform/vip/agent/subsystems/pubsub.py @@ -44,22 +44,19 @@ import random import re import weakref -import sys import gevent from zmq import green as zmq from zmq import SNDMORE from volttron.platform import jsonapi -from volttron.utils.frame_serialization import serialize_frames from .base import SubsystemBase from ..decorators import annotate, annotations, dualmethod, spawn -from ..errors import Unreachable, VIPError, UnknownSubsystem +from ..errors import Unreachable from .... import jsonrpc -from volttron.platform.agent import utils + from ..results import ResultsDictionary -from gevent.queue import Queue, Empty +from gevent.queue import Queue from collections import defaultdict -from datetime import timedelta __all__ = ['PubSub'] diff --git a/volttron/platform/vip/agent/subsystems/rpc.py b/volttron/platform/vip/agent/subsystems/rpc.py index 00ec7314af..f567f59036 100644 --- a/volttron/platform/vip/agent/subsystems/rpc.py +++ b/volttron/platform/vip/agent/subsystems/rpc.py @@ -48,16 +48,13 @@ import gevent.local from gevent.event import AsyncResult from volttron.platform import jsonapi -from volttron.platform.agent.utils import get_messagebus from .base import SubsystemBase -from ..errors import VIPError from ..results import counter, ResultsDictionary from ..decorators import annotate, annotations, dualmethod, spawn from .... import jsonrpc -from volttron.platform.vip.socket import Message -from zmq import Frame, NOBLOCK, ZMQError, EINVAL, EHOSTUNREACH +from zmq import ZMQError from zmq.green import ENOTSOCK @@ -307,9 +304,9 @@ def _add_auth_check(self, method, required_caps): def checked_method(*args, **kwargs): user = str(self.context.vip_message.user) if self._message_bus == "rmq": - # When we address issue #2107 external platform user should - # have instance name also included in username. - user = user.split(".")[1] + # remove platform instance name. rmq user names are of the format . + user = user[user.index(".")+1:] + user_capabilites = self._owner.vip.auth.get_capabilities(user) _log.debug("**user caps is: {}".format(user_capabilites)) if user_capabilites: diff --git a/volttron/platform/vip/agent/subsystems/web.py b/volttron/platform/vip/agent/subsystems/web.py index 98ec30d764..d8e3a63d6a 100644 --- a/volttron/platform/vip/agent/subsystems/web.py +++ b/volttron/platform/vip/agent/subsystems/web.py @@ -36,7 +36,6 @@ # under Contract DE-AC05-76RL01830 # }}} -from collections import defaultdict import logging import weakref from enum import Enum @@ -223,9 +222,8 @@ def _opened(self, fromip, endpoint): if callbacks is None: _log.error('Websocket endpoint {} is not available'.format( endpoint)) - else: - if callbacks[0]: - return callbacks[0](fromip, endpoint) + elif callbacks[0]: + return callbacks[0](fromip, endpoint) return False @@ -236,9 +234,8 @@ def _closed(self, endpoint): if callbacks is None: _log.error('Websocket endpoint {} is not available'.format( endpoint)) - else: - if callbacks[1]: - callbacks[1](endpoint) + elif callbacks[1]: + callbacks[1](endpoint) def _message(self, endpoint, message): @@ -246,6 +243,5 @@ def _message(self, endpoint, message): if callbacks is None: _log.error('Websocket endpoint {} is not available'.format( endpoint)) - else: - if callbacks[2]: - callbacks[2](endpoint, message) + elif callbacks[2]: + callbacks[2](endpoint, message) diff --git a/volttron/platform/vip/green.py b/volttron/platform/vip/green.py index cb7b4f6e15..3d103ed115 100644 --- a/volttron/platform/vip/green.py +++ b/volttron/platform/vip/green.py @@ -58,7 +58,6 @@ from zmq.green import NOBLOCK, POLLOUT from zmq import green as _green -from . import * from .router import BaseRouter as _BaseRouter from .socket import _Socket diff --git a/volttron/platform/vip/keydiscovery.py b/volttron/platform/vip/keydiscovery.py index 971c9f27f9..71853e45a0 100644 --- a/volttron/platform/vip/keydiscovery.py +++ b/volttron/platform/vip/keydiscovery.py @@ -47,7 +47,7 @@ from requests.exceptions import HTTPError, Timeout from volttron.platform.agent import utils -from .agent import Agent, Core, RPC +from .agent import Agent, Core from requests.packages.urllib3.connection import (ConnectionError, NewConnectionError) from urllib.parse import urlparse, urljoin diff --git a/volttron/platform/vip/pubsubservice.py b/volttron/platform/vip/pubsubservice.py index 7902c9b7d2..0da7405885 100644 --- a/volttron/platform/vip/pubsubservice.py +++ b/volttron/platform/vip/pubsubservice.py @@ -53,7 +53,6 @@ from volttron.utils.frame_serialization import serialize_frames green.Context._instance = green.Context.shadow(zmq.Context.instance().underlying) -from volttron.platform import get_home from .agent.subsystems.pubsub import ProtectedPubSubTopics from volttron.platform.jsonrpc import (INVALID_REQUEST, UNAUTHORIZED) from volttron.platform import jsonapi @@ -598,7 +597,7 @@ def _load_protected_topics(self, topics_data): self._logger.exception('invalid format for protected topics ') else: self._protected_topics = topics - self._logger.info('protected-topics loaded') + self._logger.debug('protected-topics loaded') def handle_subsystem(self, frames, user_id=''): """ diff --git a/volttron/platform/web/platform_web_service.py b/volttron/platform/web/platform_web_service.py index 50fe254ed9..a0b2db1155 100644 --- a/volttron/platform/web/platform_web_service.py +++ b/volttron/platform/web/platform_web_service.py @@ -491,7 +491,7 @@ def app_routing(self, env, start_response): retvalue = v(env, start_response, data) except TypeError: response = v(env, data) - _log.debug(f'VUI: Response at app_routing is: {response.response}') + #_log.debug(f'VUI: Response at app_routing is: {response.response}') return response(env, start_response) # retvalue = self.process_response(start_response, v(env, data)) @@ -806,7 +806,7 @@ def startupagent(self, sender, **kwargs): # Register VUI endpoints: self._vui_endpoints = VUIEndpoints(self) - _log.debug(f'VUI: adding routes - {self._vui_endpoints.get_routes()}') + #_log.debug(f'VUI: adding routes - {self._vui_endpoints.get_routes()}') self.registeredroutes.extend(self._vui_endpoints.get_routes()) # Allow authentication endpoint from any https connection diff --git a/volttron/platform/web/topic_tree.py b/volttron/platform/web/topic_tree.py index 26a1c940a0..fedc0e6820 100644 --- a/volttron/platform/web/topic_tree.py +++ b/volttron/platform/web/topic_tree.py @@ -151,17 +151,17 @@ def devices(self, nid=None): def from_store(cls, platform, rpc_caller): # TODO: Duplicate logic for external_platform check from VUIEndpoints to remove reference to it from here. kwargs = {'external_platform': platform} if 'VUIEndpoints' in rpc_caller.__repr__() else {} - devices = rpc_caller(CONFIGURATION_STORE, 'manage_list_configs', 'platform.driver', **kwargs) + devices = rpc_caller(CONFIGURATION_STORE, 'list_configs', 'platform.driver', **kwargs) devices = devices if kwargs else devices.get(timeout=5) devices = [d for d in devices if re.match('^devices/.*', d)] device_tree = cls(devices) for d in devices: - dev_config = rpc_caller(CONFIGURATION_STORE, 'manage_get', 'platform.driver', d, raw=False, **kwargs) + dev_config = rpc_caller(CONFIGURATION_STORE, 'get_config', 'platform.driver', d, raw=False, **kwargs) # TODO: If not AsyncResponse instead of if kwargs dev_config = dev_config if kwargs else dev_config.get(timeout=5) reg_cfg_name = dev_config.pop('registry_config')[len('config://'):] device_tree.update_node(d, data=dev_config, segment_type='DEVICE') - registry_config = rpc_caller('config.store', 'manage_get', 'platform.driver', + registry_config = rpc_caller('config.store', 'get_config', 'platform.driver', f'{reg_cfg_name}', raw=False, **kwargs) registry_config = registry_config if kwargs else registry_config.get(timeout=5) for pnt in registry_config: diff --git a/volttron/platform/web/vui_endpoints.py b/volttron/platform/web/vui_endpoints.py index 50e1676c86..f27fbca646 100644 --- a/volttron/platform/web/vui_endpoints.py +++ b/volttron/platform/web/vui_endpoints.py @@ -331,15 +331,15 @@ def handle_platforms_agents_configs(self, env: dict, data: dict) -> Response: try: if no_config_name: if vip_identity != '-': - setting_list = self._rpc('config.store', 'manage_list_configs', vip_identity, + setting_list = self._rpc('config.store', 'list_configs', vip_identity, external_platform=platform) route_dict = self._links(path_info, setting_list) return Response(json.dumps(route_dict), 200, content_type='application/json') else: - list_of_agents = self._rpc('config.store', 'manage_list_stores', external_platform=platform) + list_of_agents = self._rpc('config.store', 'list_stores', external_platform=platform) return Response(json.dumps(list_of_agents), 200, content_type='application/json') elif not no_config_name: - setting_dict = self._rpc('config.store', 'manage_get', vip_identity, config_name, + setting_dict = self._rpc('config.store', 'get_config', vip_identity, config_name, external_platform=platform) return Response(json.dumps(setting_dict), 200, content_type='application/json') except RemoteError as e: @@ -356,7 +356,7 @@ def handle_platforms_agents_configs(self, env: dict, data: dict) -> Response: elif request_method == 'POST' and no_config_name: if config_type in ['application/json', 'text/csv', 'text/plain']: - setting_list = self._rpc('config.store', 'manage_list_configs', vip_identity, + setting_list = self._rpc('config.store', 'list_configs', vip_identity, external_platform=platform) if config_name in setting_list: e = {'Error': f'Configuration: "{config_name}" already exists for agent: "{vip_identity}"'} @@ -373,13 +373,13 @@ def handle_platforms_agents_configs(self, env: dict, data: dict) -> Response: elif request_method == 'DELETE': if no_config_name: try: - self._rpc('config.store', 'manage_delete_store', vip_identity, external_platform=platform) + self._rpc('config.store', 'delete_store', vip_identity, external_platform=platform) return Response(None, 204, content_type='application/json') except RemoteError as e: return Response(json.dumps({"Error": f"{e}"}), 400, content_type='application/json') else: try: - self._rpc('config.store', 'manage_delete_config', vip_identity, config_name, + self._rpc('config.store', 'delete_config', vip_identity, config_name, external_platform=platform) return Response(None, 204, content_type='application/json') except RemoteError as e: @@ -998,7 +998,7 @@ def _insert_config(self, config_type, data, vip_identity, config_name, platform) 'text/csv'] else 'raw' if config_type == 'json': data = json.dumps(data) - self._rpc('config.store', 'manage_store', vip_identity, config_name, data, config_type, + self._rpc('config.store', 'set_config', vip_identity, config_name, data, config_type, external_platform=platform) return None diff --git a/volttron/utils/rmq_config_params.py b/volttron/utils/rmq_config_params.py index 2e8c247e64..c68b14538d 100644 --- a/volttron/utils/rmq_config_params.py +++ b/volttron/utils/rmq_config_params.py @@ -88,7 +88,7 @@ def __init__(self): with open(os.path.expanduser("~/.volttron_rmq_home")) as f: self.rabbitmq_server = f.read().strip() else: - self.rabbitmq_server = os.path.expanduser("~/rabbitmq_server/rabbitmq_server-3.9.7/") + self.rabbitmq_server = os.path.expanduser("~/rabbitmq_server/rabbitmq_server-3.9.29/") assert os.path.isdir(self.rabbitmq_server), "Missing rabbitmq server directory{}".format(self.rabbitmq_server) from volttron.platform.auth import certs @@ -118,7 +118,7 @@ def _set_default_config(self): self.config_opts.setdefault('reconnect-delay', 30) self.config_opts.setdefault('user', self.instance_name + '-admin') rmq_home = os.path.join(os.path.expanduser("~"), - "rabbitmq_server/rabbitmq_server-3.9.7") + "rabbitmq_server/rabbitmq_server-3.9.29") self.config_opts.setdefault('rabbitmq-service', False) self.config_opts.setdefault("rmq-home", rmq_home) diff --git a/volttron/utils/rmq_setup.py b/volttron/utils/rmq_setup.py index 66fbb7e530..f018a9a522 100644 --- a/volttron/utils/rmq_setup.py +++ b/volttron/utils/rmq_setup.py @@ -59,7 +59,7 @@ from volttron.platform import get_home from volttron.platform.agent.utils import (store_message_bus_config, execute_command) -from volttron.utils.prompt import prompt_response, y, n, y_or_n +from volttron.utils.prompt import prompt_response, y, y_or_n from volttron.platform import jsonapi from urllib.parse import urlparse from volttron.platform.agent.utils import get_platform_instance_name, get_fq_identity @@ -90,13 +90,13 @@ def _start_rabbitmq_without_ssl(rmq_config, conf_file, env=None): rmq_home = rmq_config.rmq_home if not rmq_home: rmq_home = os.path.join(os.path.expanduser("~"), - "rabbitmq_server/rabbitmq_server-3.9.7") + "rabbitmq_server/rabbitmq_server-3.9.29") if os.path.exists(rmq_home): os.environ['RABBITMQ_HOME'] = rmq_home else: _log.error("\nMissing key 'rmq_home' in RabbitMQ config and RabbitMQ is " "not installed in default path: \n" - "~/rabbitmq_server/rabbitmq_server-3.9.7 \n" + "~/rabbitmq_server/rabbitmq_server-3.9.29 \n" "Please set the correct RabbitMQ installation path in " "rabbitmq_config.yml") exit(1) @@ -1543,7 +1543,7 @@ def start_rabbit(rmq_home, env=None): execute_command(status_cmd, env=env) if not start: # if we have attempted started already - gevent.sleep(1) # give a second just to be sure + gevent.sleep(2) # give couple of seconds just to be sure started = True _log.info("Rmq server at {} is running at ".format(rmq_home)) except RuntimeError as e: diff --git a/volttrontesting/README.md b/volttrontesting/README.md index 0659841aca..dcb5eaabea 100644 --- a/volttrontesting/README.md +++ b/volttrontesting/README.md @@ -13,20 +13,20 @@ the VOLTTRON repository. ``` # Execute all tests throughout the repository -py.test +pytest # Execute a specific directory of tests recursively from the # specified directory. -py.test examples/ListenerAgent +pytest examples/ListenerAgent # Execute only tests that are marked as slow -py.test -m slow +pytest -m slow # Execute tests that are not marked as slow -py.test -m "not slow" +pytest -m "not slow" # Execute only zmq tests -py.test -m zmq +pytest -m zmq ``` ## Notes diff --git a/volttrontesting/fixtures/rmq_test_setup.py b/volttrontesting/fixtures/rmq_test_setup.py index 464179dfb8..a69a6373b1 100644 --- a/volttrontesting/fixtures/rmq_test_setup.py +++ b/volttrontesting/fixtures/rmq_test_setup.py @@ -34,7 +34,7 @@ def __init__(self): # This is overwritten in the class below during # the create_rmq_volttron_setup function, but is # left here for completeness of the configuration. - 'rmq-home': '~/rabbitmq_server-3.9.7', + 'rmq-home': '~/rabbitmq_server-3.9.29', 'reconnect-delay': 5 } diff --git a/volttrontesting/fixtures/volttron_platform_fixtures.py b/volttrontesting/fixtures/volttron_platform_fixtures.py index 1b905f9963..5264c4b4fd 100644 --- a/volttrontesting/fixtures/volttron_platform_fixtures.py +++ b/volttrontesting/fixtures/volttron_platform_fixtures.py @@ -62,14 +62,22 @@ def cleanup_wrapper(wrapper): # if wrapper.is_running(): # wrapper.remove_all_agents() # Shutdown handles case where the platform hasn't started. + if not wrapper.is_running(): + return + wrapper_pid = wrapper.p_process.pid wrapper.shutdown_platform() if wrapper.p_process is not None: - if psutil.pid_exists(wrapper.p_process.pid): - proc = psutil.Process(wrapper.p_process.pid) + if psutil.pid_exists(wrapper_pid): + proc = psutil.Process(wrapper_pid) proc.terminate() if not wrapper.debug_mode: assert not Path(wrapper.volttron_home).parent.exists(), \ f"{str(Path(wrapper.volttron_home).parent)} wasn't cleaned!" + if not wrapper.debug_mode: + assert not Path(wrapper.volttron_home).exists() + # Final way to kill off the platform wrapper for the tests. + if psutil.pid_exists(wrapper_pid): + psutil.Process(wrapper_pid).kill() def cleanup_wrappers(platforms): @@ -117,7 +125,7 @@ def volttron_instance_module_web(request): params=[ dict(messagebus='zmq'), pytest.param(dict(messagebus='rmq', ssl_auth=True), marks=rmq_skipif), - dict(messagebus='zmq', auth_enabled=False), + dict(messagebus='zmq', auth_enabled=False) ]) def volttron_instance(request, **kwargs): """Fixture that returns a single instance of volttron platform for testing @@ -126,12 +134,14 @@ def volttron_instance(request, **kwargs): @return: volttron platform instance """ address = kwargs.pop("vip_address", get_rand_vip()) + if request.param['messagebus'] == 'rmq': + kwargs['timeout'] = 120 + wrapper = build_wrapper(address, - messagebus=request.param.pop('messagebus', 'zmq'), - ssl_auth=request.param.pop('ssl_auth', False), - auth_enabled=request.param.pop('auth_enabled', True), + messagebus=request.param.get('messagebus', 'zmq'), + ssl_auth=request.param.get('ssl_auth', False), + auth_enabled=request.param.get('auth_enabled', True), **kwargs) - wrapper_pid = wrapper.p_process.pid try: yield wrapper @@ -139,11 +149,6 @@ def volttron_instance(request, **kwargs): print(ex.args) finally: cleanup_wrapper(wrapper) - if not wrapper.debug_mode: - assert not Path(wrapper.volttron_home).exists() - # Final way to kill off the platform wrapper for the tests. - if psutil.pid_exists(wrapper_pid): - psutil.Process(wrapper_pid).kill() # Use this fixture to get more than 1 volttron instance for test. @@ -182,8 +187,9 @@ def get_n_volttron_instances(n, should_start=True, **kwargs): address = kwargs.pop("vip_address", get_rand_vip()) wrapper = build_wrapper(address, should_start=should_start, - messagebus=request.param.pop('messagebus', 'zmq'), - ssl_auth=request.param.pop('ssl_auth', False), + messagebus=request.param.get('messagebus', 'zmq'), + ssl_auth=request.param.get('ssl_auth', False), + auth_enabled=request.param.get('auth_enabled', True), **kwargs) instances.append(wrapper) if should_start: @@ -196,17 +202,10 @@ def get_n_volttron_instances(n, should_start=True, **kwargs): def cleanup(): nonlocal instances - print(f"My instances: {get_n_volttron_instances.count}") - if isinstance(get_n_volttron_instances.instances, PlatformWrapper): - print('Shutting down instance: {}'.format( - get_n_volttron_instances.instances)) - cleanup_wrapper(get_n_volttron_instances.instances) - return - - for i in range(0, get_n_volttron_instances.count): + for i in range(0, len(instances)): print('Shutting down instance: {}'.format( - get_n_volttron_instances.instances[i].volttron_home)) - cleanup_wrapper(get_n_volttron_instances.instances[i]) + instances[i].volttron_home)) + cleanup_wrapper(instances[i]) try: yield get_n_volttron_instances @@ -216,20 +215,31 @@ def cleanup(): # Use this fixture when you want a single instance of volttron platform for zmq message bus # test -@pytest.fixture(scope="module") -def volttron_instance_zmq(): +@pytest.fixture(scope="module", + params=[ + dict(messagebus='zmq'), + dict(messagebus='zmq', auth_enabled=False) + ]) +def volttron_instance_zmq(request, **kwargs): """Fixture that returns a single instance of volttron platform for testing @param request: pytest request object @return: volttron platform instance """ - address = get_rand_vip() - - wrapper = build_wrapper(address) + address = kwargs.pop("vip_address", get_rand_vip()) - yield wrapper + wrapper = build_wrapper(address, + messagebus=request.param.get('messagebus', 'zmq'), + ssl_auth=request.param.get('ssl_auth', False), + auth_enabled=request.param.get('auth_enabled', True), + **kwargs) - cleanup_wrapper(wrapper) + try: + yield wrapper + except Exception as ex: + print(ex.args) + finally: + cleanup_wrapper(wrapper) # Use this fixture when you want a single instance of volttron platform for rmq message bus diff --git a/volttrontesting/multiplatform/test_federation.py b/volttrontesting/multiplatform/test_federation.py index 46243aa25f..838b4e2c2e 100644 --- a/volttrontesting/multiplatform/test_federation.py +++ b/volttrontesting/multiplatform/test_federation.py @@ -51,6 +51,8 @@ if not is_rabbitmq_available(): pytest.skip("Pika is not installed", allow_module_level=True) + +@pytest.mark.timeout(600) @pytest.mark.federation def test_federation_pubsub(federated_rmq_instances): upstream, downstream = federated_rmq_instances diff --git a/volttrontesting/multiplatform/test_multiplatform_pubsub.py b/volttrontesting/multiplatform/test_multiplatform_pubsub.py index a9a86cee18..88c35d9d4a 100644 --- a/volttrontesting/multiplatform/test_multiplatform_pubsub.py +++ b/volttrontesting/multiplatform/test_multiplatform_pubsub.py @@ -247,6 +247,7 @@ def test_multiplatform_pubsub(request, multi_platform_connection): assert message == [{'point': 'value'}] +@pytest.mark.timeout(600) @pytest.mark.multiplatform def test_multiplatform_2_publishers(request, five_platform_connection): subscription_results2 = {} @@ -544,14 +545,14 @@ def test_multiplatform_configstore_rpc(request, get_volttron_instances): test_agent = p2.build_agent() kwargs = {"external_platform": p1.instance_name} test_agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_store', + 'set_config', 'platform.thresholddetection', 'config', jsonapi.dumps(updated_config), 'json', **kwargs).get(timeout=10) config = test_agent.vip.rpc.call(CONFIGURATION_STORE, - 'manage_get', + 'get_config', 'platform.thresholddetection', 'config', raw=True, diff --git a/volttrontesting/multiplatform/test_shovel.py b/volttrontesting/multiplatform/test_shovel.py index 76afbefe58..67d487409f 100644 --- a/volttrontesting/multiplatform/test_shovel.py +++ b/volttrontesting/multiplatform/test_shovel.py @@ -259,6 +259,7 @@ def two_way_shovel_connection(request, **kwargs): sink.shutdown_platform() +@pytest.mark.timeout(800) @pytest.mark.shovel def test_shovel_pubsub(shovel_pubsub_rmq_instances): source, sink = shovel_pubsub_rmq_instances @@ -286,6 +287,7 @@ def callback2(peer, sender, bus, topic, headers, message): assert message == [{'point': 'value'}] +@pytest.mark.timeout(600) @pytest.mark.shovel def test_shovel_rpc(two_way_shovel_connection): instance_1, instance_2 = two_way_shovel_connection diff --git a/volttrontesting/platform/base_market_agent/test_poly_line.py b/volttrontesting/platform/base_market_agent/test_poly_line.py index e711093162..0032345bd5 100644 --- a/volttrontesting/platform/base_market_agent/test_poly_line.py +++ b/volttrontesting/platform/base_market_agent/test_poly_line.py @@ -47,55 +47,55 @@ @pytest.mark.market def test_poly_line_min(): - min = PolyLine.min(1,2) + min = PolyLine.min(1, 2) assert min == 1 @pytest.mark.market def test_poly_line_min_first_none(): - min = PolyLine.min(None,2) + min = PolyLine.min(None, 2) assert min == 2 @pytest.mark.market def test_poly_line_min_second_none(): - min = PolyLine.min(1,None) + min = PolyLine.min(1, None) assert min == 1 @pytest.mark.market def test_poly_line_max(): - max = PolyLine.max(1,2) + max = PolyLine.max(1, 2) assert max == 2 @pytest.mark.market def test_poly_line_max_first_none(): - max = PolyLine.max(None,2) + max = PolyLine.max(None, 2) assert max == 2 @pytest.mark.market def test_poly_line_max_second_none(): - max = PolyLine.max(1,None) + max = PolyLine.max(1, None) assert max == 1 @pytest.mark.market def test_poly_line_sum(): - sum = PolyLine.sum(1,2) + sum = PolyLine.sum(1, 2) assert sum == 3 @pytest.mark.market def test_poly_line_sum_first_none(): - sum = PolyLine.sum(None,2) + sum = PolyLine.sum(None, 2) assert sum == 2 @pytest.mark.market def test_poly_line_sum_second_none(): - sum = PolyLine.sum(1,None) + sum = PolyLine.sum(1, None) assert sum == 1 @@ -108,23 +108,23 @@ def test_poly_line_init_points_none(): @pytest.mark.market def test_poly_line_add_one_point(): line = PolyLine() - line.add(Point(4,8)) + line.add(Point(4, 8)) assert len(line.points) == 1 @pytest.mark.market def test_poly_line_add_two_points(): line = PolyLine() - line.add(Point(4,8)) - line.add(Point(2,4)) + line.add(Point(4, 8)) + line.add(Point(2, 4)) assert len(line.points) == 2 @pytest.mark.market def test_poly_line_add_points_is_sorted(): line = PolyLine() - line.add(Point(4,8)) - line.add(Point(2,4)) + line.add(Point(4, 8)) + line.add(Point(2, 4)) assert line.points[0].x == 2 @@ -156,10 +156,10 @@ def create_supply_curve(): supply_curve = PolyLine() price = 0 quantity = 0 - supply_curve.add(Point(price,quantity)) + supply_curve.add(Point(price, quantity)) price = 1000 quantity = 1000 - supply_curve.add(Point(price,quantity)) + supply_curve.add(Point(price, quantity)) return supply_curve @@ -167,8 +167,8 @@ def create_demand_curve(): demand_curve = PolyLine() price = 0 quantity = 1000 - demand_curve.add(Point(price,quantity)) + demand_curve.add(Point(price, quantity)) price = 1000 quantity = 0 - demand_curve.add(Point(price,quantity)) + demand_curve.add(Point(price, quantity)) return demand_curve diff --git a/volttrontesting/platform/control_tests/test_control.py b/volttrontesting/platform/control_tests/test_control.py index 987c33a0ac..5477b92df3 100644 --- a/volttrontesting/platform/control_tests/test_control.py +++ b/volttrontesting/platform/control_tests/test_control.py @@ -9,7 +9,7 @@ from volttron.platform.jsonrpc import RemoteError import sys - +@pytest.mark.timeout(600) @pytest.mark.control def test_agent_versions(volttron_instance): auuid = volttron_instance.install_agent( @@ -111,13 +111,14 @@ def test_prioritize_agent_valid_input(volttron_instance): assert cn.vip.rpc.call('control', 'prioritize_agent', auuid, '99').get(timeout=2) is None -@pytest.mark.xfail(reason="bytes() calls (control.py:390|398) raise: TypeError('string argument without an encoding').") @pytest.mark.parametrize('uuid, priority, expected', [ - (34, '50', "expected a string for 'uuid'"), + pytest.param(34, '50', "expected a string for 'uuid'", + marks=pytest.mark.xfail(reason="bytes() calls raise: TypeError(string argument without an encoding)")), ('34/7', '50', 'invalid agent'), ('.', '50', 'invalid agent'), ('..', '50', 'invalid agent'), - ('foo', 2, "expected a string or null for 'priority'"), + pytest.param('foo', 2, "expected a string or null for 'priority'", + marks=pytest.mark.xfail(reason="bytes() calls raise: TypeError(string argument without an encoding)")), ('foo', '-1', 'Priority must be an integer from 0 - 99.'), ('foo', '4.5', 'Priority must be an integer from 0 - 99.'), ('foo', '100', 'Priority must be an integer from 0 - 99.'), @@ -130,15 +131,16 @@ def test_prioritize_agent_invalid_input(volttron_instance, uuid, priority, expec assert expected in e.value.message +@pytest.mark.timeout(600) @pytest.mark.control -def test_recover_from_crash(get_volttron_instances): +def test_recover_from_crash(volttron_instance): """ Test if control agent periodically monitors and restarts any crashed agents :param volttron_instance: :return: """ - - volttron_instance = get_volttron_instances(1, True, agent_monitor_frequency=10) + volttron_instance.stop_platform() + volttron_instance.startup_platform(volttron_instance.vip_address, agent_monitor_frequency=20) tmpdir = tempfile.mkdtemp() os.chdir(tmpdir) @@ -158,9 +160,9 @@ def __init__(self, config_path, **kwargs): super(CrashTestAgent, self).__init__(**kwargs) @Core.receiver('onstart') - def crash_after_five_seconds(self, sender, **kwargs): + def crash_after_test_seconds(self, sender, **kwargs): print("crash test agent on start") - gevent.sleep(5) + gevent.sleep(15) print("crash test agent quitting") sys.exit(5) @@ -204,8 +206,10 @@ def main(argv=sys.argv): wheel = os.path.join(tmpdir, "dist", "crashtest-0.1-py3-none-any.whl") assert os.path.exists(wheel) - agent_uuid = volttron_instance.install_agent(agent_wheel=wheel, start=True) + agent_uuid = volttron_instance.install_agent(agent_wheel=wheel) assert agent_uuid + gevent.sleep(1) + volttron_instance.start_agent(agent_uuid) query_agent = volttron_instance.dynamic_agent status = query_agent.vip.rpc.call("control", "agent_status", agent_uuid).get( timeout=2 @@ -216,9 +220,9 @@ def main(argv=sys.argv): wait_time = 0 # wait till it has not crashed and once crashed # wait till we detect a restart or 20 seconds. - # have to do this since the test agent is hardcoded to crash 5 + # have to do this since the test agent is hardcoded to crash 15 # seconds after start - while not crashed or (not restarted and wait_time < 30): + while not crashed or (not restarted and wait_time < 50): status = query_agent.vip.rpc.call("control", "agent_status", agent_uuid).get( timeout=2 ) diff --git a/volttrontesting/platform/control_tests/test_vctl_commands.py b/volttrontesting/platform/control_tests/test_vctl_commands.py index f8f6c55701..4ac7f164f8 100644 --- a/volttrontesting/platform/control_tests/test_vctl_commands.py +++ b/volttrontesting/platform/control_tests/test_vctl_commands.py @@ -30,6 +30,7 @@ def test_needs_connection(): except AssertionError: assert not stderr.decode("utf-8") +@pytest.mark.timeout(600) @pytest.mark.control def test_needs_connection_with_connection(volttron_instance: PlatformWrapper): # Verify peerlist command works when instance is running @@ -592,4 +593,3 @@ def test_vctl_start_stop_restart_should_not_fail_on_when_no_agents_are_installed with with_os_environ(volttron_instance.env): execute_command(["vctl", subcommand, valid_option], volttron_instance.env) assert not jsonapi.loads(execute_command(["vctl", "--json", "status"], volttron_instance.env)) - \ No newline at end of file diff --git a/volttrontesting/platform/dbutils/test_mysqlfuncts.py b/volttrontesting/platform/dbutils/test_mysqlfuncts.py index 67c78bb978..17d4ddc524 100644 --- a/volttrontesting/platform/dbutils/test_mysqlfuncts.py +++ b/volttrontesting/platform/dbutils/test_mysqlfuncts.py @@ -23,9 +23,7 @@ IMAGES = [ - "mysql:8.0", - "mysql:5.7.35", - "mysql:5.6" + "mysql:8.0" ] CONNECTION_HOST = "localhost" diff --git a/volttrontesting/platform/security/SecurityAgent/security/agent.py b/volttrontesting/platform/security/SecurityAgent/security/agent.py index fd251b2bfa..404924c937 100644 --- a/volttrontesting/platform/security/SecurityAgent/security/agent.py +++ b/volttrontesting/platform/security/SecurityAgent/security/agent.py @@ -244,7 +244,7 @@ def verify_config_store_access(self, agent2_identity): """ error = None try: - self.vip.rpc.call('config.store', 'manage_store', "security_agent", 'config', + self.vip.rpc.call('config.store', 'set_config', "security_agent", 'config', json.dumps({"name": "value"}), config_type='json').get(timeout=2) except Exception as e: error = str(e) @@ -253,12 +253,12 @@ def verify_config_store_access(self, agent2_identity): return error try: - self.vip.rpc.call('config.store', 'manage_store', agent2_identity, 'config', + self.vip.rpc.call('config.store', 'set_config', agent2_identity, 'config', json.dumps({"test": "value"}), config_type='json').get(timeout=10) error = "Security agent is able to edit config store entry of security_agent2" except Exception as e: error = e.message - if error == "User can call method manage_store only with identity=security_agent " \ + if error == "User can call method set_config only with identity=security_agent " \ "but called with identity={}".format(agent2_identity): error = None diff --git a/volttrontesting/platform/security/test_aip_security.py b/volttrontesting/platform/security/test_aip_security.py index 0a45c2740e..baa1981cf1 100644 --- a/volttrontesting/platform/security/test_aip_security.py +++ b/volttrontesting/platform/security/test_aip_security.py @@ -1,13 +1,12 @@ import pwd import gevent import pytest - +import os from mock import MagicMock from volttron.platform import is_rabbitmq_available from volttron.platform import get_services_core from volttron.platform.agent.utils import execute_command -from volttron.platform.vip.agent import * from volttrontesting.fixtures.volttron_platform_fixtures import build_wrapper, cleanup_wrapper, rmq_skipif from volttrontesting.utils.utils import get_rand_vip @@ -19,9 +18,14 @@ reason="Can't run on travis as this test needs root to run " "setup script before running test case") -# Run as root or sudo scripts/secure_user_permissions.sh for both the below instance names before running these tests -INSTANCE_NAME1 = "volttron1" -INSTANCE_NAME2 = "volttron2" +# IMPORTANT steps for running this test +# 1. Make sure your test environment has acl installed (sudo apt-get install acl) +# 2. Make sure the python executable is accessible by any user. This would mean read and execute access to all +# directories in the path. For example if python is in /user/home/env/bin/python, then do chmod r+x to /user, +# and /user/home, and /user/home/env/, and /user/home/env/bin and /user/home/env/bin/python. +# 3. Run as root or sudo scripts/secure_user_permissions.sh for both the below instance names before running these +INSTANCE_NAME1 = "svolttron1" +INSTANCE_NAME2 = "svolttron2" def get_agent_user_from_dir(agent_uuid, home): diff --git a/volttrontesting/platform/test_connection.py b/volttrontesting/platform/test_connection.py index b257c7d342..b5e69a4f47 100644 --- a/volttrontesting/platform/test_connection.py +++ b/volttrontesting/platform/test_connection.py @@ -8,12 +8,12 @@ @pytest.fixture(scope="module") -def setup_control_connection(request, get_volttron_instances): +def setup_control_connection(request, volttron_instance): """ Creates a single instance of VOLTTRON for testing purposes """ global wrapper, control_connection - wrapper = get_volttron_instances(1) + wrapper = volttron_instance request.addfinalizer(wrapper.shutdown_platform) @@ -27,14 +27,14 @@ def setup_control_connection(request, get_volttron_instances): ks = KeyStore() ks.generate() - control_connection = build_connection(identity="foo", - address=wrapper.vip_address, - peer=CONTROL, - serverkey=wrapper.serverkey, - publickey=ks.public, - secretkey=ks.secret, - instance_name=wrapper.instance_name, - message_bus=wrapper.messagebus) + control_connection = wrapper.build_connection(identity="foo", + address=wrapper.vip_address, + peer=CONTROL, + serverkey=wrapper.serverkey, + publickey=ks.public, + secretkey=ks.secret, + instance_name=wrapper.instance_name, + message_bus=wrapper.messagebus) # Sleep a couple seconds to wait for things to startup gevent.sleep(2) @@ -49,11 +49,12 @@ def test_can_connect_to_control(setup_control_connection): @pytest.mark.control -def test_can_get_peers(setup_control_connection): +def test_can_get_peers(setup_control_connection, volttron_instance): wrapper, connection = setup_control_connection peers = connection.peers() assert CONTROL in peers - assert AUTH in peers + if volttron_instance.auth_enabled: + assert AUTH in peers assert CONFIGURATION_STORE in peers diff --git a/volttrontesting/platform/test_core_agent.py b/volttrontesting/platform/test_core_agent.py index 45e265b308..548d814142 100644 --- a/volttrontesting/platform/test_core_agent.py +++ b/volttrontesting/platform/test_core_agent.py @@ -174,7 +174,9 @@ def setup_channel(self, channel_name): channel.close(linger=0) del channel - +# marking the first test with extra time out as starting RMQ instance for the first time takes longer and +# pytest.timeout applies not just for the test run alone but include fixture and clean up time +@pytest.mark.timeout(600) @pytest.mark.agent def test_channel_send_data(volttron_instance: PlatformWrapper): diff --git a/volttrontesting/platform/test_instance_setup.py b/volttrontesting/platform/test_instance_setup.py index 959864a576..d4a6961b1f 100644 --- a/volttrontesting/platform/test_instance_setup.py +++ b/volttrontesting/platform/test_instance_setup.py @@ -8,12 +8,15 @@ from volttron.platform.instance_setup import _is_agent_installed from volttron.utils import get_hostname from volttron.platform.agent.utils import is_volttron_running -from volttrontesting.fixtures.rmq_test_setup import create_rmq_volttron_setup from volttrontesting.utils.platformwrapper import create_volttron_home +from volttrontesting.utils.utils import get_rand_port HAS_RMQ = is_rabbitmq_available() RMQ_TIMEOUT = 600 +if HAS_RMQ: + from volttrontesting.fixtures.rmq_test_setup import create_rmq_volttron_setup + ''' Example variables to be used during each of the tests, depending on the prompts that will be asked @@ -40,7 +43,7 @@ is_vcp = "N" instance_name = "" vc_hostname = "" -vc_port = "8443" +vc_port = "" install_historian = "N" install_driver = "N" install_fake_device = "N" @@ -80,8 +83,9 @@ def test_zmq_case_no_agents(monkeypatch): config_path = os.path.join(vhome, "config") message_bus = "zmq" - vip_address = "tcp://127.0.0.15" - vip_port = "22916" + ip = "127.0.0.15" + vip_address = "tcp://" + ip + vip_port = str(get_rand_port(ip)) instance_name = "test_zmq" is_web_enabled = "N" is_vcp = "N" @@ -116,7 +120,7 @@ def test_zmq_case_no_agents(monkeypatch): config = ConfigParser() config.read(config_path) assert config.get('volttron', 'message-bus') == "zmq" - assert config.get('volttron', 'vip-address') == "tcp://127.0.0.15:22916" + assert config.get('volttron', 'vip-address') == vip_address + ":" + vip_port assert config.get('volttron', 'instance-name').strip('"') == "test_zmq" assert not _is_agent_installed("listener") assert not _is_agent_installed("platform_driver") @@ -126,19 +130,21 @@ def test_zmq_case_no_agents(monkeypatch): assert not is_volttron_running(vhome) +@pytest.mark.timeout(400) def test_zmq_case_with_agents(monkeypatch): with create_vcfg_vhome() as vhome: monkeypatch.setenv("VOLTTRON_HOME", vhome) config_path = os.path.join(vhome, "config") message_bus = "zmq" - vip_address = "tcp://127.0.0.15" - vip_port = "22916" + ip = '127.0.0.15' + vip_address = "tcp://" + ip + vip_port = str(get_rand_port(ip)) is_web_enabled = "N" is_vcp = "Y" instance_name = "test_zmq" vc_hostname = "{}{}".format("https://", get_hostname()) - vc_port = "8443" + vc_port = str(get_rand_port(ip)) install_historian = "Y" install_driver = "Y" install_fake_device = "Y" @@ -177,7 +183,7 @@ def test_zmq_case_with_agents(monkeypatch): config = ConfigParser() config.read(config_path) assert config.get('volttron', 'message-bus') == "zmq" - assert config.get('volttron', 'vip-address') == "tcp://127.0.0.15:22916" + assert config.get('volttron', 'vip-address') == vip_address + ":" + vip_port assert config.get('volttron', 'instance-name').strip('"') == "test_zmq" assert _is_agent_installed("listener") assert _is_agent_installed("platform_driver") @@ -194,12 +200,13 @@ def test_zmq_case_web_no_agents(monkeypatch): config_path = os.path.join(vhome, "config") message_bus = "zmq" - vip_address = "tcp://127.0.0.15" - vip_port = "22916" + ip = '127.0.0.15' + vip_address = "tcp://" + ip + vip_port = str(get_rand_port(ip)) instance_name = "test_zmq" is_web_enabled = "Y" web_protocol = "https" - web_port = "8443" + web_port = str(get_rand_port(ip, 8000, 9000)) gen_web_cert = "Y" new_root_ca = "Y" ca_country = "US" @@ -248,11 +255,14 @@ def test_zmq_case_web_no_agents(monkeypatch): config = ConfigParser() config.read(config_path) assert config.get('volttron', 'message-bus') == "zmq" - assert config.get('volttron', 'vip-address') == "tcp://127.0.0.15:22916" + assert config.get('volttron', 'vip-address') == vip_address + ":" + vip_port assert config.get('volttron', 'instance-name').strip('"') == "test_zmq" - assert config.get('volttron', 'bind-web-address') == "{}{}{}".format("https://", get_hostname().lower(), ":8443") - assert config.get('volttron', 'web-ssl-cert') == os.path.join(vhome, "certificates", "certs", "platform_web-server.crt") - assert config.get('volttron', 'web-ssl-key') == os.path.join(vhome, "certificates", "private", "platform_web-server.pem") + assert config.get('volttron', 'bind-web-address') ==\ + "{}{}:{}".format("https://", get_hostname().lower(), web_port) + assert config.get('volttron', 'web-ssl-cert') == \ + os.path.join(vhome, "certificates", "certs", "platform_web-server.crt") + assert config.get('volttron', 'web-ssl-key') == \ + os.path.join(vhome, "certificates", "private", "platform_web-server.pem") assert not _is_agent_installed("listener") assert not _is_agent_installed("platform_driver") assert not _is_agent_installed("platform_historian") @@ -261,18 +271,20 @@ def test_zmq_case_web_no_agents(monkeypatch): assert not is_volttron_running(vhome) +@pytest.mark.timeout(400) def test_zmq_case_web_with_agents(monkeypatch): with create_vcfg_vhome() as vhome: monkeypatch.setenv("VOLTTRON_HOME", vhome) config_path = os.path.join(vhome, "config") message_bus = "zmq" - vip_address = "tcp://127.0.0.15" - vip_port = "22916" + ip = '127.0.0.15' + vip_address = "tcp://" + ip + vip_port = str(get_rand_port(ip)) instance_name = "test_zmq" is_web_enabled = "Y" web_protocol = "https" - web_port = "8443" + web_port = str(get_rand_port(ip, 8000, 9000)) gen_web_cert = "Y" new_root_ca = "Y" ca_country = "US" @@ -283,7 +295,7 @@ def test_zmq_case_web_with_agents(monkeypatch): is_vc = "N" is_vcp = "Y" vc_hostname = "{}{}".format("https://", get_hostname()) - vc_port = "8443" + vc_port = str(get_rand_port(ip)) install_historian = "Y" install_driver = "Y" install_fake_device = "Y" @@ -331,11 +343,14 @@ def test_zmq_case_web_with_agents(monkeypatch): config = ConfigParser() config.read(config_path) assert config.get('volttron', 'message-bus') == "zmq" - assert config.get('volttron', 'vip-address') == "tcp://127.0.0.15:22916" + assert config.get('volttron', 'vip-address') == vip_address + ":" + vip_port assert config.get('volttron', 'instance-name').strip('"') == "test_zmq" - assert config.get('volttron', 'bind-web-address') == "{}{}{}".format("https://", get_hostname().lower(), ":8443") - assert config.get('volttron', 'web-ssl-cert') == os.path.join(vhome, "certificates", "certs", "platform_web-server.crt") - assert config.get('volttron', 'web-ssl-key') == os.path.join(vhome, "certificates", "private", "platform_web-server.pem") + assert config.get('volttron', 'bind-web-address') == \ + "{}{}:{}".format("https://", get_hostname().lower(), web_port) + assert config.get('volttron', 'web-ssl-cert') == \ + os.path.join(vhome, "certificates", "certs", "platform_web-server.crt") + assert config.get('volttron', 'web-ssl-key') == \ + os.path.join(vhome, "certificates", "private", "platform_web-server.pem") assert _is_agent_installed("listener") assert _is_agent_installed("platform_driver") assert _is_agent_installed("platform_historian") @@ -350,12 +365,13 @@ def test_zmq_case_web_vc(monkeypatch): config_path = os.path.join(vhome, "config") message_bus = "zmq" - vip_address = "tcp://127.0.0.15" - vip_port = "22916" + ip = '127.0.0.15' + vip_address = "tcp://" + ip + vip_port = str(get_rand_port(ip)) instance_name = "test_zmq" is_web_enabled = "Y" web_protocol = "https" - web_port = "8443" + web_port = str(get_rand_port(ip, 8000, 9000)) gen_web_cert = "Y" new_root_ca = "Y" ca_country = "US" @@ -406,12 +422,16 @@ def test_zmq_case_web_vc(monkeypatch): config = ConfigParser() config.read(config_path) assert config.get('volttron', 'message-bus') == "zmq" - assert config.get('volttron', 'vip-address') == "tcp://127.0.0.15:22916" + assert config.get('volttron', 'vip-address') == vip_address + ":" + vip_port assert config.get('volttron', 'instance-name').strip('"') == "test_zmq" - assert config.get('volttron', 'volttron-central-address') == "{}{}{}".format("https://", get_hostname().lower(), ":8443") - assert config.get('volttron', 'bind-web-address') == "{}{}{}".format("https://", get_hostname().lower(), ":8443") - assert config.get('volttron', 'web-ssl-cert') == os.path.join(vhome, "certificates", "certs", "platform_web-server.crt") - assert config.get('volttron', 'web-ssl-key') == os.path.join(vhome, "certificates", "private", "platform_web-server.pem") + assert config.get('volttron', 'volttron-central-address') == \ + "{}{}:{}".format("https://", get_hostname().lower(), web_port) + assert config.get('volttron', 'bind-web-address') == \ + "{}{}:{}".format("https://", get_hostname().lower(), web_port) + assert config.get('volttron', 'web-ssl-cert') == \ + os.path.join(vhome, "certificates", "certs", "platform_web-server.crt") + assert config.get('volttron', 'web-ssl-key') == \ + os.path.join(vhome, "certificates", "private", "platform_web-server.pem") assert not _is_agent_installed("listener") assert not _is_agent_installed("platform_driver") assert not _is_agent_installed("platform_historian") @@ -426,12 +446,13 @@ def test_zmq_case_web_vc_with_agents(monkeypatch): config_path = os.path.join(vhome, "config") message_bus = "zmq" - vip_address = "tcp://127.0.0.15" - vip_port = "22916" + ip = '127.0.0.15' + vip_address = "tcp://" + ip + vip_port = str(get_rand_port(ip)) instance_name = "test_zmq" is_web_enabled = "Y" web_protocol = "https" - web_port = "8443" + web_port = str(get_rand_port(ip, 8000, 9000)) gen_web_cert = "Y" new_root_ca = "Y" ca_country = "US" @@ -487,12 +508,16 @@ def test_zmq_case_web_vc_with_agents(monkeypatch): config = ConfigParser() config.read(config_path) assert config.get('volttron', 'message-bus') == "zmq" - assert config.get('volttron', 'vip-address') == "tcp://127.0.0.15:22916" + assert config.get('volttron', 'vip-address') == vip_address + ":" + vip_port assert config.get('volttron', 'instance-name').strip('"') == "test_zmq" - assert config.get('volttron', 'volttron-central-address') == "{}{}{}".format("https://", get_hostname().lower(), ":8443") - assert config.get('volttron', 'bind-web-address') == "{}{}{}".format("https://", get_hostname().lower(), ":8443") - assert config.get('volttron', 'web-ssl-cert') == os.path.join(vhome, "certificates", "certs", "platform_web-server.crt") - assert config.get('volttron', 'web-ssl-key') == os.path.join(vhome, "certificates", "private", "platform_web-server.pem") + assert config.get('volttron', 'volttron-central-address') == \ + "{}{}:{}".format("https://", get_hostname().lower(), web_port) + assert config.get('volttron', 'bind-web-address') == \ + "{}{}:{}".format("https://", get_hostname().lower(), web_port) + assert config.get('volttron', 'web-ssl-cert') == \ + os.path.join(vhome, "certificates", "certs", "platform_web-server.crt") + assert config.get('volttron', 'web-ssl-key') == \ + os.path.join(vhome, "certificates", "private", "platform_web-server.pem") assert _is_agent_installed("listener") assert _is_agent_installed("platform_driver") assert _is_agent_installed("platform_historian") @@ -511,8 +536,9 @@ def test_rmq_case_no_agents(monkeypatch): message_bus = "rmq" instance_name = "test_rmq" - vip_address = "tcp://127.0.0.15" - vip_port = "22916" + ip = '127.0.0.15' + vip_address = "tcp://" + ip + vip_port = str(get_rand_port(ip)) is_web_enabled = "N" is_vcp = "N" install_historian = "N" @@ -546,7 +572,7 @@ def test_rmq_case_no_agents(monkeypatch): config = ConfigParser() config.read(config_path) assert config.get('volttron', 'message-bus') == "rmq" - assert config.get('volttron', 'vip-address') == "tcp://127.0.0.15:22916" + assert config.get('volttron', 'vip-address') == vip_address + ":" + vip_port assert config.get('volttron', 'instance-name').strip('"') == "test_rmq" assert not _is_agent_installed("listener") assert not _is_agent_installed("platform_driver") @@ -566,12 +592,13 @@ def test_rmq_case_with_agents(monkeypatch): message_bus = "rmq" instance_name = "test_rmq" - vip_address = "tcp://127.0.0.15" - vip_port = "22916" + ip = '127.0.0.15' + vip_address = "tcp://" + ip + vip_port = str(get_rand_port(ip)) is_web_enabled = "N" is_vcp = "Y" vc_hostname = "{}{}".format("https://", get_hostname()) - vc_port = "8443" + vc_port = str(get_rand_port(ip)) install_historian = "Y" install_driver = "Y" install_fake_device = "Y" @@ -612,7 +639,7 @@ def test_rmq_case_with_agents(monkeypatch): config = ConfigParser() config.read(config_path) assert config.get('volttron', 'message-bus') == "rmq" - assert config.get('volttron', 'vip-address') == "tcp://127.0.0.15:22916" + assert config.get('volttron', 'vip-address') == vip_address + ":" + vip_port assert config.get('volttron', 'instance-name').strip('"') == "test_rmq" assert _is_agent_installed("listener") assert _is_agent_installed("platform_driver") @@ -634,11 +661,12 @@ def test_rmq_case_web_no_agents(monkeypatch): message_bus = "rmq" instance_name = "test_rmq" is_web_enabled = "Y" - web_port = "8443" is_vc = "N" is_vcp = "N" - vip_address = "tcp://127.0.0.15" - vip_port = "22916" + ip = '127.0.0.15' + web_port = str(get_rand_port(ip, 8000, 9000)) + vip_address = "tcp://" + ip + vip_port = str(get_rand_port(ip)) install_historian = "N" install_driver = "N" install_listener = "N" @@ -672,9 +700,9 @@ def test_rmq_case_web_no_agents(monkeypatch): config = ConfigParser() config.read(config_path) assert config.get('volttron', 'message-bus') == "rmq" - assert config.get('volttron', 'vip-address') == "tcp://127.0.0.15:22916" + assert config.get('volttron', 'vip-address') == vip_address + ":" + vip_port assert config.get('volttron', 'instance-name').strip('"') == "test_rmq" - assert config.get('volttron', 'bind-web-address') == "{}{}{}".format("https://", get_hostname(), ":8443") + assert config.get('volttron', 'bind-web-address') == "{}{}:{}".format("https://", get_hostname(), web_port) assert not _is_agent_installed("listener") assert not _is_agent_installed("platform_driver") assert not _is_agent_installed("platform_historian") @@ -694,13 +722,14 @@ def test_rmq_case_web_with_agents(monkeypatch): message_bus = "rmq" instance_name = "test_rmq" is_web_enabled = "Y" - web_port = "8443" is_vc = "N" is_vcp = "Y" vc_hostname = "{}{}".format("https://", get_hostname()) - vc_port = "8443" - vip_address = "tcp://127.0.0.15" - vip_port = "22916" + ip = '127.0.0.15' + web_port = str(get_rand_port(ip, 8000, 9000)) + vc_port = str(get_rand_port(ip)) + vip_address = "tcp://" + ip + vip_port = str(get_rand_port(ip)) install_historian = "Y" install_driver = "Y" install_fake_device = "Y" @@ -742,9 +771,9 @@ def test_rmq_case_web_with_agents(monkeypatch): config = ConfigParser() config.read(config_path) assert config.get('volttron', 'message-bus') == "rmq" - assert config.get('volttron', 'vip-address') == "tcp://127.0.0.15:22916" + assert config.get('volttron', 'vip-address') == vip_address + ":" + vip_port assert config.get('volttron', 'instance-name').strip('"') == "test_rmq" - assert config.get('volttron', 'bind-web-address') == "{}{}{}".format("https://", get_hostname(), ":8443") + assert config.get('volttron', 'bind-web-address') == "{}{}:{}".format("https://", get_hostname(), web_port) assert _is_agent_installed("listener") assert _is_agent_installed("platform_driver") assert _is_agent_installed("platform_historian") @@ -764,10 +793,11 @@ def test_rmq_case_web_vc(monkeypatch): message_bus = "rmq" instance_name = "test_rmq" - vip_address = "tcp://127.0.0.15" - vip_port = "22916" + ip = '127.0.0.15' + vip_address = "tcp://" + ip + vip_port = str(get_rand_port(ip)) is_web_enabled = "Y" - web_port = "8443" + web_port = str(get_rand_port(ip, 8000, 9000)) is_vc = "Y" is_vcp = "Y" install_historian = "N" @@ -806,10 +836,12 @@ def test_rmq_case_web_vc(monkeypatch): config = ConfigParser() config.read(config_path) assert config.get('volttron', 'message-bus') == "rmq" - assert config.get('volttron', 'vip-address') == "tcp://127.0.0.15:22916" + assert config.get('volttron', 'vip-address') == vip_address assert config.get('volttron', 'instance-name').strip('"') == "test_rmq" - assert config.get('volttron', 'volttron-central-address') == "{}{}{}".format("https://", get_hostname(), ":8443") - assert config.get('volttron', 'bind-web-address') == "{}{}{}".format("https://", get_hostname(), ":8443") + assert config.get('volttron', 'volttron-central-address') == "{}{}:{}".format( + "https://", get_hostname(), web_port) + assert config.get('volttron', 'bind-web-address') == "{}{}:{}".format( + "https://", get_hostname(), web_port) assert not _is_agent_installed("listener") assert not _is_agent_installed("platform_driver") assert not _is_agent_installed("platform_historian") @@ -829,10 +861,11 @@ def test_rmq_case_web_vc_with_agents(monkeypatch): message_bus = "rmq" instance_name = "test_rmq" - vip_address = "tcp://127.0.0.15" - vip_port = "22916" + ip = '127.0.0.15' + vip_address = "tcp://" + ip + vip_port = str(get_rand_port(ip)) is_web_enabled = "Y" - web_port = "8443" + web_port = str(get_rand_port(ip, 8000, 9000)) is_vc = "Y" is_vcp = "Y" install_historian = "Y" @@ -876,10 +909,12 @@ def test_rmq_case_web_vc_with_agents(monkeypatch): config = ConfigParser() config.read(config_path) assert config.get('volttron', 'message-bus') == "rmq" - assert config.get('volttron', 'vip-address') == "tcp://127.0.0.15:22916" + assert config.get('volttron', 'vip-address') == vip_address + ":" + vip_port assert config.get('volttron', 'instance-name').strip('"') == "test_rmq" - assert config.get('volttron', 'volttron-central-address') == "{}{}{}".format("https://", get_hostname(), ":8443") - assert config.get('volttron', 'bind-web-address') == "{}{}{}".format("https://", get_hostname(), ":8443") + assert config.get('volttron', 'volttron-central-address') == "{}{}:{}".format( + "https://", get_hostname(), web_port) + assert config.get('volttron', 'bind-web-address') == "{}{}:{}".format( + "https://", get_hostname(), web_port) assert _is_agent_installed("listener") assert _is_agent_installed("platform_driver") assert _is_agent_installed("platform_historian") @@ -930,12 +965,13 @@ def test_web_with_agents_volttron_running(monkeypatch, volttron_instance_web): assert os.path.exists(config_path) config = ConfigParser() config.read(config_path) - assert config.get('volttron', 'message-bus') == "zmq" - if volttron_instance_web.ssl_auth is True: + assert config.get('volttron', 'message-bus') == volttron_instance_web.messagebus + if volttron_instance_web.ssl_auth is True and volttron_instance_web.messagebus == 'zmq': assert config.get('volttron', 'web-ssl-cert') == os.path.join(vhome, "certificates", "certs", "server0.crt") assert config.get('volttron', 'web-ssl-key') == os.path.join(vhome, "certificates", "private", "server0.pem") - assert _is_agent_installed("listener") - assert _is_agent_installed("platform_driver") - assert _is_agent_installed("platform_historian") - assert _is_agent_installed("vcp") + # if instance is running + assert not _is_agent_installed("listener") + # assert _is_agent_installed("platform_driver") + # assert _is_agent_installed("platform_historian") + # assert _is_agent_installed("vcp") assert is_volttron_running(vhome) diff --git a/volttrontesting/platform/test_platform_web.py b/volttrontesting/platform/test_platform_web.py index 1bf18481bf..9fbcbd9d19 100644 --- a/volttrontesting/platform/test_platform_web.py +++ b/volttrontesting/platform/test_platform_web.py @@ -229,8 +229,8 @@ def test_test_web_agent(volttron_instance_web): @pytest.mark.web -def test_register_path_route(web_instance): - vi = web_instance +def test_register_path_route(volttron_instance_web): + vi = volttron_instance_web with with_os_environ(vi.env): assert vi.is_running() diff --git a/volttrontesting/platform/test_rmq_platform_shutdown.py b/volttrontesting/platform/test_rmq_platform_shutdown.py index ad81d43dcc..0e8befda38 100644 --- a/volttrontesting/platform/test_rmq_platform_shutdown.py +++ b/volttrontesting/platform/test_rmq_platform_shutdown.py @@ -52,6 +52,9 @@ pytestmark = [pytest.mark.xfail] +pytestmark = [pytest.mark.xfail] + + @pytest.mark.rmq_shutdown def test_vctl_shutdown_on_rmq_stop(request): """ diff --git a/volttrontesting/platform/web/test_certs.py b/volttrontesting/platform/web/test_certs.py index 08e79788a5..f56d91ccf1 100644 --- a/volttrontesting/platform/web/test_certs.py +++ b/volttrontesting/platform/web/test_certs.py @@ -60,8 +60,8 @@ # defaults to true ssl: 'true' -# defaults to ~/rabbitmq_server/rabbbitmq_server-3.9.7 -rmq-home: "~/rabbitmq_server/rabbitmq_server-3.9.7" +# defaults to ~/rabbitmq_server/rabbbitmq_server-3.9.29 +rmq-home: "~/rabbitmq_server/rabbitmq_server-3.9.29" """ diff --git a/volttrontesting/platform/web/test_topic_tree.py b/volttrontesting/platform/web/test_topic_tree.py index c411495e85..f9606d25a9 100644 --- a/volttrontesting/platform/web/test_topic_tree.py +++ b/volttrontesting/platform/web/test_topic_tree.py @@ -241,17 +241,17 @@ def test_devices(nid, expected): def _mock_rpc_caller(peer, method, agent, file_name=None, raw=False, external_platform=None): - if method == 'manage_list_configs': + if method == 'list_configs': return ['config', 'devices/Campus/Building1/Fake1', 'devices/Campus/Building2/Fake1', 'devices/Campus/Building3/Fake1', 'registry_configs/fake.csv'] - elif method == 'manage_get' and '.csv' in file_name: + elif method == 'get_config' and '.csv' in file_name: return [{'Point Name': 'SampleBool1', 'Volttron Point Name': 'SampleBool1', 'Units': 'On / Off', 'Units Details': 'on/off', 'Writable': 'FALSE', 'Starting Value': 'TRUE', 'Type': 'boolean', 'Notes': 'Status indidcator of cooling stage 1'}, {'Point Name': 'SampleWritableFloat1', 'Volttron Point Name': 'SampleWritableFloat1', 'Units': 'PPM', 'Units Details': '1000.00 (default)', 'Writable': 'TRUE', 'Starting Value': '10', 'Type': 'float', 'Notes': 'Setpoint to enable demand control ventilation'}] - elif method == 'manage_get' and '.csv' not in file_name: + elif method == 'get_config' and '.csv' not in file_name: return {'driver_config': {}, 'registry_config': 'config://registry_configs/fake.csv', 'interval': 60, 'timezone': 'US/Pacific', 'driver_type': 'fakedriver', 'publish_breadth_first_all': False, 'publish_depth_first': False, 'publish_breadth_first': False, 'campus': 'campus', diff --git a/volttrontesting/platform/web/test_vui_endpoints.py b/volttrontesting/platform/web/test_vui_endpoints.py index fc47becc94..fccc475f41 100644 --- a/volttrontesting/platform/web/test_vui_endpoints.py +++ b/volttrontesting/platform/web/test_vui_endpoints.py @@ -277,16 +277,16 @@ def _mock_agents_rpc(peer, meth, *args, external_platform=None, **kwargs): 'config2': {'setting1': 3, 'setting2': 4}}}, {'identity': 'run2', 'configs': {'config1': {'setting1': 5, 'setting2': 6}, 'config2': {'setting1': 7, 'setting2': 8}}}] - if peer == 'config.store' and meth == 'manage_get': + if peer == 'config.store' and meth == 'get_config': config_list = [a['configs'].get(args[1]) for a in config_definition_list if a['identity'] == args[0]] if not config_list or config_list == [None]: raise RemoteError(f'''builtins.KeyError('No configuration file \"{args[1]}\" for VIP IDENTIY {args[0]}')''', exc_info={"exc_type": '', "exc_args": []}) return config_list[0] if config_list else [] - elif peer == 'config.store' and meth == 'manage_list_configs': + elif peer == 'config.store' and meth == 'list_configs': config_list = [a['configs'].keys() for a in config_definition_list if a['identity'] == args[0]] return config_list[0] if config_list else [] - elif peer == 'config.store' and meth == 'manage_list_stores': + elif peer == 'config.store' and meth == 'list_stores': return [a['identity'] for a in config_definition_list] elif peer == 'control' and meth == 'list_agents': return list_of_agents @@ -458,7 +458,7 @@ def test_handle_platforms_agents_configs_config_put_response(mock_platform_web_s config_type = re.search(r'([^\/]+$)', config_type).group() if config_type in ['application/json', 'text/csv'] else 'raw' if status == '204': - vui_endpoints._rpc.assert_has_calls([mock.call('config.store', 'manage_store', vip_identity, config_name, + vui_endpoints._rpc.assert_has_calls([mock.call('config.store', 'set_config', vip_identity, config_name, data_passed, config_type, external_platform='my_instance_name')]) elif status == '400': assert json.loads(response.response[0]) == \ @@ -488,7 +488,7 @@ def test_handle_platforms_agents_configs_post_response(mock_platform_web_service response = vui_endpoints.handle_platforms_agents_configs(env, data_given) check_response_codes(response, status) if status == '204': - vui_endpoints._rpc.assert_has_calls([mock.call('config.store', 'manage_store', vip_identity, config_name, + vui_endpoints._rpc.assert_has_calls([mock.call('config.store', 'set_config', vip_identity, config_name, data_passed, config_type, external_platform='my_instance_name')]) elif status == '400': assert json.loads(response.response[0]) == \ @@ -510,7 +510,7 @@ def test_handle_platforms_agents_configs_delete_response(mock_platform_web_servi response = vui_endpoints.handle_platforms_agents_configs(env, {}) check_response_codes(response, status) if status == '204': - vui_endpoints._rpc.assert_has_calls([mock.call('config.store', 'manage_delete_store', vip_identity_passed, + vui_endpoints._rpc.assert_has_calls([mock.call('config.store', 'delete_store', vip_identity_passed, external_platform='my_instance_name')]) @@ -526,7 +526,7 @@ def test_handle_platforms_agents_configs_config_delete_response(mock_platform_we response = vui_endpoints.handle_platforms_agents_configs(env, {}) check_response_codes(response, status) if status == '204': - vui_endpoints._rpc.assert_has_calls([mock.call('config.store', 'manage_delete_config', vip_identity, + vui_endpoints._rpc.assert_has_calls([mock.call('config.store', 'delete_config', vip_identity, config_name_passed, external_platform='my_instance_name')]) diff --git a/volttrontesting/services/aggregate_historian/copy_test_data.py b/volttrontesting/services/aggregate_historian/copy_test_data.py index ac019e4542..2e43ca65a4 100644 --- a/volttrontesting/services/aggregate_historian/copy_test_data.py +++ b/volttrontesting/services/aggregate_historian/copy_test_data.py @@ -36,13 +36,13 @@ def connect_mongodb(connection_params): - print ("setup mongodb") + print("setup mongodb") mongo_conn_str = 'mongodb://{user}:{passwd}@{host}:{port}/{database}' if connection_params.get('authSource'): mongo_conn_str = mongo_conn_str+ '?authSource={authSource}' params = connection_params mongo_conn_str = mongo_conn_str.format(**params) - print (mongo_conn_str) + print(mongo_conn_str) mongo_client = pymongo.MongoClient(mongo_conn_str) db = mongo_client[connection_params['database']] return db @@ -118,7 +118,7 @@ def copy(source_params, dest_params, start_date, end_date): {'$and': [{'_id': {'$gte': ObjectId.from_datetime(start_date)}}, {'_id': {'$lte': ObjectId.from_datetime(end_date)}}]}) - print ("Record count from cursor {}".format(cursor.count())) + print("Record count from cursor {}".format(cursor.count())) for record in cursor: i += 1 records.append( diff --git a/volttrontesting/services/aggregate_historian/test_aggregate_historian.py b/volttrontesting/services/aggregate_historian/test_aggregate_historian.py index 7d7107b63b..642b77ad5a 100644 --- a/volttrontesting/services/aggregate_historian/test_aggregate_historian.py +++ b/volttrontesting/services/aggregate_historian/test_aggregate_historian.py @@ -192,7 +192,7 @@ def setup_mysql(connection_params, table_names): - print ("setup mysql") + print("setup mysql") db_connection = mysql.connect(**connection_params) # clean up any rows from older runs cleanup_mysql(db_connection, None, drop_tables=True) @@ -200,11 +200,11 @@ def setup_mysql(connection_params, table_names): def setup_sqlite(connection_params, table_names): - print ("setup sqlite") + print("setup sqlite") database_path = connection_params['database'] - print ("connecting to sqlite path " + database_path) + print("connecting to sqlite path " + database_path) db_connection = sqlite3.connect(database_path) - print ("successfully connected to sqlite") + print("successfully connected to sqlite") cleanup_sqlite(db_connection, None, drop_tables=True) db_connection.commit() return db_connection @@ -328,9 +328,9 @@ def get_table_names(config): def publish_test_data(publish_agent, start_time, start_reading, count): reading = start_reading time = start_time - print ("publishing test data starttime is {} utcnow is {}".format( + print("publishing test data starttime is {} utcnow is {}".format( start_time, datetime.utcnow())) - print ("publishing test data value string {} at {}".format(reading, + print("publishing test data value string {} at {}".format(reading, datetime.now())) float_meta = {'units': 'F', 'tz': 'UTC', 'type': 'float'} @@ -426,7 +426,7 @@ def aggregate_agent(request, volttron_instance): # Set this hear so that we can create these table after connecting to db table_names = get_table_names(request.param) - print ("request.param -- {}".format(request.param)) + print("request.param -- {}".format(request.param)) # 2: Open db connection that can be used for row deletes after # each test method. Clean up old tables if any @@ -490,7 +490,7 @@ def test_get_supported_aggregations(aggregate_agent, query_agent): :param query_agent: fake agent used to query historian :return: """ - query_agent.vip.rpc.call(CONFIGURATION_STORE, "manage_store", + query_agent.vip.rpc.call(CONFIGURATION_STORE, "set_config", AGG_AGENT_VIP, "config", aggregate_agent).get() gevent.sleep(1) @@ -502,7 +502,7 @@ def test_get_supported_aggregations(aggregate_agent, query_agent): 'get_supported_aggregations').get(timeout=10) assert result - print (result) + print(result) conn = aggregate_agent.get("connection") if conn: if conn.get("type") == "mysql": @@ -565,7 +565,7 @@ def test_single_topic_pattern(aggregate_agent, query_agent): ] } ] - query_agent.vip.rpc.call(CONFIGURATION_STORE, "manage_store", + query_agent.vip.rpc.call(CONFIGURATION_STORE, "set_config", AGG_AGENT_VIP, "config", new_config).get() gevent.sleep(1) @@ -668,7 +668,7 @@ def test_single_topic(aggregate_agent, query_agent): ] } ] - query_agent.vip.rpc.call(CONFIGURATION_STORE, "manage_store", + query_agent.vip.rpc.call(CONFIGURATION_STORE, "set_config", AGG_AGENT_VIP, "config", new_config).get() gevent.sleep(3 * 60) # sleep till we see two rows in aggregate table @@ -841,7 +841,7 @@ def test_multiple_topic_pattern(aggregate_agent, query_agent): } ] - query_agent.vip.rpc.call(CONFIGURATION_STORE, "manage_store", + query_agent.vip.rpc.call(CONFIGURATION_STORE, "set_config", AGG_AGENT_VIP, "config", new_config).get() gevent.sleep(1) @@ -914,7 +914,7 @@ def test_multiple_topic_list(aggregate_agent, query_agent): } ] - query_agent.vip.rpc.call(CONFIGURATION_STORE, "manage_store", + query_agent.vip.rpc.call(CONFIGURATION_STORE, "set_config", AGG_AGENT_VIP, "config", new_config).get() gevent.sleep(1) @@ -991,7 +991,7 @@ def test_topic_reconfiguration(aggregate_agent, query_agent): } ] - query_agent.vip.rpc.call(CONFIGURATION_STORE, "manage_store", + query_agent.vip.rpc.call(CONFIGURATION_STORE, "set_config", AGG_AGENT_VIP, "config", new_config).get() gevent.sleep(2) @@ -1037,11 +1037,11 @@ def test_topic_reconfiguration(aggregate_agent, query_agent): print("Before reinstall current time is {}".format(datetime.utcnow())) - query_agent.vip.rpc.call(CONFIGURATION_STORE, "manage_store", + query_agent.vip.rpc.call(CONFIGURATION_STORE, "set_config", AGG_AGENT_VIP, "config", new_config).get() - print ("After configure\n\n") + print("After configure\n\n") gevent.sleep(3) result1 = query_agent.vip.rpc.call( diff --git a/volttrontesting/services/aggregate_historian/test_base_aggregate_historian.py b/volttrontesting/services/aggregate_historian/test_base_aggregate_historian.py index c3931725cf..ede35b0ca5 100644 --- a/volttrontesting/services/aggregate_historian/test_base_aggregate_historian.py +++ b/volttrontesting/services/aggregate_historian/test_base_aggregate_historian.py @@ -124,7 +124,7 @@ def test_time_slice_calculation_realtime(): # month aggregation period start, end = AggregateHistorian.compute_aggregation_time_slice( utc_collection_start_time, '2M', False) - print (start, end) + print(start, end) assert end == utc_collection_start_time assert start == utc_collection_start_time - timedelta(days=60) try: diff --git a/volttrontesting/services/historian/test_base_historian.py b/volttrontesting/services/historian/test_base_historian.py index 4771e257a5..fbc93b98a7 100644 --- a/volttrontesting/services/historian/test_base_historian.py +++ b/volttrontesting/services/historian/test_base_historian.py @@ -54,7 +54,7 @@ STATUS_KEY_TIME_ERROR) from volttron.platform.agent import utils from volttron.platform.messaging import headers as headers_mod -from volttron.platform.messaging.health import * +from volttron.platform.messaging.health import STATUS_BAD, STATUS_GOOD, Status from volttron.platform.messaging import topics from volttron.platform.agent.known_identities import CONFIGURATION_STORE @@ -388,7 +388,7 @@ def test_time_tolerance_check(request, volttron_instance, client_agent): # Change config to modify topic for time tolerance check historian.publish_sleep = 0 json_config = """{"time_tolerance_topics":["record"]}""" - historian.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + historian.vip.rpc.call(CONFIGURATION_STORE, 'set_config', identity, "config", json_config, config_type="json").get() gevent.sleep(2) diff --git a/volttrontesting/services/historian/test_multiplatform.py b/volttrontesting/services/historian/test_multiplatform.py index 94c4b76385..5af92514f8 100644 --- a/volttrontesting/services/historian/test_multiplatform.py +++ b/volttrontesting/services/historian/test_multiplatform.py @@ -49,16 +49,19 @@ import gevent import pytest -from volttron.platform import get_services_core, jsonapi +from volttron.platform import get_services_core, jsonapi, is_rabbitmq_available from volttron.platform.agent import utils from volttron.platform.messaging import headers as headers_mod from volttrontesting.fixtures.volttron_platform_fixtures import build_wrapper +from volttrontesting.skip_if_handlers import rmq_skipif from volttrontesting.utils.utils import get_rand_vip, get_hostname_and_random_port from volttrontesting.utils.platformwrapper import PlatformWrapper from volttrontesting.fixtures.volttron_platform_fixtures import get_rand_vip, \ get_rand_ip_and_port -from volttron.utils.rmq_setup import start_rabbit, stop_rabbit from volttron.platform.agent.utils import execute_command +HAS_RMQ = is_rabbitmq_available() +if HAS_RMQ: + from volttron.utils.rmq_setup import start_rabbit, stop_rabbit @pytest.fixture(scope="module") @@ -239,6 +242,7 @@ def test_all_platform_subscription_zmq(request, get_zmq_volttron_instances): @pytest.mark.historian @pytest.mark.multiplatform +@pytest.mark.skipif(rmq_skipif, reason="RMQ not installed.") def test_all_platform_subscription_rmq(request, federated_rmq_instances): try: upstream, downstream = federated_rmq_instances diff --git a/volttrontesting/services/market_service/test_market_service.py b/volttrontesting/services/market_service/test_market_service.py index 41758a06ac..894f6b4169 100644 --- a/volttrontesting/services/market_service/test_market_service.py +++ b/volttrontesting/services/market_service/test_market_service.py @@ -103,10 +103,10 @@ def create_supply_curve(self): supply_curve = PolyLine() price = 100 quantity = 0 - supply_curve.add(Point(price,quantity)) + supply_curve.add(Point(price, quantity)) price = 100 quantity = 1000 - supply_curve.add(Point(price,quantity)) + supply_curve.add(Point(price, quantity)) return supply_curve def create_demand_curve(self): diff --git a/volttrontesting/services/tagging/test_tagging.py b/volttrontesting/services/tagging/test_tagging.py index 8164372354..d0524b4480 100644 --- a/volttrontesting/services/tagging/test_tagging.py +++ b/volttrontesting/services/tagging/test_tagging.py @@ -161,7 +161,7 @@ def cleanup_mysql(db_connection, truncate_tables): def cleanup_mongodb(db_connection, truncate_tables): for collection in truncate_tables: - db_connection[collection].remove() + db_connection[collection].drop() print("Finished removing {}".format(truncate_tables)) @@ -245,7 +245,7 @@ def test_init_failure(volttron_instance, tagging_service, query_agent): callback=query_agent.callback).get() new_config = copy.copy(tagging_service) new_config['connection'] = {"params": - {"host": "localhost", + {"host": "localhost2", "port": 27017, "database": "mongo_test", "user": "invalid_user", @@ -259,8 +259,8 @@ def test_init_failure(volttron_instance, tagging_service, query_agent): volttron_instance.start_agent(agent_id) except: pass - gevent.sleep(1) - print ("Call back count {}".format(query_agent.callback.call_count)) + gevent.sleep(3) + print("Call back count {}".format(query_agent.callback.call_count)) assert query_agent.callback.call_count == 1 print("Call args {}".format(query_agent.callback.call_args)) assert query_agent.callback.call_args[0][1] == 'test.tagging.init' diff --git a/volttrontesting/services/weather/test_base_weather.py b/volttrontesting/services/weather/test_base_weather.py index 2a11f1ca39..b52b05e3bc 100644 --- a/volttrontesting/services/weather/test_base_weather.py +++ b/volttrontesting/services/weather/test_base_weather.py @@ -40,7 +40,7 @@ import datetime import os import sqlite3 - +import logging import gevent import pytest from mock import MagicMock @@ -48,7 +48,9 @@ from volttron.platform.agent import utils from volttron.platform.agent.base_weather import BaseWeatherAgent from volttron.platform.agent.utils import get_fq_identity -from volttron.platform.messaging.health import * +from volttron.platform.messaging.health import STATUS_BAD, STATUS_GOOD +from volttron.platform import jsonapi +from volttron.platform.agent.utils import format_timestamp utils.setup_logging() _log = logging.getLogger(__name__) @@ -524,6 +526,7 @@ def test_manage_unit_conversion_fail(weather, from_units, start, to_units, [{"location": "fake_location1"}, {"location": "fake_location2"}] ]) def test_get_current_valid_locations(weather, fake_locations): + clear_api_calls(weather) conn = weather._cache._sqlite_conn cursor = conn.cursor() weather.set_update_interval("get_current_weather", diff --git a/volttrontesting/subsystems/test_config_store.py b/volttrontesting/subsystems/test_config_store.py index 003cc17707..b9315f0b54 100644 --- a/volttrontesting/subsystems/test_config_store.py +++ b/volttrontesting/subsystems/test_config_store.py @@ -66,6 +66,14 @@ def _module_config_test_agent(request, volttron_instance): agent = volttron_instance.build_agent(identity='config_test_agent', agent_class=_config_test_agent, enable_store=True) + # wait for config store's onconnect method to complete. onconnect calls handle_callback we don't want this + # to interfere with tests that test the trigger_callback mechanism + # Quote from config store documentation: + # + # As the configuration subsystem calls all callbacks in the onconfig phase and none are called beforehand + # the trigger_callback setting is effectively ignored if an agent sets a configuration or default configuration + # before the end of the onstart phase. + gevent.sleep(3) def cleanup(): agent.core.stop() @@ -78,7 +86,7 @@ def cleanup(): def config_test_agent(request, _module_config_test_agent, volttron_instance): def cleanup(): - _module_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_delete_store', 'config_test_agent').get() + _module_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'delete_store', 'config_test_agent').get() request.addfinalizer(cleanup) return _module_config_test_agent @@ -100,6 +108,18 @@ def cleanup(): return config_test_agent +@pytest.mark.config_store +def test_set_config_json(default_config_test_agent): + json_config = """{"value":1}""" + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', + "config_test_agent", "config", json_config, config_type="json").get() + + results = default_config_test_agent.callback_results + assert len(results) == 1 + first = results[0] + assert first == ("config", "NEW", {"value": 1}) + + @pytest.mark.config_store def test_manage_store_json(default_config_test_agent): json_config = """{"value":1}""" @@ -113,9 +133,9 @@ def test_manage_store_json(default_config_test_agent): @pytest.mark.config_store -def test_manage_store_csv(default_config_test_agent): +def test_set_config_csv(default_config_test_agent): csv_config = "value\n1" - default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config", csv_config, config_type="csv").get() results = default_config_test_agent.callback_results @@ -125,9 +145,9 @@ def test_manage_store_csv(default_config_test_agent): @pytest.mark.config_store -def test_manage_store_raw(default_config_test_agent): +def test_set_config_raw(default_config_test_agent): raw_config = "test_config_stuff" - default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config", raw_config, config_type="raw").get() results = default_config_test_agent.callback_results @@ -137,9 +157,9 @@ def test_manage_store_raw(default_config_test_agent): @pytest.mark.config_store -def test_manage_update_config(default_config_test_agent): +def test_update_config(default_config_test_agent): json_config = """{"value":1}""" - default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config", json_config, config_type="json").get() results = default_config_test_agent.callback_results @@ -148,7 +168,7 @@ def test_manage_update_config(default_config_test_agent): assert first == ("config", "NEW", {"value": 1}) json_config = """{"value":2}""" - default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config", json_config, config_type="json").get() assert len(results) == 2 @@ -156,10 +176,27 @@ def test_manage_update_config(default_config_test_agent): assert second == ("config", "UPDATE", {"value": 2}) +@pytest.mark.config_store +def test_delete_config(default_config_test_agent): + json_config = """{"value":1}""" + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', + "config_test_agent", "config", json_config, config_type="json").get() + + results = default_config_test_agent.callback_results + assert len(results) == 1 + first = results[0] + assert first == ("config", "NEW", {"value": 1}) + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'delete_config', + "config_test_agent", "config").get() + assert len(results) == 2 + second = results[1] + assert second == ("config", "DELETE", None) + + @pytest.mark.config_store def test_manage_delete_config(default_config_test_agent): json_config = """{"value":1}""" - default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config", json_config, config_type="json").get() results = default_config_test_agent.callback_results @@ -173,13 +210,31 @@ def test_manage_delete_config(default_config_test_agent): assert second == ("config", "DELETE", None) +@pytest.mark.config_store +def test_delete_store(default_config_test_agent): + json_config = """{"value":1}""" + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', + "config_test_agent", "config", json_config, config_type="json").get() + + results = default_config_test_agent.callback_results + print(f"callback results is {results}") + assert len(results) == 1 + first = results[0] + assert first == ("config", "NEW", {"value": 1}) + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'delete_store', "config_test_agent").get() + assert len(results) == 2 + second = results[1] + assert second == ("config", "DELETE", None) + + @pytest.mark.config_store def test_manage_delete_store(default_config_test_agent): json_config = """{"value":1}""" - default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config", json_config, config_type="json").get() results = default_config_test_agent.callback_results + print(f"callback results is {results}") assert len(results) == 1 first = results[0] assert first == ("config", "NEW", {"value": 1}) @@ -189,10 +244,53 @@ def test_manage_delete_store(default_config_test_agent): assert second == ("config", "DELETE", None) +@pytest.mark.config_store +def test_get_config(config_test_agent): + json_config = """{"value":1}""" + config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', + "config_test_agent", "config", json_config, config_type="json").get() + + config = config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'get_config', + "config_test_agent", "config", raw=False).get() + + assert config == {"value": 1} + + @pytest.mark.config_store def test_manage_get_config(config_test_agent): json_config = """{"value":1}""" - config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', + "config_test_agent", "config", json_config, config_type="json").get() + + config = config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_get', + "config_test_agent", "config", raw=False).get() + + assert config == {"value": 1} + + +@pytest.mark.config_store +def test_get_metadata(config_test_agent): + json_config = """{"value":1}""" + config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', + "config_test_agent", "config", json_config, config_type="json").get() + + config = config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'get_config', + "config_test_agent", "config", raw=False).get() + + assert config == {"value": 1} + + metadata = config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'get_metadata', + "config_test_agent", "config").get() + print(f"Metadata {metadata}") + assert metadata["type"] == "json" + assert metadata["modified"] + assert metadata["data"] == '{"value":1}' + + +@pytest.mark.config_store +def test_manage_get_metadata(config_test_agent): + json_config = """{"value":1}""" + config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config", json_config, config_type="json").get() config = config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_get', @@ -200,11 +298,30 @@ def test_manage_get_config(config_test_agent): assert config == {"value": 1} + metadata = config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_get_metadata', + "config_test_agent", "config").get() + print(f"Metadata {metadata}") + assert metadata["type"] == "json" + assert metadata["modified"] + assert metadata["data"] == '{"value":1}' + + +@pytest.mark.config_store +def test_get_raw_config(config_test_agent): + json_config = """{"value":1}""" + config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', + "config_test_agent", "config", json_config, config_type="json").get() + + config = config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'get_config', + "config_test_agent", "config", raw=True).get() + + assert config == json_config + @pytest.mark.config_store def test_manage_get_raw_config(config_test_agent): json_config = """{"value":1}""" - config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config", json_config, config_type="json").get() config = config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_get', @@ -213,16 +330,34 @@ def test_manage_get_raw_config(config_test_agent): assert config == json_config +@pytest.mark.config_store +def test_list_config(config_test_agent): + json_config = """{"value":1}""" + config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', + "config_test_agent", "config1", json_config, config_type="json").get() + json_config = """{"value":2}""" + config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', + "config_test_agent", "config2", json_config, config_type="json").get() + json_config = """{"value":3}""" + config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', + "config_test_agent", "config3", json_config, config_type="json").get() + + config_list = config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'list_configs', + "config_test_agent").get() + + assert config_list == ['config1', 'config2', 'config3'] + + @pytest.mark.config_store def test_manage_list_config(config_test_agent): json_config = """{"value":1}""" - config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config1", json_config, config_type="json").get() json_config = """{"value":2}""" - config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config2", json_config, config_type="json").get() json_config = """{"value":3}""" - config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config3", json_config, config_type="json").get() config_list = config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_list_configs', @@ -231,10 +366,21 @@ def test_manage_list_config(config_test_agent): assert config_list == ['config1', 'config2', 'config3'] +@pytest.mark.config_store +def test_list_store(config_test_agent): + json_config = """{"value":1}""" + config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', + "config_test_agent", "config1", json_config, config_type="json").get() + + config_list = config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'list_stores').get() + + assert "config_test_agent" in config_list + + @pytest.mark.config_store def test_manage_list_store(config_test_agent): json_config = """{"value":1}""" - config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config1", json_config, config_type="json").get() config_list = config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_list_stores').get() @@ -245,13 +391,13 @@ def test_manage_list_store(config_test_agent): @pytest.mark.config_store def test_agent_list_config(default_config_test_agent): json_config = """{"value":1}""" - default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config1", json_config, config_type="json").get() json_config = """{"value":2}""" - default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config2", json_config, config_type="json").get() json_config = """{"value":3}""" - default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config3", json_config, config_type="json").get() config_list = default_config_test_agent.vip.config.list() @@ -262,7 +408,7 @@ def test_agent_list_config(default_config_test_agent): @pytest.mark.config_store def test_agent_get_config(default_config_test_agent): json_config = """{"value":1}""" - default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config", json_config, config_type="json").get() config = default_config_test_agent.vip.config.get("config") @@ -273,7 +419,7 @@ def test_agent_get_config(default_config_test_agent): @pytest.mark.config_store def test_agent_reference_config_and_callback_order(default_config_test_agent): json_config = """{"config2":"config://config2", "config3":"config://config3"}""" - default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config", json_config, config_type="json").get() config = default_config_test_agent.vip.config.get("config") @@ -281,7 +427,7 @@ def test_agent_reference_config_and_callback_order(default_config_test_agent): assert config == {"config2":None, "config3":None} json_config = """{"value":2}""" - default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config2", json_config, config_type="json").get() # Also use th to verify that the callback for "config" is called first. @@ -289,7 +435,7 @@ def test_agent_reference_config_and_callback_order(default_config_test_agent): default_config_test_agent.reset_results() json_config = """{"value":3}""" - default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + default_config_test_agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config3", json_config, config_type="json").get() config = default_config_test_agent.vip.config.get("config") @@ -304,12 +450,13 @@ def test_agent_reference_config_and_callback_order(default_config_test_agent): second = results[1] assert second == ("config3", "NEW", {"value": 3}) + @pytest.mark.config_store -def test_agent_set_config(default_config_test_agent): - json_config = {"value":1} +def test_agent_set_config(default_config_test_agent, volttron_instance): + json_config = {"value": 1} default_config_test_agent.vip.config.set("config", json_config) - + gevent.sleep(5) # wait to avoid case where we are simply missing the callback results = default_config_test_agent.callback_results assert len(results) == 0 @@ -318,7 +465,7 @@ def test_agent_set_config(default_config_test_agent): assert config == {"value": 1} default_config_test_agent.vip.config.set("config", json_config, trigger_callback=True) - + gevent.sleep(5) results = default_config_test_agent.callback_results assert len(results) == 1 first = results[0] @@ -360,7 +507,7 @@ def test_agent_default_config(request, volttron_instance): def cleanup(): if agent: - agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_delete_store', 'test_default_agent').get() + agent.vip.rpc.call(CONFIGURATION_STORE, 'delete_store', 'test_default_agent').get() agent.core.stop() request.addfinalizer(cleanup) @@ -383,14 +530,14 @@ def __init__(self, **kwargs): result = results[0] assert result == ("config", "NEW", {"value": 2}) - agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "test_default_agent", "config", '{"value": 1}', config_type="json").get() assert len(results) == 2 result = results[-1] assert result == ("config", "UPDATE", {"value": 1}) - agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_delete_config', "test_default_agent", "config").get() + agent.vip.rpc.call(CONFIGURATION_STORE, 'delete_config', "test_default_agent", "config").get() assert len(results) == 3 result = results[-1] @@ -401,7 +548,7 @@ def __init__(self, **kwargs): def test_agent_sub_options(request, volttron_instance): def cleanup(): - agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_delete_store', 'test_agent_sub_options').get() + agent.vip.rpc.call(CONFIGURATION_STORE, 'delete_store', 'test_agent_sub_options').get() agent.core.stop() request.addfinalizer(cleanup) @@ -424,13 +571,13 @@ def __init__(self, **kwargs): update_json = """{"value": 2}""" for name in ("new/config", "update/config", "delete/config"): - agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "test_agent_sub_options", name, new_json, config_type="json").get() - agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', + agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "test_agent_sub_options", name, update_json, config_type="json").get() - agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_delete_config', + agent.vip.rpc.call(CONFIGURATION_STORE, 'delete_config', "test_agent_sub_options", name).get() results = agent.callback_results @@ -456,9 +603,9 @@ def test_config_store_security(volttron_instance, default_config_test_agent): # By default agents should have access to edit their own config store json_config = """{"value":1}""" - agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', "rpc_agent", "config", json_config, + agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "rpc_agent", "config", json_config, config_type="json").get() - config = agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_get', "rpc_agent", "config", raw=False).get() + config = agent.vip.rpc.call(CONFIGURATION_STORE, 'get_config', "rpc_agent", "config", raw=False).get() assert config == {"value": 1} @@ -466,20 +613,20 @@ def test_config_store_security(volttron_instance, default_config_test_agent): # default_config_test_agent unless explicitly granted permissions try: json_config = """{"value":1}""" - agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_store', "config_test_agent", "config", + agent.vip.rpc.call(CONFIGURATION_STORE, 'set_config', "config_test_agent", "config", json_config, config_type="json").get() except jsonrpc.RemoteError as e: - assert e.message == "User rpc_agent can call method manage_store only with " \ + assert e.message == "User rpc_agent can call method set_config only with " \ "identity=rpc_agent but called with identity=config_test_agent" try: - agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_delete_store', 'config_test_agent').get() + agent.vip.rpc.call(CONFIGURATION_STORE, 'delete_store', 'config_test_agent').get() except jsonrpc.RemoteError as e: - assert e.message == "User rpc_agent can call method manage_delete_store only with " \ + assert e.message == "User rpc_agent can call method delete_store only with " \ "identity=rpc_agent but called with identity=config_test_agent" # Should be able to view - result = agent.vip.rpc.call(CONFIGURATION_STORE, 'manage_list_configs', "config_test_agent").get() + result = agent.vip.rpc.call(CONFIGURATION_STORE, 'list_configs', "config_test_agent").get() print(result) finally: diff --git a/volttrontesting/subsystems/test_health_subsystem.py b/volttrontesting/subsystems/test_health_subsystem.py index 9bb415458b..d0c2a4290b 100644 --- a/volttrontesting/subsystems/test_health_subsystem.py +++ b/volttrontesting/subsystems/test_health_subsystem.py @@ -5,7 +5,7 @@ from volttron.platform.messaging import topics from volttron.platform.messaging.headers import DATE -from volttron.platform.messaging.health import * +from volttron.platform.messaging.health import STATUS_BAD, STATUS_GOOD, Status from volttron.platform.agent.utils import parse_timestamp_string from volttrontesting.utils.utils import (poll_gevent_sleep, messages_contains_prefix) diff --git a/volttrontesting/testutils/test_getinstance_1.py b/volttrontesting/testutils/test_getinstance_1.py index a445d0fc2a..eaaca45102 100644 --- a/volttrontesting/testutils/test_getinstance_1.py +++ b/volttrontesting/testutils/test_getinstance_1.py @@ -5,11 +5,9 @@ @pytest.mark.wrapper def test_fixture_returns_correct_number_of_instances(get_volttron_instances): - num_instances = 4 + num_instances = 2 wrappers = get_volttron_instances(num_instances, should_start=False) - assert num_instances == len(wrappers) for w in wrappers: assert isinstance(w, PlatformWrapper) - assert not w.is_running() diff --git a/volttrontesting/utils/platformwrapper.py b/volttrontesting/utils/platformwrapper.py index 17e6817149..4c9688f763 100644 --- a/volttrontesting/utils/platformwrapper.py +++ b/volttrontesting/utils/platformwrapper.py @@ -170,7 +170,8 @@ def start_wrapper_platform(wrapper, with_http=False, with_tcp=True, wrapper.startup_platform(vip_address=vc_tcp, bind_web_address=bind_address, volttron_central_address=volttron_central_address, - volttron_central_serverkey=volttron_central_serverkey) + volttron_central_serverkey=volttron_central_serverkey, + timeout=100) if with_http: discovery = "{}/discovery/".format(vc_http) response = grequests.get(discovery).send().response @@ -305,9 +306,6 @@ def __init__(self, messagebus=None, ssl_auth=False, instance_name=None, # with older 2.0 agents. self.opts = None - keystorefile = os.path.join(self.volttron_home, 'keystore') - self.keystore = KeyStore(keystorefile) - self.keystore.generate() self.messagebus = messagebus if messagebus else 'zmq' # Regardless of what is passed in if using rmq we need auth and ssl. if self.messagebus == 'rmq': @@ -330,9 +328,12 @@ def __init__(self, messagebus=None, ssl_auth=False, instance_name=None, # Writes the main volttron config file for this instance. store_message_bus_config(self.messagebus, self.instance_name) - - # Update volttron config file with non-auth setting if auth_enabled is False - if not self.auth_enabled: + if self.auth_enabled: + keystorefile = os.path.join(self.volttron_home, 'keystore') + self.keystore = KeyStore(keystorefile) + self.keystore.generate() + else: + # Update volttron config file with non-auth setting if auth_enabled is False config_path = os.path.join(self.volttron_home, "config") if os.path.exists(config_path): config = configparser.ConfigParser() @@ -358,7 +359,6 @@ def __init__(self, messagebus=None, ssl_auth=False, instance_name=None, if not self.debug_mode: self.debug_mode = self.env.get('DEBUG', False) self.skip_cleanup = self.env.get('SKIP_CLEANUP', False) - self._web_admin_api = None @property @@ -419,10 +419,11 @@ def build_connection(self, peer=None, address=None, identity=None, self.logit( 'Default address was None so setting to current instances') address = self.vip_address + if self.auth_enabled: - if address is None: - serverkey = self.serverkey if serverkey is None: + serverkey = self.serverkey + if serverkey is None and address is not None: self.logit("serverkey wasn't set but the address was.") raise Exception("Invalid state.") if publickey is None or secretkey is None: @@ -479,31 +480,32 @@ def build_agent(self, address=None, should_spawn=True, identity=None, print(f"Publickey is: {publickey}\nServerkey is: {serverkey}") if serverkey is None: serverkey = self.serverkey - if publickey is None: - self.logit(f'generating new public secret key pair {KeyStore.get_agent_keystore_path(identity=identity)}') - ks = KeyStore(KeyStore.get_agent_keystore_path(identity=identity)) - publickey = ks.public - secretkey = ks.secret if address is None: self.logit('Using vip-address {address}'.format( address=self.vip_address)) address = self.vip_address + if self.auth_enabled: + if publickey is None: + self.logit( + f'generating new public secret key pair {KeyStore.get_agent_keystore_path(identity=identity)}') + ks = KeyStore(KeyStore.get_agent_keystore_path(identity=identity)) + publickey = ks.public + secretkey = ks.secret + if publickey and not serverkey: + self.logit('using instance serverkey: {}'.format(publickey)) + serverkey = publickey + self.logit("BUILD agent VOLTTRON HOME: {}".format(self.volttron_home)) + if self.bind_web_address: + kwargs['enable_web'] = True - if publickey and not serverkey: - self.logit('using instance serverkey: {}'.format(publickey)) - serverkey = publickey - self.logit("BUILD agent VOLTTRON HOME: {}".format(self.volttron_home)) - if self.bind_web_address: - kwargs['enable_web'] = True + if 'enable_store' not in kwargs: + kwargs['enable_store'] = False - if 'enable_store' not in kwargs: - kwargs['enable_store'] = False + if capabilities is None: + capabilities = dict(edit_config_store=dict(identity=identity)) + print(f"Publickey is: {publickey}\nServerkey is: {serverkey}") - if capabilities is None: - capabilities = dict(edit_config_store=dict(identity=identity)) - print(f"Publickey is: {publickey}\nServerkey is: {serverkey}") - if self.auth_enabled: entry = AuthEntry(user_id=identity, identity=identity, credentials=publickey, capabilities=capabilities, comments="Added by platform wrapper") @@ -529,6 +531,9 @@ def build_agent(self, address=None, should_spawn=True, identity=None, event.wait(timeout=4) has_control = False times = 0 + if self.messagebus == 'rmq': + # agent seem to need a extra second for agent to establish connection + gevent.sleep(1) while not has_control and times < 10: times += 1 try: @@ -599,6 +604,9 @@ def disable_auto_csr(self): assert not self.is_auto_csr_enabled() def add_capabilities(self, publickey, capabilities): + if not self.auth_enabled: + self.logit("Auth is not enabled, so ignore and return False") + return False with with_os_environ(self.env): if isinstance(capabilities, str) or isinstance(capabilities, dict): capabilities = [capabilities] @@ -619,17 +627,21 @@ def add_capabilities(self, publickey, capabilities): # when invoked in quick succession. add_capabilities updates auth.json, gets the peerlist and calls all peers' # auth.update rpc call. So sleeping here instead expecting individual test cases to sleep for long gevent.sleep(2) + return True - @staticmethod - def add_capability(entry, capabilites): + def add_capability(self, entry, capabilities): + if not self.auth_enabled: + self.logit("Auth is not enabled, so ignore and return False") + return False if isinstance(entry, str): - if entry not in capabilites: - capabilites[entry] = None + if entry not in capabilities: + capabilities[entry] = None elif isinstance(entry, dict): - capabilites.update(entry) + capabilities.update(entry) else: raise ValueError("Invalid capability {}. Capability should be string or dictionary or list of string" "and dictionary.") + return True def set_auth_dict(self, auth_dict): if auth_dict: @@ -845,12 +857,13 @@ def startup_platform(self, vip_address, auth_dict=None, pconfig = os.path.join(self.volttron_home, 'config') config = {} - # Add platform's public key to known hosts file - publickey = self.keystore.public - known_hosts_file = os.path.join(self.volttron_home, 'known_hosts') - known_hosts = KnownHostsStore(known_hosts_file) - known_hosts.add(self.opts['vip_local_address'], publickey) - known_hosts.add(self.opts['vip_address'], publickey) + if self.auth_enabled: + # Add platform's public key to known hosts file + publickey = self.keystore.public + known_hosts_file = os.path.join(self.volttron_home, 'known_hosts') + known_hosts = KnownHostsStore(known_hosts_file) + known_hosts.add(self.opts['vip_local_address'], publickey) + known_hosts.add(self.opts['vip_address'], publickey) # Set up the configuration file based upon the passed parameters. parser = configparser.ConfigParser() @@ -918,8 +931,9 @@ def startup_platform(self, vip_address, auth_dict=None, utils.wait_for_volttron_startup(self.volttron_home, timeout) - self.serverkey = self.keystore.public - assert self.serverkey + if self.auth_enabled: + self.serverkey = self.keystore.public + assert self.serverkey # Use dynamic_agent so we can look and see the agent with peerlist. if not setupmode: @@ -1223,7 +1237,12 @@ def __wait_for_control_connection_to_exit__(self, timeout: int = 10): disconnected = False timer_start = time.time() while not disconnected: - peers = self.dynamic_agent.vip.peerlist().get(timeout=20) + try: + peers = self.dynamic_agent.vip.peerlist().get(timeout=10) + except gevent.Timeout: + self.logit("peerlist call timed out. Exiting loop. " + "Not waiting for control connection to exit.") + break disconnected = CONTROL_CONNECTION not in peers if disconnected: break @@ -1602,10 +1621,9 @@ def mergetree(src, dst, symlinks=False, ignore=None): d = os.path.join(dst, item) if os.path.isdir(s): mergetree(s, d, symlinks, ignore) - else: - if not os.path.exists(d) or os.stat(src).st_mtime - os.stat( + elif not os.path.exists(d) or os.stat(src).st_mtime - os.stat( dst).st_mtime > 1: - shutil.copy2(s, d) + shutil.copy2(s, d) class WebAdminApi: diff --git a/volttrontesting/utils/utils.py b/volttrontesting/utils/utils.py index cfd2a81df2..3ff7076d23 100644 --- a/volttrontesting/utils/utils.py +++ b/volttrontesting/utils/utils.py @@ -134,7 +134,7 @@ def build_devices_header_and_message(points=['abc', 'def']): for point in points: data[point] = random() * 10 - meta_data[point] = meta_templates[randint(0,len(meta_templates)-1)] + meta_data[point] = meta_templates[randint(0, len(meta_templates)-1)] time1 = utils.format_timestamp( datetime.utcnow()) headers = {