Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2815ede
First commit of the abstract server and a default server
VladIftime May 23, 2025
cb9cd6b
fix: comment out line that deadlocks the server when stopping the server
Flix6x May 23, 2025
279bc25
Fixing challange
VladIftime May 23, 2025
6db48f6
Merge branch 'feat/abstract_server' of github.com:flexiblepower/s2-py…
VladIftime May 23, 2025
57f0839
Moved example to appropiate folder
VladIftime May 26, 2025
e712082
Chore: Fixed examples
VladIftime May 26, 2025
c30551d
Separated the hhtp and ws instance
VladIftime May 26, 2025
9a7a495
Refactor default server (#123)
Flix6x May 26, 2025
5077b80
docs: add notes on running examples
Flix6x May 26, 2025
323f32d
Websocekt connecteion esteablisehd with new specs. Next step is to ac…
VladIftime Jun 9, 2025
d2bebf0
S2Connection works for both RM and CEM
VladIftime Jun 10, 2025
0fa4b94
Serever side default logic for the WS. Also modifed the example
VladIftime Jun 12, 2025
c360091
CEM does not receive more than one message
VladIftime Jun 13, 2025
4aa4528
fix: tell server how to process incoming requests
Flix6x Jun 13, 2025
e8c9648
Added a receive_messages method
VladIftime Jun 13, 2025
29369a6
Merge branch 'feat/abstract_server' of https://github.com/flexiblepow…
VladIftime Jun 13, 2025
ec2f5f7
feat: log subject message type and diagnostic labels for reception st…
Flix6x Jun 13, 2025
112773e
Merge branch 'feat/abstract_server' of https://github.com/flexiblepow…
VladIftime Jun 13, 2025
e7aec25
feat: also log subject message type and diagnostic labels for recepti…
Flix6x Jun 13, 2025
d8ea154
feat: also log message type in case of permanent error
Flix6x Jun 13, 2025
d0afbba
feat: improve diagnostic label of SendOkay
Flix6x Jun 13, 2025
47e69f4
Refactored default_ws_server. The handlers now need to receive the we…
VladIftime Jun 15, 2025
c003053
Merge branch 'feat/abstract_server' of https://github.com/flexiblepow…
VladIftime Jun 15, 2025
c7f5d16
Ignore
VladIftime Jun 15, 2025
d4949f2
Fully functioning example with http server, ws server and client.
VladIftime Jun 15, 2025
d8ea92c
Running server from single file with database and header ccheck for t…
VladIftime Jun 20, 2025
c1af98f
Added working examples for CEM server and RM client with auth/pair an…
VladIftime Jun 23, 2025
6014902
dev: lower quality standards temporarily (while in development)
VladIftime Jun 20, 2025
0e98fdc
dev: Fixing tests
VladIftime Jun 23, 2025
827279d
chore: Clean-up
VladIftime Jun 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ venv
dist/
build/
%LOCALAPPDATA%
temp_java/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's that about? (Seems a bit odd in a python library is all)

26 changes: 25 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ Add to your pyproject.toml:
[tool.mypy]
plugins = ['pydantic.mypy']

Example
Examples
---------

Use S2 classes in your code:

.. code-block:: python

from s2python.common import PowerRange, CommodityQuantity
Expand All @@ -54,6 +56,28 @@ Example
json_str = '{"start_of_range": 4.0, "end_of_range": 5.0, "commodity_quantity": "ELECTRIC.POWER.L1"}'
PowerRange.from_json(json_str)

Run an example CEM server with websocket and http server:

.. code-block:: bash

python -m examples.example_s2_server --host localhost --http-port 8000 --ws-port 8080 --pairing-token ca14fda4


This will start both a http and a websocket server instances. It also allows to set a hardcoded pairing token.

Run an example RM client that pairs with the CEM server, authenticates and starts sending S2 messages that describe an FRBC device:

.. code-block:: bash

python -m examples.example_pairing_frbc_rm --pairing_endpoint http://localhost:8000/requestPairing --pairing_token ca14fda4

In case you want to run the example of a client that does not need to pair with the CEM server, you can add the --dev-mode flag. This will disable the pairing/authentication check and allows you to send messages to the CEM server without pairing. The CEM server still needs to be running.

.. code-block:: bash

python -m examples.example_frbc_rm --endpoint ws://localhost:8080


Development
-------------

Expand Down
Binary file added challenges.db
Binary file not shown.
Empty file added examples/__init__.py
Empty file.
41 changes: 30 additions & 11 deletions examples/example_frbc_rm.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
FRBCStorageStatus,
FRBCActuatorStatus,
)
from s2python.s2_connection import S2Connection, AssetDetails
from s2python.communication.s2_connection import S2Connection, AssetDetails
from s2python.s2_control_type import FRBCControlType, NoControlControlType
from s2python.message import S2Message

