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.
@@ -1,23 +1,25 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: s2-python
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: S2 Protocol Python Wrapper
5
- Home-page: https://github.com/flexiblepower/s2-ws-json-python
6
- Author: Flexiblepower
7
- Author-email: info@info.nl
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
- Description-Content-Type: text/x-rst; charset=UTF-8
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
- Requires-Dist: websockets~=13.1
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 Pypi:
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 Pypi may be found `here <https://pypi.org/project/s2-python/>`_
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=Ql0Aj9pjkhKa8eYe2o78B48zAI5oqAPUsqNz_yp5k-s,3127
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=DNn27sE5IVQvkxPBVWnRB6CMf6NiEG1ftGyuBJWe9z0,21062
6
- s2python/s2_control_type.py,sha256=-Rwz2emQkk4hL20GvfWN90Iu1rxBT_DBIQ38uuMrxcs,3281
7
- s2python/s2_parser.py,sha256=BBwSFMXz5Ut-aGqiVQCurlr4T_VZiB31dmWzAudmat8,4340
8
- s2python/s2_validation_error.py,sha256=BkOLoNsrcQ3MzdCYaPDgs1Wu6lPdlQDpZsTpykKGQmE,384
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=VZiFlIUzDyWu5UrSnNqmzT5umIt9mjJiYaFeMX8WWgE,726
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=cONUwv9gJEQLmxgKPHu9Qto1wm5GCITe-au8RlkmMd4,870
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=EMfy6sPeo9f55yjvKhhN8gz5KTWk9RI4Z8qzevib-M8,743
22
- s2python/common/power_range.py,sha256=dz27VZJPE6-3Ubo1vPRX5FG1sGVIjjENJ7aySM2cV6A,672
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=xA_loJFgkcXKzGabgEI0zbgQ8E40rhbWB1lyV0MGkcE,6353
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=aJtVUnRYr2gApkQQyDNikv8hTZyOQAIivx6PpUNIFdQ,1324
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=PIREQPoA4P11BHqnRjdAnWFkikrrHoJ_p3XD4LZ7uhI,1118
50
- s2python/frbc/frbc_operation_mode.py,sha256=a39Cs_pe09QkzLYmsv2agsnL0DJzxQHMooNPgprn5Sg,1970
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=-l2jUe1F45R1G51_Zo8_f2vpKx9Sp436BVbop4TSqy8,63401
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=vqLfXOg8E9rO5KLTru8GODiH6akE2KLWrTQMYEkSIZM,1229
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=5Xhsm3Uc0dO6KOyp9fFDTdKGPfNuvXz8BYDAi782XEg,1286
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.5.0.dist-info/METADATA,sha256=5pQe4viDShRojNuDNFDpxR4LbVXLrygO5RjBh9Ojb9Y,3638
79
- s2_python-0.5.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
80
- s2_python-0.5.0.dist-info/entry_points.txt,sha256=feX-xmgJZgSe5-jxMgFKPKCJz4Ys3eQcGrsXsirNZyM,61
81
- s2_python-0.5.0.dist-info/top_level.txt,sha256=OLFq0oDhr77Mp-EYLEcWk5P3jvooOt4IHkTI5KYJMc8,9
82
- s2_python-0.5.0.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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.
@@ -1,6 +1,9 @@
1
1
  from typing import Any
2
2
 
3
- from s2python.validate_values_mixin import S2MessageComponent, catch_and_convert_exceptions
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 s2python.generated.gen_s2 import PowerForecastElement as GenPowerForecastElement
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 PowerMeasurement as GenPowerMeasurement
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
@@ -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(self, "start_of_range should not be higher than end_of_range")
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(self, f"Id {timer.id} was found multiple times in 'timers'.")
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(commodity, power_range.commodity_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 catch_and_convert_exceptions, S2MessageComponent
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 FRBCLeakageBehaviourElement as GenFRBCLeakageBehaviourElement
8
- from s2python.validate_values_mixin import catch_and_convert_exceptions, S2MessageComponent
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(sorted_fill_level_ranges):
38
- if current_fill_level_range.end_of_range != next_fill_level_range.start_of_range:
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 "
@@ -180,6 +180,7 @@ class NumberRange(BaseModel):
180
180
  class Transition(BaseModel):
181
181
  model_config = ConfigDict(
182
182
  extra="forbid",
183
+ populate_by_name=True
183
184
  )
184
185
  id: ID = Field(
185
186
  ...,
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 websockets
12
- from websockets.asyncio.client import ClientConnection as WSConnection, connect as ws_connect
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() for control_type in control_types
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(self, msg_type: Type[S2Message], handler: S2MessageHandler) -> None:
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(SelectControlType, self.handle_select_control_type_as_rm)
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 (websockets.ConnectionClosedError, websockets.ConnectionClosedOK):
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(), role=self.role, supported_protocol_versions=[S2_VERSION]
364
+ message_id=uuid.uuid4(),
365
+ role=self.role,
366
+ supported_protocol_versions=[S2_VERSION],
348
367
  )
349
368
  )
350
- logger.debug("Send handshake to CEM. Expecting Handshake and HandshakeResponse from CEM.")
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", type(message)
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("CEM selected to use version %s", message.selected_protocol_version)
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("CEM selected control type %s. Activating control type.", message.control_type)
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] = control_types_by_protocol_name.get(
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(None, self._current_control_type.deactivate, self)
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(None, self._current_control_type.activate, self)
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("Responding to message %s with status %s", subject_message_id, status)
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(subject_message_id, status, diagnostic_label),
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, s2_msg: S2Message, timeout_reception_status: float = 5.0, raise_on_error: bool = True
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(f"ReceptionStatus was not OK but rather {reception_status.status}")
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, s2_msg: S2Message, timeout_reception_status: float = 5.0, raise_on_error: bool = True
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(
@@ -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(unparsed_message: Union[dict, str, bytes], as_message: Type[M]) -> M:
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.
@@ -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[ValidationErrorV1, ValidationError, TypeError, None]
13
+ pydantic_validation_error: Union[
14
+ ValidationErrorV1, ValidationError, TypeError, None
15
+ ]