s2-python 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {s2_python-0.5.0.dist-info → s2_python-0.6.0.dist-info}/METADATA +15 -11
- {s2_python-0.5.0.dist-info → s2_python-0.6.0.dist-info}/RECORD +28 -21
- {s2_python-0.5.0.dist-info → s2_python-0.6.0.dist-info}/WHEEL +1 -1
- s2_python-0.6.0.dist-info/licenses/LICENSE +201 -0
- s2python/common/number_range.py +4 -1
- s2python/common/power_forecast_element.py +23 -1
- s2python/common/power_measurement.py +24 -2
- s2python/common/power_range.py +3 -1
- s2python/frbc/frbc_actuator_description.py +6 -2
- s2python/frbc/frbc_fill_level_target_profile_element.py +4 -1
- s2python/frbc/frbc_leakage_behaviour_element.py +7 -2
- s2python/frbc/frbc_operation_mode.py +7 -2
- s2python/generated/gen_s2.py +1 -0
- s2python/message.py +12 -1
- s2python/ombc/__init__.py +5 -0
- s2python/ombc/ombc_instruction.py +19 -0
- s2python/ombc/ombc_operation_mode.py +25 -0
- s2python/ombc/ombc_status.py +17 -0
- s2python/ombc/ombc_system_description.py +25 -0
- s2python/ombc/ombc_timer_status.py +17 -0
- s2python/pebc/pebc_allowed_limit_range.py +16 -0
- s2python/pebc/pebc_power_constraints.py +51 -1
- s2python/s2_connection.py +63 -20
- s2python/s2_control_type.py +21 -0
- s2python/s2_parser.py +7 -1
- s2python/s2_validation_error.py +3 -1
- {s2_python-0.5.0.dist-info → s2_python-0.6.0.dist-info}/entry_points.txt +0 -0
- {s2_python-0.5.0.dist-info → s2_python-0.6.0.dist-info}/top_level.txt +0 -0
@@ -1,23 +1,25 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: s2-python
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.6.0
|
4
4
|
Summary: S2 Protocol Python Wrapper
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
License: APACHE
|
5
|
+
Author-email: Flexiblepower <info@info.nl>
|
6
|
+
License-Expression: Apache-2.0
|
7
|
+
Project-URL: Source code, https://github.com/flexiblepower/s2-ws-json-python
|
9
8
|
Platform: Linux
|
10
9
|
Classifier: Development Status :: 4 - Beta
|
11
|
-
Classifier: Programming Language :: Python :: 3.8
|
12
10
|
Classifier: Programming Language :: Python :: 3.9
|
13
11
|
Classifier: Programming Language :: Python :: 3.10
|
14
12
|
Classifier: Programming Language :: Python :: 3.11
|
15
13
|
Classifier: Programming Language :: Python :: 3.12
|
16
|
-
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
15
|
+
Requires-Python: <3.14,>=3.9
|
16
|
+
Description-Content-Type: text/x-rst
|
17
|
+
License-File: LICENSE
|
17
18
|
Requires-Dist: pydantic>=2.8.2
|
18
19
|
Requires-Dist: pytz
|
19
20
|
Requires-Dist: click
|
20
|
-
|
21
|
+
Provides-Extra: ws
|
22
|
+
Requires-Dist: websockets~=13.1; extra == "ws"
|
21
23
|
Provides-Extra: testing
|
22
24
|
Requires-Dist: pytest; extra == "testing"
|
23
25
|
Requires-Dist: pytest-coverage; extra == "testing"
|
@@ -38,6 +40,7 @@ Requires-Dist: sphinx-tabs; extra == "docs"
|
|
38
40
|
Requires-Dist: sphinx_copybutton; extra == "docs"
|
39
41
|
Requires-Dist: sphinx_fontawesome; extra == "docs"
|
40
42
|
Requires-Dist: sphinxcontrib.httpdomain; extra == "docs"
|
43
|
+
Dynamic: license-file
|
41
44
|
|
42
45
|
Python Wrapper for S2 Flexibility Protocol
|
43
46
|
===========================================
|
@@ -55,13 +58,14 @@ Currently, the package supports the *common* and *FILL RATE BASED CONTROL* types
|
|
55
58
|
|
56
59
|
To Install
|
57
60
|
-----------
|
58
|
-
You can install this package using pip or any Python dependency manager that collects the packages from
|
61
|
+
You can install this package using pip or any Python dependency manager that collects the packages from PyPI:
|
59
62
|
|
60
63
|
.. code-block:: bash
|
61
64
|
|
62
65
|
pip install s2-python
|
66
|
+
pip install s2-python[ws] # for S2 over WebSockets
|
63
67
|
|
64
|
-
The packages on
|
68
|
+
The packages on PyPI may be found `here <https://pypi.org/project/s2-python/>`_
|
65
69
|
|
66
70
|
Mypy support
|
67
71
|
------------
|
@@ -99,7 +103,7 @@ Development
|
|
99
103
|
|
100
104
|
For development, you can install the required dependencies using the following command:
|
101
105
|
|
102
|
-
pip install -e .[testing,development]
|
106
|
+
pip install -e .[testing,development,ws]
|
103
107
|
|
104
108
|
|
105
109
|
The tests can be run using tox:
|
@@ -1,11 +1,12 @@
|
|
1
|
+
s2_python-0.6.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
1
2
|
s2python/__init__.py,sha256=e5lwvqsPl-z7IfEd0hRQhLBRKBYcuw2eqrecXnMfLdg,384
|
2
|
-
s2python/message.py,sha256=
|
3
|
+
s2python/message.py,sha256=62DtKyfovcDM4Eroc33i72ZnQScWDV7sUvYifagXcmI,3352
|
3
4
|
s2python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
5
|
s2python/reception_status_awaiter.py,sha256=jKMliFk1XxwsEGtx3vFESbJhUtClB7cTu-td90-qBN8,2137
|
5
|
-
s2python/s2_connection.py,sha256=
|
6
|
-
s2python/s2_control_type.py,sha256
|
7
|
-
s2python/s2_parser.py,sha256=
|
8
|
-
s2python/s2_validation_error.py,sha256=
|
6
|
+
s2python/s2_connection.py,sha256=QGpYtIxoRXIKeiw-1C7MywxxLwT1OwwqMxUVZ-NJ79Y,21685
|
7
|
+
s2python/s2_control_type.py,sha256=uLZb_7l2mmqY6tJk3KuQYGpKRnZaDyLZ-E8YvAGFCwQ,4192
|
8
|
+
s2python/s2_parser.py,sha256=uYR-8yTzqyf7xerX7ULk6prm7_Cc5h_2EC9CGCKHIJM,4583
|
9
|
+
s2python/s2_validation_error.py,sha256=paWJAkmjOwigeFmF8Zwo-5ithRJ2SRqOLX-rHRZ6ces,398
|
9
10
|
s2python/utils.py,sha256=QX9b-mi-H_YUGTmGmJsrAbaWWM3dgaoaRLRXHHlaZDE,212
|
10
11
|
s2python/validate_values_mixin.py,sha256=AsycGNUjH9oMAYdOXgJR1QelyNEiM31Cp0HpBzenMxU,2539
|
11
12
|
s2python/version.py,sha256=IBzoytgbYYYekQnSTfSmWeYAZ4c_yUFU2oLIAG4UYjs,45
|
@@ -14,12 +15,12 @@ s2python/common/duration.py,sha256=7u2SF7rWtwLfehD9RUiY1Mhntnah1ctvYjin04ZiYnI,7
|
|
14
15
|
s2python/common/handshake.py,sha256=1S7WaonNztCkwiWNuOtQyDufimGXuD-2vmnqrJ9UOzU,489
|
15
16
|
s2python/common/handshake_response.py,sha256=aJyZSl62wUUdtrJ4WFzu4ZIlhl-4wjxdyQ7rnaa3oNI,537
|
16
17
|
s2python/common/instruction_status_update.py,sha256=z1nt5ToXinuX44N-t-w5oCYXQtG27q9QZEsg1xsISwY,735
|
17
|
-
s2python/common/number_range.py,sha256=
|
18
|
+
s2python/common/number_range.py,sha256=twkIKKtSHtxb4eegwPaDSIGRDWzHAH3gm5t3SPbIyfo,739
|
18
19
|
s2python/common/power_forecast.py,sha256=XXtRJWeHDa-mkr4FnHrN55Fp_byBmTLsaAQKqO7Zcpo,757
|
19
|
-
s2python/common/power_forecast_element.py,sha256=
|
20
|
+
s2python/common/power_forecast_element.py,sha256=Uy0dOe6Lo_WEswS8pHHZXSeC5UnrrwpYb8R7NENNUKQ,1606
|
20
21
|
s2python/common/power_forecast_value.py,sha256=-tIGJn1ME4ozVmg1jdoEWUYT4Eb5uxvp443TmJpmBB0,389
|
21
|
-
s2python/common/power_measurement.py,sha256=
|
22
|
-
s2python/common/power_range.py,sha256=
|
22
|
+
s2python/common/power_measurement.py,sha256=4c6OEIG9-75M9aiqmvUUWMwGkAkQEVGRcVvi_Iz6mlk,1486
|
23
|
+
s2python/common/power_range.py,sha256=zb8Juw9WtV-UvkICRIO6LGvCt19eqErryKoQe2S3Kog,702
|
23
24
|
s2python/common/power_value.py,sha256=mjHEPWFzMAZPJ0P5M9zASHf5uIW47kM8SqqdVaZCkAs,349
|
24
25
|
s2python/common/reception_status.py,sha256=Lf6c81jq5VWUTFgGmmTUly3PK82MmRrt4n6T3zdG-aA,541
|
25
26
|
s2python/common/resource_manager_details.py,sha256=-t4sOlVrlAzFaokb3d_94QUxQr1FeQlRJghVjMJxodM,1158
|
@@ -40,14 +41,14 @@ s2python/ddbc/ddbc_operation_mode.py,sha256=JuIcWod2vdUqCdECS7WGDyA_b-q6AgNg4BU4
|
|
40
41
|
s2python/ddbc/ddbc_system_description.py,sha256=FjFzAyIK119BbTIk0xaPY2beNm2lDFq3M-ELnz7TuFg,1361
|
41
42
|
s2python/ddbc/ddbc_timer_status.py,sha256=Hvbgv4LA7pq3J_KR9E9QrMaiN9VN4M6SFJRj8VgmADc,816
|
42
43
|
s2python/frbc/__init__.py,sha256=X2DytTb_e8R8xZaA4fc1-EkKJmleF37U5OUmNUCsfPU,1549
|
43
|
-
s2python/frbc/frbc_actuator_description.py,sha256=
|
44
|
+
s2python/frbc/frbc_actuator_description.py,sha256=KBpe_Df9f71FHqx4orTY0MmUfGNCv3AnCk4gUSa73-0,6445
|
44
45
|
s2python/frbc/frbc_actuator_status.py,sha256=m7FdDC6weKiDDJPGbMqqA6zmPcInxB4Sj5_Ot1cQ6L4,1125
|
45
46
|
s2python/frbc/frbc_fill_level_target_profile.py,sha256=bO9-J3EfKN5jb3nNlnimL01J1FHN-CgM9JTg5mX3VyI,937
|
46
|
-
s2python/frbc/frbc_fill_level_target_profile_element.py,sha256=
|
47
|
+
s2python/frbc/frbc_fill_level_target_profile_element.py,sha256=OeWr755qB8U5r-uleluezd69cbcwViyY9DDmRQvRaY8,1337
|
47
48
|
s2python/frbc/frbc_instruction.py,sha256=1tC1pLXz3nw3J8ozx4fppqcOvsS4lb7uGyx2ACi9fHM,930
|
48
49
|
s2python/frbc/frbc_leakage_behaviour.py,sha256=a8SI14m1KctQYhufXd7MxehiPajtvBCIk9Dls56x4vQ,857
|
49
|
-
s2python/frbc/frbc_leakage_behaviour_element.py,sha256=
|
50
|
-
s2python/frbc/frbc_operation_mode.py,sha256=
|
50
|
+
s2python/frbc/frbc_leakage_behaviour_element.py,sha256=xYNd8EBlEoC-1yiemzRpcz0OjVRLY8KZ6DxeJ-nHrVI,1140
|
51
|
+
s2python/frbc/frbc_operation_mode.py,sha256=4FUYS5B4-UJ8rtZqogzoZDNCUxtXumK6HUomCZUGAiA,2040
|
51
52
|
s2python/frbc/frbc_operation_mode_element.py,sha256=Yz3xfNvEsyKSBh8NxiyQy6NGNi7zc9dXO-Lz-Q4f8Hs,1236
|
52
53
|
s2python/frbc/frbc_storage_description.py,sha256=t0MgCuSbphSf13cfJJPir19lbs_f0qOxGSxtTpo28Sc,648
|
53
54
|
s2python/frbc/frbc_storage_status.py,sha256=1jWMu1D8Y6s9hCmICwycGhHNKVPHBNoU7soIVPxV-iU,537
|
@@ -57,12 +58,18 @@ s2python/frbc/frbc_usage_forecast.py,sha256=tQFZMuYls8wvpvGRq1gzguSmLNoq6SzPAYem
|
|
57
58
|
s2python/frbc/frbc_usage_forecast_element.py,sha256=l55xN8ywrX45i5IOK5gbVR7lGm6lbRPjC7aPHZcc-Sc,608
|
58
59
|
s2python/frbc/rm.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
59
60
|
s2python/generated/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
60
|
-
s2python/generated/gen_s2.py,sha256
|
61
|
+
s2python/generated/gen_s2.py,sha256=l1jzvXGSRa09nu6GBN8Mg4z_2HfwtY3aQeBHbmrBJFM,63431
|
62
|
+
s2python/ombc/__init__.py,sha256=gwSNmlgU35lm4W1y-pvaP5h7urpdWhy0rUuUL16qaO4,309
|
63
|
+
s2python/ombc/ombc_instruction.py,sha256=_my3X76uUPwDXMjqfkSLPzHQhw1eeFV54nBrFkzDqCE,944
|
64
|
+
s2python/ombc/ombc_operation_mode.py,sha256=-jpw1ccIrV1-DkgH4pbvihheRBTDZt79l5Lsn89HAsc,856
|
65
|
+
s2python/ombc/ombc_status.py,sha256=4SDZUYAGGYiTEo68swDo_9IrmJPfMby3aPQ9XnLzmUo,593
|
66
|
+
s2python/ombc/ombc_system_description.py,sha256=mPUfow7sEw8uzvMRZaBNnmmHOzjFsv1CtIFCNrXAhXs,1095
|
67
|
+
s2python/ombc/ombc_timer_status.py,sha256=qaLyY9Jl16hqoIEDR5tTuoSttMHHsRdh-AxvTgODxx0,606
|
61
68
|
s2python/pebc/__init__.py,sha256=O8CONhsj4ZEN9bPbPPZwxvr3DxMN8HnGAwdlXVT2kA8,781
|
62
|
-
s2python/pebc/pebc_allowed_limit_range.py,sha256=
|
69
|
+
s2python/pebc/pebc_allowed_limit_range.py,sha256=_-6cuAd2wtbe7p3WPr_8WS9BD-_mb8Nmr2oKpNdS0jo,1996
|
63
70
|
s2python/pebc/pebc_energy_constraint.py,sha256=ZjrAQVWPJ5jfxENQ5NdR9ULHwQF6dlgOhkCUCHIwk0I,1231
|
64
71
|
s2python/pebc/pebc_instruction.py,sha256=6aVH4qu7kDE0i9DGv6voAhglUbJTlZWN6X15-GRdXJY,1275
|
65
|
-
s2python/pebc/pebc_power_constraints.py,sha256=
|
72
|
+
s2python/pebc/pebc_power_constraints.py,sha256=wzL9crLiguUCIHKs2X8F4YiBQZBGlwU8KjBVPHyW3Gk,2997
|
66
73
|
s2python/pebc/pebc_power_envelope.py,sha256=VjqbAcAPK0Y6MMWHqWqGedNSPm293E-RGtQDtQesZC8,965
|
67
74
|
s2python/pebc/pebc_power_envelope_element.py,sha256=Jh6Xd_d9ZUDXsO3ZswPA792q0ETFxO7qQuMd8UNUXNQ,717
|
68
75
|
s2python/ppbc/__init__.py,sha256=nlSgMwMC9r6Ca-91CTw49eQABtdZHXrgYoHT-2I9YDc,1061
|
@@ -75,8 +82,8 @@ s2python/ppbc/ppbc_power_sequence_container_status.py,sha256=KhbyqgzX2yncYT6TkHG
|
|
75
82
|
s2python/ppbc/ppbc_power_sequence_element.py,sha256=AbCk4lqpBkP8ppE4gMfOGLpcC53pBIcm81fUDZp5xAM,850
|
76
83
|
s2python/ppbc/ppbc_schedule_instruction.py,sha256=C-MUpHhUMPKebmHlT8ClpmKvtLzPgs4MAq10mVcmi6Y,1291
|
77
84
|
s2python/ppbc/ppbc_start_interruption_instruction.py,sha256=cZndUsFhBtKAmTM89qYTF74Y_SUIy1jukn7-Zz-YLo8,1420
|
78
|
-
s2_python-0.
|
79
|
-
s2_python-0.
|
80
|
-
s2_python-0.
|
81
|
-
s2_python-0.
|
82
|
-
s2_python-0.
|
85
|
+
s2_python-0.6.0.dist-info/METADATA,sha256=Yn_Nam6WQGtdbU6a92XiXK7hXjzOwV8xlX4aJcbZUJ8,3814
|
86
|
+
s2_python-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
87
|
+
s2_python-0.6.0.dist-info/entry_points.txt,sha256=feX-xmgJZgSe5-jxMgFKPKCJz4Ys3eQcGrsXsirNZyM,61
|
88
|
+
s2_python-0.6.0.dist-info/top_level.txt,sha256=OLFq0oDhr77Mp-EYLEcWk5P3jvooOt4IHkTI5KYJMc8,9
|
89
|
+
s2_python-0.6.0.dist-info/RECORD,,
|
@@ -0,0 +1,201 @@
|
|
1
|
+
Apache License
|
2
|
+
Version 2.0, January 2004
|
3
|
+
http://www.apache.org/licenses/
|
4
|
+
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
6
|
+
|
7
|
+
1. Definitions.
|
8
|
+
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
11
|
+
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
13
|
+
the copyright owner that is granting the License.
|
14
|
+
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
16
|
+
other entities that control, are controlled by, or are under common
|
17
|
+
control with that entity. For the purposes of this definition,
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
19
|
+
direction or management of such entity, whether by contract or
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
22
|
+
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
24
|
+
exercising permissions granted by this License.
|
25
|
+
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
27
|
+
including but not limited to software source code, documentation
|
28
|
+
source, and configuration files.
|
29
|
+
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
31
|
+
transformation or translation of a Source form, including but
|
32
|
+
not limited to compiled object code, generated documentation,
|
33
|
+
and conversions to other media types.
|
34
|
+
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
36
|
+
Object form, made available under the License, as indicated by a
|
37
|
+
copyright notice that is included in or attached to the work
|
38
|
+
(an example is provided in the Appendix below).
|
39
|
+
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
46
|
+
the Work and Derivative Works thereof.
|
47
|
+
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
49
|
+
the original version of the Work and any modifications or additions
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
61
|
+
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
64
|
+
subsequently incorporated within the Work.
|
65
|
+
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
72
|
+
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
78
|
+
where such license applies only to those patent claims licensable
|
79
|
+
by such Contributor that are necessarily infringed by their
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
82
|
+
institute patent litigation against any entity (including a
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
85
|
+
or contributory patent infringement, then any patent licenses
|
86
|
+
granted to You under this License for that Work shall terminate
|
87
|
+
as of the date such litigation is filed.
|
88
|
+
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
91
|
+
modifications, and in Source or Object form, provided that You
|
92
|
+
meet the following conditions:
|
93
|
+
|
94
|
+
(a) You must give any other recipients of the Work or
|
95
|
+
Derivative Works a copy of this License; and
|
96
|
+
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
98
|
+
stating that You changed the files; and
|
99
|
+
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
102
|
+
attribution notices from the Source form of the Work,
|
103
|
+
excluding those notices that do not pertain to any part of
|
104
|
+
the Derivative Works; and
|
105
|
+
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
108
|
+
include a readable copy of the attribution notices contained
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
111
|
+
of the following places: within a NOTICE text file distributed
|
112
|
+
as part of the Derivative Works; within the Source form or
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
114
|
+
within a display generated by the Derivative Works, if and
|
115
|
+
wherever such third-party notices normally appear. The contents
|
116
|
+
of the NOTICE file are for informational purposes only and
|
117
|
+
do not modify the License. You may add Your own attribution
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
120
|
+
that such additional attribution notices cannot be construed
|
121
|
+
as modifying the License.
|
122
|
+
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
124
|
+
may provide additional or different license terms and conditions
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
128
|
+
the conditions stated in this License.
|
129
|
+
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
133
|
+
this License, without any additional terms or conditions.
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
135
|
+
the terms of any separate license agreement you may have executed
|
136
|
+
with Licensor regarding such Contributions.
|
137
|
+
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
140
|
+
except as required for reasonable and customary use in describing the
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
142
|
+
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
152
|
+
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
158
|
+
incidental, or consequential damages of any character arising as a
|
159
|
+
result of this License or out of the use or inability to use the
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
162
|
+
other commercial damages or losses), even if such Contributor
|
163
|
+
has been advised of the possibility of such damages.
|
164
|
+
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
168
|
+
or other liability obligations and/or rights consistent with this
|
169
|
+
License. However, in accepting such obligations, You may act only
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
174
|
+
of your accepting any such warranty or additional liability.
|
175
|
+
|
176
|
+
END OF TERMS AND CONDITIONS
|
177
|
+
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
179
|
+
|
180
|
+
To apply the Apache License to your work, attach the following
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
182
|
+
replaced with your own identifying information. (Don't include
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
184
|
+
comment syntax for the file format. We also recommend that a
|
185
|
+
file or class name and description of purpose be included on the
|
186
|
+
same "printed page" as the copyright notice for easier
|
187
|
+
identification within third-party archives.
|
188
|
+
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
190
|
+
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
192
|
+
you may not use this file except in compliance with the License.
|
193
|
+
You may obtain a copy of the License at
|
194
|
+
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
196
|
+
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
200
|
+
See the License for the specific language governing permissions and
|
201
|
+
limitations under the License.
|
s2python/common/number_range.py
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
from typing import Any
|
2
2
|
|
3
|
-
from s2python.validate_values_mixin import
|
3
|
+
from s2python.validate_values_mixin import (
|
4
|
+
S2MessageComponent,
|
5
|
+
catch_and_convert_exceptions,
|
6
|
+
)
|
4
7
|
from s2python.generated.gen_s2 import NumberRange as GenNumberRange
|
5
8
|
|
6
9
|
|
@@ -1,6 +1,12 @@
|
|
1
1
|
from typing import List
|
2
|
+
from typing_extensions import Self
|
2
3
|
|
3
|
-
from
|
4
|
+
from pydantic import model_validator
|
5
|
+
|
6
|
+
from s2python.generated.gen_s2 import (
|
7
|
+
CommodityQuantity,
|
8
|
+
PowerForecastElement as GenPowerForecastElement,
|
9
|
+
)
|
4
10
|
from s2python.validate_values_mixin import (
|
5
11
|
catch_and_convert_exceptions,
|
6
12
|
S2MessageComponent,
|
@@ -18,3 +24,19 @@ class PowerForecastElement(GenPowerForecastElement, S2MessageComponent):
|
|
18
24
|
power_values: List[PowerForecastValue] = ( # type: ignore[reportIncompatibleVariableOverride]
|
19
25
|
GenPowerForecastElement.model_fields["power_values"] # type: ignore[assignment]
|
20
26
|
)
|
27
|
+
|
28
|
+
@model_validator(mode="after")
|
29
|
+
def validate_values_at_most_one_per_commodity_quantity(self) -> Self:
|
30
|
+
"""Validates the power measurement values to check that there is at most 1 PowerValue per CommodityQuantity."""
|
31
|
+
|
32
|
+
has_value: dict[CommodityQuantity, bool] = {}
|
33
|
+
|
34
|
+
for value in self.power_values:
|
35
|
+
if has_value.get(value.commodity_quantity, False):
|
36
|
+
raise ValueError(
|
37
|
+
self,
|
38
|
+
"There must be at most 1 PowerForecastValue per CommodityQuantity",
|
39
|
+
)
|
40
|
+
has_value[value.commodity_quantity] = True
|
41
|
+
|
42
|
+
return self
|
@@ -1,8 +1,13 @@
|
|
1
|
-
from typing import List
|
2
1
|
import uuid
|
2
|
+
from typing import List
|
3
|
+
from typing_extensions import Self
|
3
4
|
|
5
|
+
from pydantic import model_validator
|
4
6
|
from s2python.common.power_value import PowerValue
|
5
|
-
from s2python.generated.gen_s2 import
|
7
|
+
from s2python.generated.gen_s2 import (
|
8
|
+
PowerMeasurement as GenPowerMeasurement,
|
9
|
+
CommodityQuantity,
|
10
|
+
)
|
6
11
|
from s2python.validate_values_mixin import (
|
7
12
|
catch_and_convert_exceptions,
|
8
13
|
S2MessageComponent,
|
@@ -16,3 +21,20 @@ class PowerMeasurement(GenPowerMeasurement, S2MessageComponent):
|
|
16
21
|
|
17
22
|
message_id: uuid.UUID = GenPowerMeasurement.model_fields["message_id"] # type: ignore[assignment,reportIncompatibleVariableOverride]
|
18
23
|
values: List[PowerValue] = GenPowerMeasurement.model_fields["values"] # type: ignore[assignment,reportIncompatibleVariableOverride]
|
24
|
+
|
25
|
+
@model_validator(mode="after")
|
26
|
+
def validate_values_at_most_one_per_commodity_quantity(self) -> Self:
|
27
|
+
"""Validates the power measurement values to check that there is at most 1 PowerValue per CommodityQuantity."""
|
28
|
+
|
29
|
+
has_value: dict[CommodityQuantity, bool] = {}
|
30
|
+
|
31
|
+
for value in self.values:
|
32
|
+
if has_value.get(value.commodity_quantity, False):
|
33
|
+
raise ValueError(
|
34
|
+
self,
|
35
|
+
"The measured PowerValues must contain at most one item per CommodityQuantity.",
|
36
|
+
)
|
37
|
+
|
38
|
+
has_value[value.commodity_quantity] = True
|
39
|
+
|
40
|
+
return self
|
s2python/common/power_range.py
CHANGED
@@ -17,6 +17,8 @@ class PowerRange(GenPowerRange, S2MessageComponent):
|
|
17
17
|
@model_validator(mode="after")
|
18
18
|
def validate_start_end_order(self) -> Self:
|
19
19
|
if self.start_of_range > self.end_of_range:
|
20
|
-
raise ValueError(
|
20
|
+
raise ValueError(
|
21
|
+
self, "start_of_range should not be higher than end_of_range"
|
22
|
+
)
|
21
23
|
|
22
24
|
return self
|
@@ -61,7 +61,9 @@ class FRBCActuatorDescription(GenFRBCActuatorDescription, S2MessageComponent):
|
|
61
61
|
timer: Timer
|
62
62
|
for timer in self.timers:
|
63
63
|
if timer.id in ids:
|
64
|
-
raise ValueError(
|
64
|
+
raise ValueError(
|
65
|
+
self, f"Id {timer.id} was found multiple times in 'timers'."
|
66
|
+
)
|
65
67
|
ids.append(timer.id)
|
66
68
|
|
67
69
|
return self
|
@@ -113,7 +115,9 @@ class FRBCActuatorDescription(GenFRBCActuatorDescription, S2MessageComponent):
|
|
113
115
|
power_ranges_for_commodity = [
|
114
116
|
power_range
|
115
117
|
for power_range in operation_mode_element.power_ranges
|
116
|
-
if commodity_has_quantity(
|
118
|
+
if commodity_has_quantity(
|
119
|
+
commodity, power_range.commodity_quantity
|
120
|
+
)
|
117
121
|
]
|
118
122
|
|
119
123
|
if len(power_ranges_for_commodity) > 1:
|
@@ -8,7 +8,10 @@ from s2python.common import Duration, NumberRange
|
|
8
8
|
from s2python.generated.gen_s2 import (
|
9
9
|
FRBCFillLevelTargetProfileElement as GenFRBCFillLevelTargetProfileElement,
|
10
10
|
)
|
11
|
-
from s2python.validate_values_mixin import
|
11
|
+
from s2python.validate_values_mixin import (
|
12
|
+
catch_and_convert_exceptions,
|
13
|
+
S2MessageComponent,
|
14
|
+
)
|
12
15
|
|
13
16
|
|
14
17
|
@catch_and_convert_exceptions
|
@@ -4,8 +4,13 @@ from pydantic import model_validator
|
|
4
4
|
from typing_extensions import Self
|
5
5
|
|
6
6
|
from s2python.common import NumberRange
|
7
|
-
from s2python.generated.gen_s2 import
|
8
|
-
|
7
|
+
from s2python.generated.gen_s2 import (
|
8
|
+
FRBCLeakageBehaviourElement as GenFRBCLeakageBehaviourElement,
|
9
|
+
)
|
10
|
+
from s2python.validate_values_mixin import (
|
11
|
+
catch_and_convert_exceptions,
|
12
|
+
S2MessageComponent,
|
13
|
+
)
|
9
14
|
|
10
15
|
|
11
16
|
@catch_and_convert_exceptions
|
@@ -34,8 +34,13 @@ class FRBCOperationMode(GenFRBCOperationMode, S2MessageComponent):
|
|
34
34
|
sorted_fill_level_ranges = list(elements_by_fill_level_range.keys())
|
35
35
|
sorted_fill_level_ranges.sort(key=lambda r: r.start_of_range)
|
36
36
|
|
37
|
-
for current_fill_level_range, next_fill_level_range in pairwise(
|
38
|
-
|
37
|
+
for current_fill_level_range, next_fill_level_range in pairwise(
|
38
|
+
sorted_fill_level_ranges
|
39
|
+
):
|
40
|
+
if (
|
41
|
+
current_fill_level_range.end_of_range
|
42
|
+
!= next_fill_level_range.start_of_range
|
43
|
+
):
|
39
44
|
raise ValueError(
|
40
45
|
self,
|
41
46
|
f"Elements with fill level ranges {current_fill_level_range} and "
|
s2python/generated/gen_s2.py
CHANGED
s2python/message.py
CHANGED
@@ -38,6 +38,13 @@ from s2python.ddbc import (
|
|
38
38
|
DDBCSystemDescription,
|
39
39
|
DDBCTimerStatus,
|
40
40
|
)
|
41
|
+
from s2python.ombc import (
|
42
|
+
OMBCInstruction,
|
43
|
+
OMBCOperationMode,
|
44
|
+
OMBCTimerStatus,
|
45
|
+
OMBCStatus,
|
46
|
+
OMBCSystemDescription,
|
47
|
+
)
|
41
48
|
|
42
49
|
from s2python.pebc import (
|
43
50
|
PEBCAllowedLimitRange,
|
@@ -82,6 +89,10 @@ S2Message = Union[
|
|
82
89
|
FRBCSystemDescription,
|
83
90
|
FRBCTimerStatus,
|
84
91
|
FRBCUsageForecast,
|
92
|
+
OMBCSystemDescription,
|
93
|
+
OMBCStatus,
|
94
|
+
OMBCTimerStatus,
|
95
|
+
OMBCInstruction,
|
85
96
|
PEBCPowerConstraints,
|
86
97
|
PPBCEndInterruptionInstruction,
|
87
98
|
PPBCPowerProfileDefinition,
|
@@ -93,7 +104,6 @@ S2Message = Union[
|
|
93
104
|
SelectControlType,
|
94
105
|
SessionRequest,
|
95
106
|
DDBCActuatorStatus,
|
96
|
-
FRBCInstruction,
|
97
107
|
PEBCEnergyConstraint,
|
98
108
|
PEBCInstruction,
|
99
109
|
Handshake,
|
@@ -115,6 +125,7 @@ S2MessageElement = Union[
|
|
115
125
|
FRBCOperationModeElement,
|
116
126
|
FRBCStorageDescription,
|
117
127
|
FRBCUsageForecastElement,
|
128
|
+
OMBCOperationMode,
|
118
129
|
PEBCAllowedLimitRange,
|
119
130
|
PEBCPowerEnvelope,
|
120
131
|
PEBCPowerEnvelopeElement,
|
@@ -0,0 +1,5 @@
|
|
1
|
+
from s2python.ombc.ombc_instruction import OMBCInstruction
|
2
|
+
from s2python.ombc.ombc_operation_mode import OMBCOperationMode
|
3
|
+
from s2python.ombc.ombc_status import OMBCStatus
|
4
|
+
from s2python.ombc.ombc_system_description import OMBCSystemDescription
|
5
|
+
from s2python.ombc.ombc_timer_status import OMBCTimerStatus
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import uuid
|
2
|
+
|
3
|
+
from s2python.generated.gen_s2 import OMBCInstruction as GenOMBCInstruction
|
4
|
+
from s2python.validate_values_mixin import (
|
5
|
+
catch_and_convert_exceptions,
|
6
|
+
S2MessageComponent,
|
7
|
+
)
|
8
|
+
|
9
|
+
|
10
|
+
@catch_and_convert_exceptions
|
11
|
+
class OMBCInstruction(GenOMBCInstruction, S2MessageComponent):
|
12
|
+
model_config = GenOMBCInstruction.model_config
|
13
|
+
model_config["validate_assignment"] = True
|
14
|
+
|
15
|
+
id: uuid.UUID = GenOMBCInstruction.model_fields["id"] # type: ignore[assignment]
|
16
|
+
message_id: uuid.UUID = GenOMBCInstruction.model_fields["message_id"] # type: ignore[assignment]
|
17
|
+
abnormal_condition: bool = GenOMBCInstruction.model_fields["abnormal_condition"] # type: ignore[assignment]
|
18
|
+
operation_mode_factor: float = GenOMBCInstruction.model_fields["operation_mode_factor"] # type: ignore[assignment]
|
19
|
+
operation_mode_id: uuid.UUID = GenOMBCInstruction.model_fields["operation_mode_id"] # type: ignore[assignment]
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from typing import List
|
2
|
+
import uuid
|
3
|
+
|
4
|
+
from s2python.generated.gen_s2 import OMBCOperationMode as GenOMBCOperationMode
|
5
|
+
from s2python.common.power_range import PowerRange
|
6
|
+
|
7
|
+
|
8
|
+
from s2python.validate_values_mixin import (
|
9
|
+
catch_and_convert_exceptions,
|
10
|
+
S2MessageComponent,
|
11
|
+
)
|
12
|
+
|
13
|
+
|
14
|
+
@catch_and_convert_exceptions
|
15
|
+
class OMBCOperationMode(GenOMBCOperationMode, S2MessageComponent):
|
16
|
+
model_config = GenOMBCOperationMode.model_config
|
17
|
+
model_config["validate_assignment"] = True
|
18
|
+
|
19
|
+
id: uuid.UUID = GenOMBCOperationMode.model_fields["id"] # type: ignore[assignment]
|
20
|
+
power_ranges: List[PowerRange] = GenOMBCOperationMode.model_fields[
|
21
|
+
"power_ranges"
|
22
|
+
] # type: ignore[assignment]
|
23
|
+
abnormal_condition_only: bool = GenOMBCOperationMode.model_fields[
|
24
|
+
"abnormal_condition_only"
|
25
|
+
] # type: ignore[assignment]
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import uuid
|
2
|
+
|
3
|
+
from s2python.generated.gen_s2 import OMBCStatus as GenOMBCStatus
|
4
|
+
|
5
|
+
from s2python.validate_values_mixin import (
|
6
|
+
catch_and_convert_exceptions,
|
7
|
+
S2MessageComponent,
|
8
|
+
)
|
9
|
+
|
10
|
+
|
11
|
+
@catch_and_convert_exceptions
|
12
|
+
class OMBCStatus(GenOMBCStatus, S2MessageComponent):
|
13
|
+
model_config = GenOMBCStatus.model_config
|
14
|
+
model_config["validate_assignment"] = True
|
15
|
+
|
16
|
+
message_id: uuid.UUID = GenOMBCStatus.model_fields["message_id"] # type: ignore[assignment]
|
17
|
+
operation_mode_factor: float = GenOMBCStatus.model_fields["operation_mode_factor"] # type: ignore[assignment]
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from typing import List
|
2
|
+
import uuid
|
3
|
+
|
4
|
+
from s2python.generated.gen_s2 import OMBCSystemDescription as GenOMBCSystemDescription
|
5
|
+
from s2python.ombc.ombc_operation_mode import OMBCOperationMode
|
6
|
+
from s2python.common.transition import Transition
|
7
|
+
from s2python.common.timer import Timer
|
8
|
+
|
9
|
+
from s2python.validate_values_mixin import (
|
10
|
+
catch_and_convert_exceptions,
|
11
|
+
S2MessageComponent,
|
12
|
+
)
|
13
|
+
|
14
|
+
|
15
|
+
@catch_and_convert_exceptions
|
16
|
+
class OMBCSystemDescription(GenOMBCSystemDescription, S2MessageComponent):
|
17
|
+
model_config = GenOMBCSystemDescription.model_config
|
18
|
+
model_config["validate_assignment"] = True
|
19
|
+
|
20
|
+
message_id: uuid.UUID = GenOMBCSystemDescription.model_fields["message_id"] # type: ignore[assignment]
|
21
|
+
operation_modes: List[OMBCOperationMode] = GenOMBCSystemDescription.model_fields[
|
22
|
+
"operation_modes"
|
23
|
+
] # type: ignore[assignment]
|
24
|
+
transitions: List[Transition] = GenOMBCSystemDescription.model_fields["transitions"] # type: ignore[assignment]
|
25
|
+
timers: List[Timer] = GenOMBCSystemDescription.model_fields["timers"] # type: ignore[assignment]
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from uuid import UUID
|
2
|
+
|
3
|
+
from s2python.generated.gen_s2 import OMBCTimerStatus as GenOMBCTimerStatus
|
4
|
+
|
5
|
+
from s2python.validate_values_mixin import (
|
6
|
+
catch_and_convert_exceptions,
|
7
|
+
S2MessageComponent,
|
8
|
+
)
|
9
|
+
|
10
|
+
|
11
|
+
@catch_and_convert_exceptions
|
12
|
+
class OMBCTimerStatus(GenOMBCTimerStatus, S2MessageComponent):
|
13
|
+
model_config = GenOMBCTimerStatus.model_config
|
14
|
+
model_config["validate_assignment"] = True
|
15
|
+
|
16
|
+
message_id: UUID = GenOMBCTimerStatus.model_fields["message_id"] # type: ignore[assignment]
|
17
|
+
timer_id: UUID = GenOMBCTimerStatus.model_fields["timer_id"] # type: ignore[assignment]
|
@@ -1,3 +1,5 @@
|
|
1
|
+
from typing_extensions import Self
|
2
|
+
from pydantic import model_validator
|
1
3
|
from s2python.generated.gen_s2 import (
|
2
4
|
PEBCAllowedLimitRange as GenPEBCAllowedLimitRange,
|
3
5
|
PEBCPowerEnvelopeLimitType as GenPEBCPowerEnvelopeLimitType,
|
@@ -24,3 +26,17 @@ class PEBCAllowedLimitRange(GenPEBCAllowedLimitRange, S2MessageComponent):
|
|
24
26
|
abnormal_condition_only: bool = [
|
25
27
|
GenPEBCAllowedLimitRange.model_fields["abnormal_condition_only"] # type: ignore[assignment,reportIncompatibleVariableOverride]
|
26
28
|
]
|
29
|
+
|
30
|
+
@model_validator(mode="after")
|
31
|
+
def validate_range_boundary(self) -> Self:
|
32
|
+
# According to the specification "There must be at least one PEBC.AllowedLimitRange for the UPPER_LIMIT
|
33
|
+
# and at least one AllowedLimitRange for the LOWER_LIMIT." However for something that produces energy
|
34
|
+
# end_of_range=-2000 and start_of_range=0 is valid. Therefore absolute value used here.
|
35
|
+
if abs(self.range_boundary.start_of_range) > abs(
|
36
|
+
self.range_boundary.end_of_range
|
37
|
+
):
|
38
|
+
raise ValueError(
|
39
|
+
self,
|
40
|
+
"The start of the range must be smaller or equal than the end of the range.",
|
41
|
+
)
|
42
|
+
return self
|
@@ -1,9 +1,14 @@
|
|
1
1
|
import uuid
|
2
|
-
from typing import List
|
2
|
+
from typing import List, Dict, Tuple
|
3
|
+
from typing_extensions import Self
|
3
4
|
|
5
|
+
from pydantic import model_validator
|
6
|
+
|
7
|
+
from s2python.common import CommodityQuantity
|
4
8
|
from s2python.generated.gen_s2 import (
|
5
9
|
PEBCPowerConstraints as GenPEBCPowerConstraints,
|
6
10
|
PEBCPowerEnvelopeConsequenceType as GenPEBCPowerEnvelopeConsequenceType,
|
11
|
+
PEBCPowerEnvelopeLimitType,
|
7
12
|
)
|
8
13
|
from s2python.pebc.pebc_allowed_limit_range import PEBCAllowedLimitRange
|
9
14
|
from s2python.validate_values_mixin import (
|
@@ -25,3 +30,48 @@ class PEBCPowerConstraints(GenPEBCPowerConstraints, S2MessageComponent):
|
|
25
30
|
allowed_limit_ranges: List[PEBCAllowedLimitRange] = GenPEBCPowerConstraints.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
|
26
31
|
"allowed_limit_ranges"
|
27
32
|
] # type: ignore[assignment]
|
33
|
+
|
34
|
+
@model_validator(mode="after")
|
35
|
+
def validate_has_one_upper_one_lower_limit_range(self) -> Self:
|
36
|
+
|
37
|
+
commodity_type_ranges: Dict[CommodityQuantity, Tuple[bool, bool]] = {}
|
38
|
+
|
39
|
+
for limit_range in self.allowed_limit_ranges:
|
40
|
+
current: Tuple[bool, bool] = commodity_type_ranges.get(
|
41
|
+
limit_range.commodity_quantity, (False, False)
|
42
|
+
)
|
43
|
+
|
44
|
+
if limit_range.limit_type == PEBCPowerEnvelopeLimitType.UPPER_LIMIT:
|
45
|
+
current = (
|
46
|
+
True,
|
47
|
+
current[1],
|
48
|
+
)
|
49
|
+
|
50
|
+
if limit_range.limit_type == PEBCPowerEnvelopeLimitType.LOWER_LIMIT:
|
51
|
+
current = (
|
52
|
+
current[0],
|
53
|
+
True,
|
54
|
+
)
|
55
|
+
|
56
|
+
commodity_type_ranges[limit_range.commodity_quantity] = current
|
57
|
+
|
58
|
+
valid = True
|
59
|
+
|
60
|
+
for upper, lower in commodity_type_ranges.values():
|
61
|
+
valid = valid and upper and lower
|
62
|
+
|
63
|
+
if not valid:
|
64
|
+
raise ValueError(
|
65
|
+
self,
|
66
|
+
"There shall be at least one PEBC.AllowedLimitRange for the UPPER_LIMIT and at least one AllowedLimitRange for the LOWER_LIMIT.",
|
67
|
+
)
|
68
|
+
|
69
|
+
return self
|
70
|
+
|
71
|
+
@model_validator(mode="after")
|
72
|
+
def validate_valid_until_after_valid_from(self) -> Self:
|
73
|
+
if self.valid_until is not None and self.valid_until < self.valid_from:
|
74
|
+
raise ValueError(
|
75
|
+
self, "valid_until cannot be set to a value that is before valid_from."
|
76
|
+
)
|
77
|
+
return self
|
s2python/s2_connection.py
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
try:
|
2
|
+
import websockets
|
3
|
+
except ImportError as exc:
|
4
|
+
raise ImportError(
|
5
|
+
"The 'websockets' package is required. Run 'pip install s2-python[ws]' to use this feature."
|
6
|
+
) from exc
|
7
|
+
|
1
8
|
import asyncio
|
2
9
|
import json
|
3
10
|
import logging
|
@@ -8,8 +15,10 @@ import ssl
|
|
8
15
|
from dataclasses import dataclass
|
9
16
|
from typing import Any, Optional, List, Type, Dict, Callable, Awaitable, Union
|
10
17
|
|
11
|
-
import
|
12
|
-
|
18
|
+
from websockets.asyncio.client import (
|
19
|
+
ClientConnection as WSConnection,
|
20
|
+
connect as ws_connect,
|
21
|
+
)
|
13
22
|
|
14
23
|
from s2python.common import (
|
15
24
|
ReceptionStatusValues,
|
@@ -56,7 +65,8 @@ class AssetDetails: # pylint: disable=too-many-instance-attributes
|
|
56
65
|
) -> ResourceManagerDetails:
|
57
66
|
return ResourceManagerDetails(
|
58
67
|
available_control_types=[
|
59
|
-
control_type.get_protocol_control_type()
|
68
|
+
control_type.get_protocol_control_type()
|
69
|
+
for control_type in control_types
|
60
70
|
],
|
61
71
|
currency=self.currency,
|
62
72
|
firmware_version=self.firmware_version,
|
@@ -171,7 +181,9 @@ class MessageHandlers:
|
|
171
181
|
type(msg),
|
172
182
|
)
|
173
183
|
|
174
|
-
def register_handler(
|
184
|
+
def register_handler(
|
185
|
+
self, msg_type: Type[S2Message], handler: S2MessageHandler
|
186
|
+
) -> None:
|
175
187
|
"""Register a coroutine function or a normal function as the handler for a specific S2 message type.
|
176
188
|
|
177
189
|
:param msg_type: The S2 message type to attach the handler to.
|
@@ -228,7 +240,9 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
228
240
|
self.asset_details = asset_details
|
229
241
|
self._verify_certificate = verify_certificate
|
230
242
|
|
231
|
-
self._handlers.register_handler(
|
243
|
+
self._handlers.register_handler(
|
244
|
+
SelectControlType, self.handle_select_control_type_as_rm
|
245
|
+
)
|
232
246
|
self._handlers.register_handler(Handshake, self.handle_handshake)
|
233
247
|
self._handlers.register_handler(HandshakeResponse, self.handle_handshake_response_as_rm)
|
234
248
|
self._bearer_token = bearer_token
|
@@ -310,7 +324,10 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
310
324
|
await task
|
311
325
|
except asyncio.CancelledError:
|
312
326
|
pass
|
313
|
-
except (
|
327
|
+
except (
|
328
|
+
websockets.ConnectionClosedError,
|
329
|
+
websockets.ConnectionClosedOK,
|
330
|
+
):
|
314
331
|
logger.info("The other party closed the websocket connection.")
|
315
332
|
|
316
333
|
for task in pending:
|
@@ -344,10 +361,14 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
344
361
|
async def _connect_as_rm(self) -> None:
|
345
362
|
await self.send_msg_and_await_reception_status_async(
|
346
363
|
Handshake(
|
347
|
-
message_id=uuid.uuid4(),
|
364
|
+
message_id=uuid.uuid4(),
|
365
|
+
role=self.role,
|
366
|
+
supported_protocol_versions=[S2_VERSION],
|
348
367
|
)
|
349
368
|
)
|
350
|
-
logger.debug(
|
369
|
+
logger.debug(
|
370
|
+
"Send handshake to CEM. Expecting Handshake and HandshakeResponse from CEM."
|
371
|
+
)
|
351
372
|
|
352
373
|
await self._handle_received_messages()
|
353
374
|
|
@@ -356,7 +377,8 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
356
377
|
) -> None:
|
357
378
|
if not isinstance(message, Handshake):
|
358
379
|
logger.error(
|
359
|
-
"Handler for Handshake received a message of the wrong type: %s",
|
380
|
+
"Handler for Handshake received a message of the wrong type: %s",
|
381
|
+
type(message),
|
360
382
|
)
|
361
383
|
return
|
362
384
|
|
@@ -379,7 +401,9 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
379
401
|
|
380
402
|
logger.debug("Received HandshakeResponse %s", message.to_json())
|
381
403
|
|
382
|
-
logger.debug(
|
404
|
+
logger.debug(
|
405
|
+
"CEM selected to use version %s", message.selected_protocol_version
|
406
|
+
)
|
383
407
|
await send_okay
|
384
408
|
logger.debug("Handshake complete. Sending first ResourceManagerDetails.")
|
385
409
|
|
@@ -399,22 +423,29 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
399
423
|
|
400
424
|
await send_okay
|
401
425
|
|
402
|
-
logger.debug(
|
426
|
+
logger.debug(
|
427
|
+
"CEM selected control type %s. Activating control type.",
|
428
|
+
message.control_type,
|
429
|
+
)
|
403
430
|
|
404
431
|
control_types_by_protocol_name = {
|
405
432
|
c.get_protocol_control_type(): c for c in self.control_types
|
406
433
|
}
|
407
|
-
selected_control_type: Optional[S2ControlType] =
|
408
|
-
message.control_type
|
434
|
+
selected_control_type: Optional[S2ControlType] = (
|
435
|
+
control_types_by_protocol_name.get(message.control_type)
|
409
436
|
)
|
410
437
|
|
411
438
|
if self._current_control_type is not None:
|
412
|
-
await self._eventloop.run_in_executor(
|
439
|
+
await self._eventloop.run_in_executor(
|
440
|
+
None, self._current_control_type.deactivate, self
|
441
|
+
)
|
413
442
|
|
414
443
|
self._current_control_type = selected_control_type
|
415
444
|
|
416
445
|
if self._current_control_type is not None:
|
417
|
-
await self._eventloop.run_in_executor(
|
446
|
+
await self._eventloop.run_in_executor(
|
447
|
+
None, self._current_control_type.activate, self
|
448
|
+
)
|
418
449
|
self._current_control_type.register_handlers(self._handlers)
|
419
450
|
|
420
451
|
async def _receive_messages(self) -> None:
|
@@ -485,7 +516,9 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
485
516
|
async def respond_with_reception_status(
|
486
517
|
self, subject_message_id: uuid.UUID, status: ReceptionStatusValues, diagnostic_label: str
|
487
518
|
) -> None:
|
488
|
-
logger.debug(
|
519
|
+
logger.debug(
|
520
|
+
"Responding to message %s with status %s", subject_message_id, status
|
521
|
+
)
|
489
522
|
await self._send_and_forget(
|
490
523
|
ReceptionStatus(
|
491
524
|
subject_message_id=subject_message_id,
|
@@ -498,12 +531,17 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
498
531
|
self, subject_message_id: uuid.UUID, status: ReceptionStatusValues, diagnostic_label: str
|
499
532
|
) -> None:
|
500
533
|
asyncio.run_coroutine_threadsafe(
|
501
|
-
self.respond_with_reception_status(
|
534
|
+
self.respond_with_reception_status(
|
535
|
+
subject_message_id, status, diagnostic_label
|
536
|
+
),
|
502
537
|
self._eventloop,
|
503
538
|
).result()
|
504
539
|
|
505
540
|
async def send_msg_and_await_reception_status_async(
|
506
|
-
self,
|
541
|
+
self,
|
542
|
+
s2_msg: S2Message,
|
543
|
+
timeout_reception_status: float = 5.0,
|
544
|
+
raise_on_error: bool = True,
|
507
545
|
) -> ReceptionStatus:
|
508
546
|
await self._send_and_forget(s2_msg)
|
509
547
|
logger.debug(
|
@@ -524,12 +562,17 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
524
562
|
raise
|
525
563
|
|
526
564
|
if reception_status.status != ReceptionStatusValues.OK and raise_on_error:
|
527
|
-
raise RuntimeError(
|
565
|
+
raise RuntimeError(
|
566
|
+
f"ReceptionStatus was not OK but rather {reception_status.status}"
|
567
|
+
)
|
528
568
|
|
529
569
|
return reception_status
|
530
570
|
|
531
571
|
def send_msg_and_await_reception_status_sync(
|
532
|
-
self,
|
572
|
+
self,
|
573
|
+
s2_msg: S2Message,
|
574
|
+
timeout_reception_status: float = 5.0,
|
575
|
+
raise_on_error: bool = True,
|
533
576
|
) -> ReceptionStatus:
|
534
577
|
return asyncio.run_coroutine_threadsafe(
|
535
578
|
self.send_msg_and_await_reception_status_async(
|
s2python/s2_control_type.py
CHANGED
@@ -4,6 +4,7 @@ import typing
|
|
4
4
|
from s2python.common import ControlType as ProtocolControlType
|
5
5
|
from s2python.frbc import FRBCInstruction
|
6
6
|
from s2python.ppbc import PPBCScheduleInstruction
|
7
|
+
from s2python.ombc import OMBCInstruction
|
7
8
|
from s2python.message import S2Message
|
8
9
|
|
9
10
|
if typing.TYPE_CHECKING:
|
@@ -66,6 +67,26 @@ class PPBCControlType(S2ControlType):
|
|
66
67
|
"""Overwrite with the actual deactivation logic of your Resource Manager for this particular control type."""
|
67
68
|
|
68
69
|
|
70
|
+
class OMBCControlType(S2ControlType):
|
71
|
+
def get_protocol_control_type(self) -> ProtocolControlType:
|
72
|
+
return ProtocolControlType.OPERATION_MODE_BASED_CONTROL
|
73
|
+
|
74
|
+
def register_handlers(self, handlers: "MessageHandlers") -> None:
|
75
|
+
handlers.register_handler(OMBCInstruction, self.handle_instruction)
|
76
|
+
|
77
|
+
@abc.abstractmethod
|
78
|
+
def handle_instruction(
|
79
|
+
self, conn: "S2Connection", msg: S2Message, send_okay: typing.Callable[[], None]
|
80
|
+
) -> None: ...
|
81
|
+
|
82
|
+
@abc.abstractmethod
|
83
|
+
def activate(self, conn: "S2Connection") -> None:
|
84
|
+
"""Overwrite with the actual dctivation logic of your Resource Manager for this particular control type."""
|
85
|
+
|
86
|
+
@abc.abstractmethod
|
87
|
+
def deactivate(self, conn: "S2Connection") -> None:
|
88
|
+
"""Overwrite with the actual deactivation logic of your Resource Manager for this particular control type."""
|
89
|
+
|
69
90
|
|
70
91
|
class PEBCControlType(S2ControlType):
|
71
92
|
def get_protocol_control_type(self) -> ProtocolControlType:
|
s2python/s2_parser.py
CHANGED
@@ -24,6 +24,7 @@ from s2python.frbc import (
|
|
24
24
|
FRBCTimerStatus,
|
25
25
|
FRBCUsageForecast,
|
26
26
|
)
|
27
|
+
from s2python.pebc import PEBCPowerConstraints, PEBCEnergyConstraint, PEBCInstruction
|
27
28
|
from s2python.ppbc import PPBCScheduleInstruction
|
28
29
|
|
29
30
|
from s2python.message import S2Message
|
@@ -48,6 +49,9 @@ TYPE_TO_MESSAGE_CLASS: Dict[str, Type[S2Message]] = {
|
|
48
49
|
"FRBC.TimerStatus": FRBCTimerStatus,
|
49
50
|
"FRBC.UsageForecast": FRBCUsageForecast,
|
50
51
|
"PPBC.ScheduleInstruction": PPBCScheduleInstruction,
|
52
|
+
"PEBC.PowerConstraints": PEBCPowerConstraints,
|
53
|
+
"PEBC.Instruction": PEBCInstruction,
|
54
|
+
"PEBC.EnergyConstraint": PEBCEnergyConstraint,
|
51
55
|
"Handshake": Handshake,
|
52
56
|
"HandshakeResponse": HandshakeResponse,
|
53
57
|
"InstructionStatusUpdate": InstructionStatusUpdate,
|
@@ -90,7 +94,9 @@ class S2Parser:
|
|
90
94
|
return TYPE_TO_MESSAGE_CLASS[message_type].model_validate(message_json)
|
91
95
|
|
92
96
|
@staticmethod
|
93
|
-
def parse_as_message(
|
97
|
+
def parse_as_message(
|
98
|
+
unparsed_message: Union[dict, str, bytes], as_message: Type[M]
|
99
|
+
) -> M:
|
94
100
|
"""Parse the message to a specific S2 python message.
|
95
101
|
|
96
102
|
:param unparsed_message: The message as a JSON-formatted string or as a JSON-parsed dictionary.
|
s2python/s2_validation_error.py
CHANGED
@@ -10,4 +10,6 @@ class S2ValidationError(Exception):
|
|
10
10
|
class_: Optional[Type]
|
11
11
|
obj: object
|
12
12
|
msg: str
|
13
|
-
pydantic_validation_error: Union[
|
13
|
+
pydantic_validation_error: Union[
|
14
|
+
ValidationErrorV1, ValidationError, TypeError, None
|
15
|
+
]
|
File without changes
|
File without changes
|