Expand Down Expand Up @@ -92,7 +92,9 @@ def activate(self, conn: S2Connection) -> None:
)
],
storage=FRBCStorageDescription(
fill_level_range=NumberRange(start_of_range=0.0, end_of_range=100.0),
fill_level_range=NumberRange(
start_of_range=0.0, end_of_range=100.0
),
fill_level_label="%",
diagnostic_label="Imaginary battery",
provides_fill_level_target_profile=True,
Expand All @@ -110,11 +112,15 @@ def activate(self, conn: S2Connection) -> None:
elements=[
FRBCFillLevelTargetProfileElement(
duration=Duration.from_milliseconds(30_000),
fill_level_range=NumberRange(start_of_range=20.0, end_of_range=30.0),
fill_level_range=NumberRange(
start_of_range=20.0, end_of_range=30.0
),
),
FRBCFillLevelTargetProfileElement(
duration=Duration.from_milliseconds(300_000),
fill_level_range=NumberRange(start_of_range=40.0, end_of_range=50.0),
fill_level_range=NumberRange(
start_of_range=40.0, end_of_range=50.0
),
),
],
)
Expand Down Expand Up @@ -161,27 +167,40 @@ def start_s2_session(url, client_node_id=str(uuid.uuid4())):
resource_id=client_node_id,
name="Some asset",
instruction_processing_delay=Duration.from_milliseconds(20),
roles=[Role(role=RoleType.ENERGY_CONSUMER, commodity=Commodity.ELECTRICITY)],
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()
# Create signal handlers
def sigint_handler(signum, frame):
stop(s2_conn, signum, frame)

def sigterm_handler(signum, frame):
stop(s2_conn, signum, frame)

# Register signal handlers
signal.signal(signal.SIGINT, sigint_handler)
signal.signal(signal.SIGTERM, sigterm_handler)

s2_conn.start()


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="A simple S2 reseource manager example.")
parser = argparse.ArgumentParser(
description="A simple S2 reseource manager example."
)
parser.add_argument(
"endpoint",
"--endpoint",
type=str,
help="WebSocket endpoint uri for the server (CEM) e.g. "
"ws://localhost:8080/backend/rm/s2python-frbc/cem/dummy_model/ws",
"ws://localhost:8080/",
)
args = parser.parse_args()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import argparse
from functools import partial
import logging
import sys
import uuid
import signal
import threading
import datetime
from typing import Any, Callable, Optional
import uuid
from typing import Callable

from s2python.authorization.default_client import S2DefaultClient
from s2python.generated.gen_s2_pairing import (
S2NodeDescription,
Deployment,
PairingToken,
S2Role,
Protocols,
)

