Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion ci/lint.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env sh

. .venv/bin/activate
pylint src/ tests/unit/
pylint src/ tests/unit/ examples/
2 changes: 1 addition & 1 deletion ci/typecheck.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env sh

. .venv/bin/activate
mypy --config-file mypy.ini src/ ./tests/unit/
mypy --config-file mypy.ini src/ ./tests/unit/ examples/
38 changes: 19 additions & 19 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ alabaster==0.7.13
# via sphinx
annotated-types==0.7.0
# via pydantic
argcomplete==3.4.0
argcomplete==3.5.0
# via datamodel-code-generator
astroid==3.2.4
# via pylint
babel==2.15.0
babel==2.16.0
# via sphinx
black==24.4.2
black==24.8.0
# via datamodel-code-generator
build==1.2.1
# via pip-tools
cachetools==5.4.0
cachetools==5.5.0
# via tox
certifi==2024.7.4
certifi==2024.8.30
# via requests
cfgv==3.4.0
# via pre-commit
Expand All @@ -35,11 +35,9 @@ click==8.1.7
# s2-python (setup.cfg)
colorama==0.4.6
# via tox
coverage[toml]==7.6.0
# via
# coverage
# pytest-cov
datamodel-code-generator==0.25.8
coverage[toml]==7.6.1
# via pytest-cov
datamodel-code-generator==0.26.0
# via s2-python (setup.cfg)
dill==0.3.8
# via pylint
Expand All @@ -64,13 +62,13 @@ genson==1.3.0
# via datamodel-code-generator
identify==2.6.0
# via pre-commit
idna==3.7
idna==3.8
# via
# email-validator
# requests
imagesize==1.4.1
# via sphinx
importlib-metadata==8.2.0
importlib-metadata==8.4.0
# via
# build
# sphinx
Expand All @@ -90,7 +88,7 @@ markupsafe==2.1.5
# via jinja2
mccabe==0.7.0
# via pylint
mypy==1.11.0
mypy==1.11.2
# via s2-python (setup.cfg)
mypy-extensions==1.0.0
# via
Expand Down Expand Up @@ -133,7 +131,7 @@ pygments==2.18.0
# via
# sphinx
# sphinx-tabs
pylint==3.2.6
pylint==3.2.7
# via s2-python (setup.cfg)
pyproject-api==1.7.1
# via tox
Expand All @@ -158,7 +156,7 @@ pytz==2024.1
# via
# babel
# s2-python (setup.cfg)
pyyaml==6.0.1
pyyaml==6.0.2
# via
# datamodel-code-generator
# pre-commit
Expand Down Expand Up @@ -214,9 +212,9 @@ tomli==2.0.1
# pyproject-api
# pytest
# tox
tomlkit==0.13.0
tomlkit==0.13.2
# via pylint
tox==4.16.0
tox==4.18.0
# via s2-python (setup.cfg)
types-pytz==2024.1.0.20240417
# via s2-python (setup.cfg)
Expand All @@ -235,9 +233,11 @@ virtualenv==20.26.3
# via
# pre-commit
# tox
wheel==0.43.0
websockets==13.0.1
# via s2-python (setup.cfg)
wheel==0.44.0
# via pip-tools
zipp==3.19.2
zipp==3.20.1
# via importlib-metadata

# The following packages are considered to be unsafe in a requirements file:
Expand Down
172 changes: 172 additions & 0 deletions examples/example_frbc_rm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import logging
import sys
import uuid
import signal
import datetime
from typing import Callable

from s2python.common import (
EnergyManagementRole,
Duration,
Role,
RoleType,
Commodity,
Currency,
NumberRange,
PowerRange,
CommodityQuantity,
)
from s2python.frbc import (
FRBCInstruction,
FRBCSystemDescription,
FRBCActuatorDescription,
FRBCStorageDescription,
FRBCOperationMode,
FRBCOperationModeElement,
FRBCFillLevelTargetProfile,
FRBCFillLevelTargetProfileElement,
FRBCStorageStatus,
FRBCActuatorStatus,
)
from s2python.s2_connection import S2Connection, AssetDetails
from s2python.s2_control_type import FRBCControlType, NoControlControlType
from s2python.validate_values_mixin 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:
if not isinstance(msg, FRBCInstruction):
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:
print("The control type FRBC is now activated.")

