From b8443f8b6a83281105e1c808d133a35fbe727990 Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Fri, 14 Mar 2025 09:43:21 +0100 Subject: [PATCH 1/7] started getting pairing details and decrypring challenge --- examples/example_frbc_rm.py | 60 ++++++++++++++---------- examples/example_with_pairing_frbc_rm.py | 37 +++++++++++++++ src/s2python/generated/gen_s2_pairing.py | 14 +++--- src/s2python/s2_pairing.py | 16 +++---- 4 files changed, 87 insertions(+), 40 deletions(-) create mode 100644 examples/example_with_pairing_frbc_rm.py diff --git a/examples/example_frbc_rm.py b/examples/example_frbc_rm.py index 774d936..8c2390e 100644 --- a/examples/example_frbc_rm.py +++ b/examples/example_frbc_rm.py @@ -1,3 +1,6 @@ +import argparse +import re +from functools import partial import logging import sys import uuid @@ -150,30 +153,35 @@ def activate(self, conn: S2Connection) -> None: def deactivate(self, conn: S2Connection) -> None: print("The control type NoControl is now deactivated.") - -s2_conn = S2Connection( - url="ws://localhost:8080/backend/rm/s2python-frbc/cem/dummy_model/ws", - role=EnergyManagementRole.RM, - control_types=[MyFRBCControlType(), MyNoControlControlType()], - asset_details=AssetDetails( - resource_id=str(uuid.uuid4()), - name="Some asset", - instruction_processing_delay=Duration.from_milliseconds(20), - roles=[Role(role=RoleType.ENERGY_CONSUMER, commodity=Commodity.ELECTRICITY)], - currency=Currency.EUR, - provides_forecast=False, - provides_power_measurements=[CommodityQuantity.ELECTRIC_POWER_L1], - ), - reconnect=True, -) - - -def stop(signal_num, _current_stack_frame): +def stop(s2_connection, signal_num, _current_stack_frame): print(f"Received signal {signal_num}. Will stop S2 connection.") - s2_conn.stop() - - -signal.signal(signal.SIGINT, stop) -signal.signal(signal.SIGTERM, stop) - -s2_conn.start_as_rm() + s2_connection.stop() + +def start_s2_session(url, client_node_id=str(uuid.uuid4())): + s2_conn = S2Connection( + url=url, + role=EnergyManagementRole.RM, + control_types=[MyFRBCControlType(), MyNoControlControlType()], + asset_details=AssetDetails( + resource_id=client_node_id, + name="Some asset", + instruction_processing_delay=Duration.from_milliseconds(20), + roles=[Role(role=RoleType.ENERGY_CONSUMER, commodity=Commodity.ELECTRICITY)], + currency=Currency.EUR, + provides_forecast=False, + provides_power_measurements=[CommodityQuantity.ELECTRIC_POWER_L1] + ), + reconnect=True, + verify_certificate=False + ) + signal.signal(signal.SIGINT, partial(stop, s2_conn)) + signal.signal(signal.SIGTERM, partial(stop, s2_conn)) + + s2_conn.start_as_rm() + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="A simple S2 reseource manager example.") + parser.add_argument('endpoint', type=str, help="WebSocket endpoint uri for the server (CEM) e.h. ws://localhost:8080/websocket/s2/my-first-websocket-rm") + args = parser.parse_args() + + start_s2_session(args.endpoint) diff --git a/examples/example_with_pairing_frbc_rm.py b/examples/example_with_pairing_frbc_rm.py new file mode 100644 index 0000000..3cc579c --- /dev/null +++ b/examples/example_with_pairing_frbc_rm.py @@ -0,0 +1,37 @@ +import argparse +import re +import uuid +import logging + +from example_frbc_rm import start_s2_session +from s2python.s2_pairing import PairingDetails, S2Pairing +from s2python.generated.gen_s2_pairing import S2NodeDescription, Deployment +from s2python.generated.gen_s2 import EnergyManagementRole + +logger = logging.getLogger("s2python") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="A simple S2 reseource manager example.") + parser.add_argument('endpoint', type=str, help="Rest endpoint to start S2 pairing. E.g. https://localhost/requestPairing") + parser.add_argument('pairing_token', type=str, help="The pairing toekn for teh endpoint. You should get this from the S2 server e.g. ca14fda4") + args = parser.parse_args() + + nodeDescription: S2NodeDescription = S2NodeDescription(brand="TNO", + logoUri = "https://www.tno.nl/publish/pages/5604/tno-logo-1484x835_003_.jpg", + type = "demo frbc example", + modelName = "S2 pairing example stub", + userDefinedName = "TNO S2 pairing example for frbc", + role = EnergyManagementRole.RM, + deployment = Deployment.LAN) + client_node_id: str = str(uuid.uuid4()) + + pairing: S2Pairing = S2Pairing(request_pairing_endpoint = args.endpoint, + token = args.pairing_token, + s2_client_node_description = nodeDescription, + client_node_id = client_node_id ) + + logger.info(f'Pairing details: \n{pairing.pairing_details}') + #print(pairing.pairing_details) + + start_s2_session(pairing.pairing_details.connection_details.connectionUri) \ No newline at end of file diff --git a/src/s2python/generated/gen_s2_pairing.py b/src/s2python/generated/gen_s2_pairing.py index 7ed825d..df45a08 100644 --- a/src/s2python/generated/gen_s2_pairing.py +++ b/src/s2python/generated/gen_s2_pairing.py @@ -8,16 +8,18 @@ from typing import List from pydantic import BaseModel, ConfigDict, Field -from s2python.common import EnergyManagementRole as S2Role - -class Deployment(Enum): +class S2Role(str, Enum): + CEM = 'CEM' + RM = 'RM' + +class Deployment(str, Enum): WAN = 'WAN' LAN = 'LAN' -class Protocols(Enum): +class Protocols(str, Enum): WebSocketSecure = 'WebSocketSecure' @@ -50,7 +52,7 @@ class PairingRequest(BaseModel): token: str publicKey: str s2ClientNodeId: str - s2ClientNodeDescription: str + s2ClientNodeDescription: S2NodeDescription supportedProtocols: List[Protocols] @@ -59,7 +61,7 @@ class PairingResponse(BaseModel): extra='forbid', ) s2ServerNodeId: str - serverNodeDescription: str + serverNodeDescription: S2NodeDescription requestConnectionUri: str diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index c28f8fd..d5570bf 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -5,7 +5,7 @@ from typing import Tuple, Union import requests -from jwskate import JweCompact, Jwk +from jwskate import JweCompact, Jwk, Jwt from binapy.binapy import BinaPy from s2python.generated.gen_s2_pairing import (Protocols, @@ -77,7 +77,6 @@ def _pair(self) -> None: self._paring_timestamp = datetime.datetime.now() rsa_key_pair = Jwk.generate_for_alg(KEY_ALGORITHM).with_kid_thumbprint() - pairing_request: PairingRequest = PairingRequest(token=self._token, publicKey=rsa_key_pair.public_jwk().to_pem(), s2ClientNodeId=self._client_node_id, @@ -85,11 +84,11 @@ def _pair(self) -> None: supportedProtocols=self._supported_protocols) response = requests.post(self._request_pairing_endpoint, - json=pairing_request.model_dump_json(), - timeout=REQTEST_TIMEOUT, + json = pairing_request.dict(), + timeout = REQTEST_TIMEOUT, verify = self._verify_certificate) response.raise_for_status() - pairing_response: PairingResponse = PairingResponse.parse_raw(response.json()) + pairing_response: PairingResponse = PairingResponse.parse_raw(response.text) connection_request: ConnectionRequest = ConnectionRequest(s2ClientNodeId=self._client_node_id, supportedProtocols=self._supported_protocols) @@ -102,12 +101,13 @@ def _pair(self) -> None: 'requestConnection') response = requests.post(restest_pairing_uri, - json=connection_request.model_dump_json(), - timeout=REQTEST_TIMEOUT, + json = connection_request.dict(), + timeout = REQTEST_TIMEOUT, verify = self._verify_certificate) response.raise_for_status() - connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.json()) + connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.text) challenge = JweCompact(connection_details.challenge).decrypt(rsa_key_pair) + challenge = Jwt.unprotected(JweCompact(connection_details.challenge).decrypt(rsa_key_pair)) self._pairing_details = PairingDetails(pairing_response, connection_details, challenge) From 6421621b0310bf643addcdb8e09b2df4efeaa219 Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Fri, 14 Mar 2025 09:46:40 +0100 Subject: [PATCH 2/7] catch up with changes in main to catch_and_convert_exceptions --- src/s2python/validate_values_mixin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/s2python/validate_values_mixin.py b/src/s2python/validate_values_mixin.py index cc9c6fd..fa4a8d7 100644 --- a/src/s2python/validate_values_mixin.py +++ b/src/s2python/validate_values_mixin.py @@ -59,7 +59,10 @@ def inner(*args: List[Any], **kwargs: Dict[str, Any]) -> Any: return inner -def catch_and_convert_exceptions(input_class: Type[S2MessageComponent[B_co]]) -> Type[S2MessageComponent[B_co]]: +S = TypeVar("S", bound=S2MessageComponent) + + +def catch_and_convert_exceptions(input_class: Type[S]) -> Type[S]: input_class.__init__ = convert_to_s2exception(input_class.__init__) # type: ignore[method-assign] input_class.__setattr__ = convert_to_s2exception(input_class.__setattr__) # type: ignore[method-assign] input_class.model_validate_json = convert_to_s2exception( # type: ignore[method-assign] From 5232c33a3f2e58bf50e15291117f4180ac615b99 Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Fri, 14 Mar 2025 11:22:57 +0100 Subject: [PATCH 3/7] addingbearer token to websocket --- examples/example_frbc_rm.py | 12 +++++--- examples/example_with_pairing_frbc_rm.py | 36 +++++++++++++----------- src/s2python/s2_pairing.py | 4 +-- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/examples/example_frbc_rm.py b/examples/example_frbc_rm.py index 8c2390e..ff10fd4 100644 --- a/examples/example_frbc_rm.py +++ b/examples/example_frbc_rm.py @@ -1,5 +1,4 @@ import argparse -import re from functools import partial import logging import sys @@ -157,7 +156,7 @@ def stop(s2_connection, signal_num, _current_stack_frame): print(f"Received signal {signal_num}. Will stop S2 connection.") s2_connection.stop() -def start_s2_session(url, client_node_id=str(uuid.uuid4())): +def start_s2_session(url, client_node_id=str(uuid.uuid4()), bearer_token=None): s2_conn = S2Connection( url=url, role=EnergyManagementRole.RM, @@ -172,7 +171,8 @@ def start_s2_session(url, client_node_id=str(uuid.uuid4())): provides_power_measurements=[CommodityQuantity.ELECTRIC_POWER_L1] ), reconnect=True, - verify_certificate=False + verify_certificate=False, + bearer_token=bearer_token ) signal.signal(signal.SIGINT, partial(stop, s2_conn)) signal.signal(signal.SIGTERM, partial(stop, s2_conn)) @@ -181,7 +181,11 @@ def start_s2_session(url, client_node_id=str(uuid.uuid4())): if __name__ == "__main__": parser = argparse.ArgumentParser(description="A simple S2 reseource manager example.") - parser.add_argument('endpoint', type=str, help="WebSocket endpoint uri for the server (CEM) e.h. ws://localhost:8080/websocket/s2/my-first-websocket-rm") + parser.add_argument( + 'endpoint', + type=str, + help="WebSocket endpoint uri for the server (CEM) e.h. ws://localhost:8080/websocket/s2/my-first-websocket-rm" + ) args = parser.parse_args() start_s2_session(args.endpoint) diff --git a/examples/example_with_pairing_frbc_rm.py b/examples/example_with_pairing_frbc_rm.py index 3cc579c..74a31e1 100644 --- a/examples/example_with_pairing_frbc_rm.py +++ b/examples/example_with_pairing_frbc_rm.py @@ -1,10 +1,9 @@ import argparse -import re import uuid import logging from example_frbc_rm import start_s2_session -from s2python.s2_pairing import PairingDetails, S2Pairing +from s2python.s2_pairing import S2Pairing from s2python.generated.gen_s2_pairing import S2NodeDescription, Deployment from s2python.generated.gen_s2 import EnergyManagementRole @@ -13,25 +12,30 @@ if __name__ == "__main__": parser = argparse.ArgumentParser(description="A simple S2 reseource manager example.") - parser.add_argument('endpoint', type=str, help="Rest endpoint to start S2 pairing. E.g. https://localhost/requestPairing") - parser.add_argument('pairing_token', type=str, help="The pairing toekn for teh endpoint. You should get this from the S2 server e.g. ca14fda4") + parser.add_argument('endpoint', + type=str, + help="Rest endpoint to start S2 pairing. E.g. https://localhost/requestPairing") + parser.add_argument('pairing_token', + type=str, + help="The pairing toekn for teh endpoint. You should get this from the S2 server e.g. ca14fda4") args = parser.parse_args() - nodeDescription: S2NodeDescription = S2NodeDescription(brand="TNO", - logoUri = "https://www.tno.nl/publish/pages/5604/tno-logo-1484x835_003_.jpg", - type = "demo frbc example", - modelName = "S2 pairing example stub", - userDefinedName = "TNO S2 pairing example for frbc", - role = EnergyManagementRole.RM, - deployment = Deployment.LAN) + nodeDescription: S2NodeDescription = \ + S2NodeDescription(brand="TNO", + logoUri = "https://www.tno.nl/publish/pages/5604/tno-logo-1484x835_003_.jpg", + type = "demo frbc example", + modelName = "S2 pairing example stub", + userDefinedName = "TNO S2 pairing example for frbc", + role = EnergyManagementRole.RM, + deployment = Deployment.LAN) client_node_id: str = str(uuid.uuid4()) pairing: S2Pairing = S2Pairing(request_pairing_endpoint = args.endpoint, token = args.pairing_token, s2_client_node_description = nodeDescription, - client_node_id = client_node_id ) + client_node_id = client_node_id) - logger.info(f'Pairing details: \n{pairing.pairing_details}') - #print(pairing.pairing_details) - - start_s2_session(pairing.pairing_details.connection_details.connectionUri) \ No newline at end of file + logger.info("Pairing details: \n%s", pairing.pairing_details) + + start_s2_session(pairing.pairing_details.connection_details.connectionUri, + bearer_token=pairing.pairing_details.decrypted_challenge) diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index d5570bf..d018220 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -107,8 +107,8 @@ def _pair(self) -> None: response.raise_for_status() connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.text) challenge = JweCompact(connection_details.challenge).decrypt(rsa_key_pair) - challenge = Jwt.unprotected(JweCompact(connection_details.challenge).decrypt(rsa_key_pair)) - self._pairing_details = PairingDetails(pairing_response, connection_details, challenge) + decrypted_challenge_token = Jwt.unprotected(challenge).decrypt(rsa_key_pair)) + self._pairing_details = PairingDetails(pairing_response, connection_details, decrypted_challenge_token) @property From f4434fec4e5588312dc999a12221229386389f3a Mon Sep 17 00:00:00 2001 From: Dr Maurice Hendrix Date: Fri, 14 Mar 2025 10:53:16 +0100 Subject: [PATCH 4/7] S2 connection kwargs (#94) added %LOCALAPPDATA% to gitignore to prevent checking it in on windows --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8a082ed..5baf340 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ venv .tox/ dist/ build/ +%LOCALAPPDATA% From 3c81f027bd2ef26e266c0c86ddc308a8174e9a92 Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Fri, 14 Mar 2025 11:35:33 +0100 Subject: [PATCH 5/7] removed extra ) --- src/s2python/s2_pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index d018220..dd4559a 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -107,7 +107,7 @@ def _pair(self) -> None: response.raise_for_status() connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.text) challenge = JweCompact(connection_details.challenge).decrypt(rsa_key_pair) - decrypted_challenge_token = Jwt.unprotected(challenge).decrypt(rsa_key_pair)) + decrypted_challenge_token = Jwt.unprotected(challenge).decrypt(rsa_key_pair) self._pairing_details = PairingDetails(pairing_response, connection_details, decrypted_challenge_token) From 88254861d0b691b9dfb62acaed357bfcd9537dd4 Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Fri, 14 Mar 2025 12:06:44 +0100 Subject: [PATCH 6/7] fixed typing error in s2 pairing class --- src/s2python/s2_pairing.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index dd4559a..5968bae 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -2,7 +2,8 @@ import uuid import datetime from dataclasses import dataclass -from typing import Tuple, Union +from typing import Tuple, Union, Mapping, Any +import json import requests from jwskate import JweCompact, Jwk, Jwt @@ -106,8 +107,8 @@ def _pair(self) -> None: verify = self._verify_certificate) response.raise_for_status() connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.text) - challenge = JweCompact(connection_details.challenge).decrypt(rsa_key_pair) - decrypted_challenge_token = Jwt.unprotected(challenge).decrypt(rsa_key_pair) + challenge: Mapping[str, Any] = json.loads(JweCompact(connection_details.challenge).decrypt(rsa_key_pair)) + decrypted_challenge_token: BinaPy = Jwt.unprotected(challenge).decrypt(rsa_key_pair) self._pairing_details = PairingDetails(pairing_response, connection_details, decrypted_challenge_token) From f86c6633e5fd6eb6b26fce12ee29f88a9f2de792 Mon Sep 17 00:00:00 2001 From: Maurice Hendrix Date: Fri, 14 Mar 2025 12:18:24 +0100 Subject: [PATCH 7/7] fix typing errors in s2 pairing --- src/s2python/s2_pairing.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/s2python/s2_pairing.py b/src/s2python/s2_pairing.py index 5968bae..2675746 100644 --- a/src/s2python/s2_pairing.py +++ b/src/s2python/s2_pairing.py @@ -6,8 +6,7 @@ import json import requests -from jwskate import JweCompact, Jwk, Jwt -from binapy.binapy import BinaPy +from jwskate import JweCompact, Jwk, Jwt, SignedJwt from s2python.generated.gen_s2_pairing import (Protocols, PairingRequest, @@ -29,10 +28,10 @@ class PairingDetails: """The result of an S2 pairing :param pairing_response: Details about the server. :param connection_details: Details about how to connect. - :param supported_protocols: The decrypted challenge needed as bearer token.""" + :param decrypted_challenge: The decrypted challenge needed as bearer token.""" pairing_response: PairingResponse connection_details: ConnectionDetails - decrypted_challenge: BinaPy + decrypted_challenge: str class S2Pairing: # pylint: disable=too-many-instance-attributes _pairing_details: PairingDetails @@ -108,8 +107,8 @@ def _pair(self) -> None: response.raise_for_status() connection_details: ConnectionDetails = ConnectionDetails.parse_raw(response.text) challenge: Mapping[str, Any] = json.loads(JweCompact(connection_details.challenge).decrypt(rsa_key_pair)) - decrypted_challenge_token: BinaPy = Jwt.unprotected(challenge).decrypt(rsa_key_pair) - self._pairing_details = PairingDetails(pairing_response, connection_details, decrypted_challenge_token) + decrypted_challenge_token: SignedJwt = Jwt.unprotected(challenge) + self._pairing_details = PairingDetails(pairing_response, connection_details, str(decrypted_challenge_token)) @property