from s2python.common import (
EnergyManagementRole,
Expand Down Expand Up @@ -35,18 +42,12 @@
from s2python.message import S2Message

logger = logging.getLogger("s2python")
logger.addHandler(logging.StreamHandler(sys.stdout))
logger.setLevel(logging.DEBUG)


class MyFRBCControlType(FRBCControlType):
def handle_instruction(
self, conn: S2Connection, msg: S2Message, send_okay: Callable[[], None]
) -> None:
def handle_instruction(self, conn: S2Connection, msg: S2Message, send_okay: Callable[[], None]) -> None:
if not isinstance(msg, FRBCInstruction):
raise RuntimeError(
f"Expected an FRBCInstruction but received a message of type {type(msg)}."
)
raise RuntimeError(f"Expected an FRBCInstruction but received a message of type {type(msg)}.")
print(f"I have received the message {msg} from {conn}")

def activate(self, conn: S2Connection) -> None:
Expand All @@ -67,12 +68,8 @@ def activate(self, conn: S2Connection) -> None:
id=operation_mode_id,
elements=[
FRBCOperationModeElement(
fill_level_range=NumberRange(
start_of_range=0.0, end_of_range=100.0
),
fill_rate=NumberRange(
start_of_range=-5.0, end_of_range=5.0
),
fill_level_range=NumberRange(start_of_range=0.0, end_of_range=100.0),
fill_rate=NumberRange(start_of_range=-5.0, end_of_range=5.0),
power_ranges=[
PowerRange(
start_of_range=-200.0,
Expand All @@ -92,9 +89,7 @@ def activate(self, conn: S2Connection) -> None:
)
],
storage=FRBCStorageDescription(
fill_level_range=NumberRange(
start_of_range=0.0, end_of_range=100.0
),
fill_level_range=NumberRange(start_of_range=0.0, end_of_range=100.0),
fill_level_label="%",
diagnostic_label="Imaginary battery",
provides_fill_level_target_profile=True,
Expand All @@ -112,15 +107,11 @@ def activate(self, conn: S2Connection) -> None:
elements=[
FRBCFillLevelTargetProfileElement(
duration=Duration.from_milliseconds(30_000),
fill_level_range=NumberRange(
start_of_range=20.0, end_of_range=30.0
),
fill_level_range=NumberRange(start_of_range=20.0, end_of_range=30.0),
),
FRBCFillLevelTargetProfileElement(
duration=Duration.from_milliseconds(300_000),
fill_level_range=NumberRange(
start_of_range=40.0, end_of_range=50.0
),
fill_level_range=NumberRange(start_of_range=40.0, end_of_range=50.0),
),
],
)
Expand Down Expand Up @@ -153,52 +144,84 @@ def deactivate(self, conn: S2Connection) -> None:
print("The control type NoControl is now deactivated.")


def stop(
s2_connection: S2Connection, signal_num: int, _current_stack_frame: Any
) -> None:
print(f"Received signal {signal_num}. Will stop S2 connection.")
s2_connection.stop()


def start_s2_session(
url: str,
client_node_id: uuid.UUID = uuid.uuid4(),
bearer_token: Optional[str] = None,
) -> None:
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,
bearer_token=bearer_token,
)
signal.signal(signal.SIGINT, partial(stop, s2_conn))
signal.signal(signal.SIGTERM, partial(stop, s2_conn))

s2_conn.start_as_rm()
if __name__ == "__main__":
# Configuration
parser = argparse.ArgumentParser(description="S2 pairing example for FRBC RM")
parser.add_argument("--pairing_endpoint", type=str, required=True)
parser.add_argument("--pairing_token", type=str, required=True)

args = parser.parse_args()

if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="A simple S2 reseource manager example."
pairing_endpoint = args.pairing_endpoint
pairing_token = args.pairing_token

# --- Client Setup ---
# Create node description
node_description = 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=S2Role.RM,
deployment=Deployment.LAN,
)
parser.add_argument(
"endpoint",
type=str,
help="WebSocket endpoint uri for the server (CEM) e.g. ws://localhost:8080/websocket/s2/my-first-websocket-rm",

# Create a client to perform the pairing
client = S2DefaultClient(
pairing_uri=pairing_endpoint,
token=PairingToken(token=pairing_token),
node_description=node_description,
verify_certificate=False,
supported_protocols=[Protocols.WebSocketSecure],
)
args = parser.parse_args()

start_s2_session(args.endpoint)
try:
# Request pairing
logger.info("Initiating pairing with endpoint: %s", pairing_endpoint)
pairing_response = client.request_pairing()
logger.info("Pairing request successful, requesting connection...")

# Request connection details
connection_details = client.request_connection()
logger.info("Connection request successful")

# Solve challenge
challenge_result = client.solve_challenge()
logger.info("Challenge solved successfully")

s2_connection = S2Connection(
url=connection_details.connectionUri, # type: ignore
role=EnergyManagementRole.RM,
control_types=[MyFRBCControlType(), MyNoControlControlType()],
asset_details=AssetDetails(
resource_id=client.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,
bearer_token=challenge_result,
)

# Start S2 session with the connection details
logger.info("Starting S2 session...")
s2_connection.start_as_rm()
logger.info("S2 session is running. Press Ctrl+C to exit.")

# Keep the main thread alive to allow the WebSocket connection to run.
event = threading.Event()
event.wait()

except KeyboardInterrupt:
logger.info("Program interrupted by user.")
except Exception as e:
logger.error("Error during pairing process: %s", e, exc_info=True)
raise e
finally:
client.close_connection()
logger.info("Connection closed.")
Loading