print("Time to send a FRBC SystemDescription")
actuator_id = uuid.uuid4()
operation_mode_id = uuid.uuid4()
conn.send_msg_and_await_reception_status_sync(
FRBCSystemDescription(
message_id=uuid.uuid4(),
valid_from=datetime.datetime.now(tz=datetime.timezone.utc),
actuators=[
FRBCActuatorDescription(
id=actuator_id,
operation_modes=[
FRBCOperationMode(
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
),
power_ranges=[
PowerRange(
start_of_range=-200.0,
end_of_range=200.0,
commodity_quantity=CommodityQuantity.ELECTRIC_POWER_L1,
)
],
)
],
diagnostic_label="Load & unload battery",
abnormal_condition_only=False,
)
],
transitions=[],
timers=[],
supported_commodities=[Commodity.ELECTRICITY],
)
],
storage=FRBCStorageDescription(
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,
provides_leakage_behaviour=False,
provides_usage_forecast=False,
),
)
)
print("Also send the target profile")

conn.send_msg_and_await_reception_status_sync(
FRBCFillLevelTargetProfile(
message_id=uuid.uuid4(),
start_time=datetime.datetime.now(tz=datetime.timezone.utc),
elements=[
FRBCFillLevelTargetProfileElement(
duration=Duration.from_milliseconds(30_000),
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),
),
],
)
)

print("Also send the storage status.")
conn.send_msg_and_await_reception_status_sync(
FRBCStorageStatus(message_id=uuid.uuid4(), present_fill_level=10.0)
)

print("Also send the actuator status.")
conn.send_msg_and_await_reception_status_sync(
FRBCActuatorStatus(
message_id=uuid.uuid4(),
actuator_id=actuator_id,
active_operation_mode_id=operation_mode_id,
operation_mode_factor=0.5,
)
)

def deactivate(self, conn: S2Connection) -> None:
print("The control type FRBC is now deactivated.")


class MyNoControlControlType(NoControlControlType):
def activate(self, conn: S2Connection) -> None:
print("The control type NoControl is now activated.")

def deactivate(self, conn: S2Connection) -> None:
print("The control type NoControl is now deactivated.")


s2_conn = S2Connection(
url="ws://localhost:8001/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],
),
)


def stop(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()
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ install_requires =
pydantic~=2.8.2
pytz
click
websockets~=13.0.1

[options.packages.find]
where = src
Expand Down
2 changes: 1 addition & 1 deletion src/s2python/common/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from s2python.generated.gen_s2 import (
RoleType,
Currency,
CommodityQuantity,
Commodity,
InstructionStatus,
ReceptionStatusValues,
EnergyManagementRole,
SessionRequestType,
ControlType,
Currency,
RevokableObjects,
)

Expand Down
Empty file added src/s2python/frbc/rm.py
Empty file.
60 changes: 60 additions & 0 deletions src/s2python/reception_status_awaiter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""ReceptationStatusAwaiter class which notifies any coroutine waiting for a certain reception status message.

Copied from
https://github.com/flexiblepower/s2-analyzer/blob/main/backend/s2_analyzer_backend/reception_status_awaiter.py under
Apache2 license on 31-08-2024.
"""

import asyncio
import uuid
from typing import Dict

from s2python.common import ReceptionStatus


class ReceptionStatusAwaiter:
received: Dict[uuid.UUID, ReceptionStatus]
awaiting: Dict[uuid.UUID, asyncio.Event]

def __init__(self) -> None:
self.received = {}
self.awaiting = {}

async def wait_for_reception_status(
self, message_id: uuid.UUID, timeout_reception_status: float
) -> ReceptionStatus:
if message_id in self.received:
reception_status = self.received[message_id]
else:
if message_id in self.awaiting:
received_event = self.awaiting[message_id]
else:
received_event = asyncio.Event()
self.awaiting[message_id] = received_event

await asyncio.wait_for(received_event.wait(), timeout_reception_status)
reception_status = self.received[message_id]

if message_id in self.awaiting:
del self.awaiting[message_id]

return reception_status

async def receive_reception_status(self, reception_status: ReceptionStatus) -> None:
if not isinstance(reception_status, ReceptionStatus):
raise RuntimeError(
f"Expected a ReceptionStatus but received message {reception_status}"
)

if reception_status.subject_message_id in self.received:
raise RuntimeError(
f"ReceptationStatus for message_subject_id {reception_status.subject_message_id} has already "
f"been received!"
)

self.received[reception_status.subject_message_id] = reception_status
awaiting = self.awaiting.get(reception_status.subject_message_id)

if awaiting:
awaiting.set()
del self.awaiting[reception_status.subject_message_id]
Loading