diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2bbe39..53ce7d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: strategy: matrix: python: - - "3.9" + - "3.10" # - "3.7" # oldest Python supported by PSF - "3.11" # newest Python that is stable platform: diff --git a/setup.cfg b/setup.cfg index 00da022..03e935f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,8 +52,8 @@ install_requires = aiohttp pandas pydantic>=1.10.8,<2.0 - async-timeout - + s2-python + async_timeout [options.packages.find] where = src @@ -74,6 +74,7 @@ testing = pytest-mock aioresponses + [options.entry_points] # Add here console scripts like: # console_scripts = diff --git a/src/flexmeasures_client/s2/__init__.py b/src/flexmeasures_client/s2/__init__.py index ce4d2fd..42f6813 100644 --- a/src/flexmeasures_client/s2/__init__.py +++ b/src/flexmeasures_client/s2/__init__.py @@ -7,12 +7,8 @@ from typing import Callable, Coroutine, Dict, Type import pydantic +from s2python.common import ReceptionStatus, ReceptionStatusValues, RevokeObject -from flexmeasures_client.s2.python_s2_protocol.common.messages import ( - ReceptionStatus, - ReceptionStatusValues, - RevokeObject, -) from flexmeasures_client.s2.utils import ( SizeLimitOrderedDict, get_message_id, @@ -58,7 +54,7 @@ def wrap(*args, **kwargs): except pydantic.ValidationError as e: return ReceptionStatus( - subject_message_id=incoming_message.message_id, + subject_message_id=str(incoming_message.message_id), diagnostic_label=get_validation_error_summary(e), status=ReceptionStatusValues.INVALID_DATA, ) # TODO: Discuss status @@ -201,9 +197,7 @@ def handle_response_status(self, message: ReceptionStatus): # save acknowledgement status code # TODO: implement function __hash__ in ID that returns the value of __root__ - self.outgoing_messages_status[ - message.subject_message_id.__root__ - ] = message.status + self.outgoing_messages_status[str(message.subject_message_id)] = message.status # choose which callback to call, depending on the ReceptionStatus value if message.status == ReceptionStatusValues.OK: @@ -212,12 +206,12 @@ def handle_response_status(self, message: ReceptionStatus): callback_store = self.failure_callbacks # pop callback from callback_store and run it, if there exists one - if callback := callback_store.pop(message.subject_message_id.__root__, None): + if callback := callback_store.pop(str(message.subject_message_id), None): callback() # delete success callback related to this message if callback is None and (message.status != ReceptionStatusValues.OK): - self.success_callbacks.pop(message.subject_message_id.__root__, None) + self.success_callbacks.pop(str(message.subject_message_id), None) @register(RevokeObject) def handle_revoke_object(self, message: RevokeObject): @@ -225,8 +219,8 @@ def handle_revoke_object(self, message: RevokeObject): Stores the revoked object ID into the objects_revoked list """ - self.objects_revoked.append(message.object_id.__root__) + self.objects_revoked.append(message.object_id) return ReceptionStatus( - subject_message_id=message.message_id, status=ReceptionStatusValues.OK + subject_message_id=str(message.message_id), status=ReceptionStatusValues.OK ) diff --git a/src/flexmeasures_client/s2/cem.py b/src/flexmeasures_client/s2/cem.py index 6e8f2af..c64bb82 100644 --- a/src/flexmeasures_client/s2/cem.py +++ b/src/flexmeasures_client/s2/cem.py @@ -7,11 +7,8 @@ from typing import Dict, Optional import pydantic - -from flexmeasures_client.client import FlexMeasuresClient -from flexmeasures_client.s2 import Handler, register -from flexmeasures_client.s2.control_types import ControlTypeHandler -from flexmeasures_client.s2.python_s2_protocol.common.messages import ( +from s2python.common import ( + ControlType, Handshake, HandshakeResponse, PowerMeasurement, @@ -21,7 +18,10 @@ RevokeObject, SelectControlType, ) -from flexmeasures_client.s2.python_s2_protocol.common.schemas import ControlType + +from flexmeasures_client.client import FlexMeasuresClient +from flexmeasures_client.s2 import Handler, register +from flexmeasures_client.s2.control_types import ControlTypeHandler from flexmeasures_client.s2.utils import get_reception_status, get_unique_id diff --git a/src/flexmeasures_client/s2/control_types/FRBC/__init__.py b/src/flexmeasures_client/s2/control_types/FRBC/__init__.py index efce7c2..e61e61a 100644 --- a/src/flexmeasures_client/s2/control_types/FRBC/__init__.py +++ b/src/flexmeasures_client/s2/control_types/FRBC/__init__.py @@ -1,14 +1,8 @@ import asyncio import pydantic - -from flexmeasures_client.s2 import SizeLimitOrderedDict, register -from flexmeasures_client.s2.control_types import ControlTypeHandler -from flexmeasures_client.s2.python_s2_protocol.common.messages import ( - ReceptionStatusValues, -) -from flexmeasures_client.s2.python_s2_protocol.common.schemas import ControlType -from flexmeasures_client.s2.python_s2_protocol.FRBC.messages import ( +from s2python.common import ControlType, ReceptionStatusValues +from s2python.frbc import ( FRBCActuatorStatus, FRBCFillLevelTargetProfile, FRBCInstruction, @@ -18,6 +12,9 @@ FRBCTimerStatus, FRBCUsageForecast, ) + +from flexmeasures_client.s2 import SizeLimitOrderedDict, register +from flexmeasures_client.s2.control_types import ControlTypeHandler from flexmeasures_client.s2.utils import get_reception_status, get_unique_id @@ -59,7 +56,7 @@ def __init__(self, max_size: int = 100) -> None: def handle_system_description( self, message: FRBCSystemDescription ) -> pydantic.BaseModel: - system_description_id = message.message_id.__root__ + system_description_id = str(message.message_id) # store system_description message for later self._system_description_history[system_description_id] = message @@ -77,7 +74,7 @@ async def send_actuator_status(self, status: FRBCActuatorStatus): @register(FRBCStorageStatus) def handle_storage_status(self, message: FRBCStorageStatus) -> pydantic.BaseModel: - message_id = message.message_id.__root__ + message_id = str(message.message_id) self._storage_status_history[message_id] = message @@ -87,7 +84,7 @@ def handle_storage_status(self, message: FRBCStorageStatus) -> pydantic.BaseMode @register(FRBCActuatorStatus) def handle_actuator_status(self, message: FRBCActuatorStatus) -> pydantic.BaseModel: - message_id = message.message_id.__root__ + message_id = str(message.message_id) self._actuator_status_history[message_id] = message @@ -110,6 +107,10 @@ def handle_usage_forecast(self, message: FRBCUsageForecast) -> pydantic.BaseMode async def trigger_schedule(self, system_description_id: str): raise NotImplementedError() + @register(FRBCTimerStatus) + def handle_frbc_timer_status(self, message: FRBCTimerStatus) -> pydantic.BaseModel: + return get_reception_status(message, status=ReceptionStatusValues.OK) + class FRBCTest(FRBC): """Dummy class to simulate the triggering of a schedule.""" @@ -129,8 +130,8 @@ async def trigger_schedule(self, system_description_id: str): instruction = FRBCInstruction( message_id=get_unique_id(), id=get_unique_id(), - actuator_id=actuator.id.__root__, - operation_mode=actuator.operation_modes[0].id.__root__, + actuator_id=actuator.id, + operation_mode=actuator.operation_modes[0].id, operation_mode_factor=0.5, execution_time=system_description.valid_from, abnormal_condition=False, diff --git a/src/flexmeasures_client/s2/control_types/FRBC/frbc_simple.py b/src/flexmeasures_client/s2/control_types/FRBC/frbc_simple.py index 99b56ee..85acb55 100644 --- a/src/flexmeasures_client/s2/control_types/FRBC/frbc_simple.py +++ b/src/flexmeasures_client/s2/control_types/FRBC/frbc_simple.py @@ -6,14 +6,10 @@ from datetime import datetime, timedelta import pytz +from s2python.frbc import FRBCActuatorStatus, FRBCStorageStatus, FRBCSystemDescription from flexmeasures_client.s2.control_types.FRBC import FRBC from flexmeasures_client.s2.control_types.FRBC.utils import fm_schedule_to_instructions -from flexmeasures_client.s2.python_s2_protocol.FRBC.messages import ( - FRBCActuatorStatus, - FRBCStorageStatus, - FRBCSystemDescription, -) class FRBCSimple(FRBC): diff --git a/src/flexmeasures_client/s2/control_types/FRBC/utils.py b/src/flexmeasures_client/s2/control_types/FRBC/utils.py index e3857e7..22ea836 100644 --- a/src/flexmeasures_client/s2/control_types/FRBC/utils.py +++ b/src/flexmeasures_client/s2/control_types/FRBC/utils.py @@ -3,12 +3,8 @@ from typing import List import pandas as pd +from s2python.frbc import FRBCInstruction, FRBCOperationMode, FRBCSystemDescription -from flexmeasures_client.s2.python_s2_protocol.FRBC.messages import ( - FRBCInstruction, - FRBCSystemDescription, -) -from flexmeasures_client.s2.python_s2_protocol.FRBC.schemas import FRBCOperationMode from flexmeasures_client.s2.utils import get_unique_id @@ -94,8 +90,8 @@ def fm_schedule_to_instructions( instruction = FRBCInstruction( message_id=get_unique_id(), id=get_unique_id(), - actuator_id=actuator.id.__root__, - operation_mode=operation_mode.id.__root__, + actuator_id=actuator.id, + operation_mode=operation_mode.id, operation_mode_factor=operation_mode_factor, execution_time=start, abnormal_condition=False, diff --git a/src/flexmeasures_client/s2/control_types/__init__.py b/src/flexmeasures_client/s2/control_types/__init__.py index 30fa6db..db310d5 100644 --- a/src/flexmeasures_client/s2/control_types/__init__.py +++ b/src/flexmeasures_client/s2/control_types/__init__.py @@ -4,17 +4,15 @@ from typing import cast from pydantic import BaseModel - -from flexmeasures_client.client import FlexMeasuresClient -from flexmeasures_client.s2 import Handler, register -from flexmeasures_client.s2.python_s2_protocol.common.messages import ( +from s2python.common import ( + ControlType, InstructionStatus, InstructionStatusUpdate, -) -from flexmeasures_client.s2.python_s2_protocol.common.schemas import ( - ControlType, ReceptionStatusValues, ) + +from flexmeasures_client.client import FlexMeasuresClient +from flexmeasures_client.s2 import Handler, register from flexmeasures_client.s2.utils import SizeLimitOrderedDict, get_reception_status @@ -33,7 +31,7 @@ def __init__(self, max_size: int = 100) -> None: @register(InstructionStatusUpdate) def handle_instruction_status_update(self, message: InstructionStatusUpdate): - instruction_id: str = cast(str, message.instruction_id.__root__) + instruction_id: str = cast(str, message.instruction_id) self._instruction_status_history[instruction_id] = message.status_type diff --git a/src/flexmeasures_client/s2/python_s2_protocol/FRBC/messages.py b/src/flexmeasures_client/s2/python_s2_protocol/FRBC/messages.py deleted file mode 100644 index d955128..0000000 --- a/src/flexmeasures_client/s2/python_s2_protocol/FRBC/messages.py +++ /dev/null @@ -1,179 +0,0 @@ -# flake8: noqa - -from __future__ import annotations - -from datetime import datetime -from typing import List, Optional - -from pydantic import BaseModel, Extra, Field, constr - -from flexmeasures_client.s2.python_s2_protocol.common.schemas import ( - ID, - Duration, - NumberRange, -) -from flexmeasures_client.s2.python_s2_protocol.FRBC.schemas import ( - FRBCActuatorDescription, - FRBCFillLevelTargetProfileElement, - FRBCLeakageBehaviourElement, - FRBCStorageDescription, - FRBCUsageForecastElement, -) - - -class FRBCActuatorStatus(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("FRBC.ActuatorStatus", const=True) - message_id: ID = Field(..., description="ID of this message") - actuator_id: ID = Field( - ..., description="ID of the actuator this messages refers to" - ) - active_operation_mode_id: ID = Field( - ..., description="ID of the FRBC.OperationMode that is presently active." - ) - operation_mode_factor: float = Field( - ..., - description="The number indicates the factor with which the FRBC.OperationMode is configured. The factor should be greater than or equal than 0 and less or equal to 1.", - ) - previous_operation_mode_id: Optional[ID] = Field( - None, - description="ID of the FRBC.OperationMode that was active before the present one. This value shall always be provided, unless the active FRBC.OperationMode is the first FRBC.OperationMode the Resource Manager is aware of.", - ) - transition_timestamp: Optional[datetime] = Field( - None, - description="Time at which the transition from the previous FRBC.OperationMode to the active FRBC.OperationMode was initiated. This value shall always be provided, unless the active FRBC.OperationMode is the first FRBC.OperationMode the Resource Manager is aware of.", - ) - - -class FRBCFillLevelTargetProfile(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("FRBC.FillLevelTargetProfile", const=True) - message_id: ID = Field(..., description="ID of this message") - start_time: datetime = Field( - ..., description="Time at which the FRBC.FillLevelTargetProfile starts." - ) - elements: List[FRBCFillLevelTargetProfileElement] = Field( - ..., - description="List of different fill levels that have to be targeted within a given duration. There shall be at least one element. Elements must be placed in chronological order.", - max_items=288, - min_items=1, - ) - - -class FRBCInstruction(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("FRBC.Instruction", const=True) - message_id: ID = Field(..., description="ID of this message") - id: ID = Field( - ..., - description="ID of the instruction. Must be unique in the scope of the Resource Manager, for at least the duration of the session between Resource Manager and CEM.", - ) - actuator_id: ID = Field( - ..., description="ID of the actuator this instruction belongs to." - ) - operation_mode: ID = Field( - ..., description="ID of the FRBC.OperationMode that should be activated." - ) - operation_mode_factor: float = Field( - ..., - description="The number indicates the factor with which the FRBC.OperationMode should be configured. The factor should be greater than or equal to 0 and less or equal to 1.", - ) - execution_time: datetime = Field( - ..., description="Time when instruction should be executed." - ) - abnormal_condition: bool = Field( - ..., - description="Indicates if this is an instruction during an abnormal condition.", - ) - - -class FRBCLeakageBehaviour(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("FRBC.LeakageBehaviour", const=True) - message_id: ID = Field(..., description="ID of this message") - valid_from: datetime = Field( - ..., - description="Moment this FRBC.LeakageBehaviour starts to be valid. If the FRBC.LeakageBehaviour is immediately valid, the DateTimeStamp should be now or in the past.", - ) - elements: List[FRBCLeakageBehaviourElement] = Field( - ..., - description="List of elements that model the leakage behaviour of the buffer. The fill_level_ranges of the elements must be contiguous.", - max_items=288, - min_items=1, - ) - - -class FRBCStorageStatus(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("FRBC.StorageStatus", const=True) - message_id: ID = Field(..., description="ID of this message") - present_fill_level: float = Field( - ..., description="Present fill level of the Storage" - ) - - -class FRBCSystemDescription(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("FRBC.SystemDescription", const=True) - message_id: ID = Field(..., description="ID of this message") - valid_from: datetime = Field( - ..., - description="Moment this FRBC.SystemDescription starts to be valid. If the system description is immediately valid, the DateTimeStamp should be now or in the past.", - ) - actuators: List[FRBCActuatorDescription] = Field( - ..., description="Details of all Actuators.", max_items=10, min_items=1 - ) - storage: FRBCStorageDescription = Field(..., description="Details of the storage.") - - -class FRBCTimerStatus(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("FRBC.TimerStatus", const=True) - message_id: ID = Field(..., description="ID of this message") - timer_id: ID = Field(..., description="The ID of the timer this message refers to") - actuator_id: ID = Field( - ..., description="The ID of the actuator the timer belongs to" - ) - finished_at: datetime = Field( - ..., - description="Indicates when the Timer will be finished. If the DateTimeStamp is in the future, the timer is not yet finished. If the DateTimeStamp is in the past, the timer is finished. If the timer was never started, the value can be an arbitrary DateTimeStamp in the past.", - ) - - -class FRBCUsageForecast(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("FRBC.UsageForecast", const=True) - message_id: ID = Field(..., description="ID of this message") - start_time: datetime = Field( - ..., description="Time at which the FRBC.UsageForecast starts." - ) - elements: List[FRBCUsageForecastElement] = Field( - ..., - description="Further elements that model the profile. There shall be at least one element. Elements must be placed in chronological order.", - max_items=288, - min_items=1, - ) diff --git a/src/flexmeasures_client/s2/python_s2_protocol/FRBC/schemas.py b/src/flexmeasures_client/s2/python_s2_protocol/FRBC/schemas.py deleted file mode 100644 index 2b1affe..0000000 --- a/src/flexmeasures_client/s2/python_s2_protocol/FRBC/schemas.py +++ /dev/null @@ -1,199 +0,0 @@ -# flake8: noqa - -from __future__ import annotations - -from typing import List, Optional - -from pydantic import BaseModel, Extra, Field - -from flexmeasures_client.s2.python_s2_protocol.common.schemas import ( - ID, - Commodity, - Duration, - NumberRange, - PowerRange, - Timer, - Transition, -) - - -class FRBCFillLevelTargetProfileElement(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - duration: Duration = Field(..., description="The duration of the element.") - fill_level_range: NumberRange = Field( - ..., - description="The target range in which the fill_level must be for the time period during which the element is active. The start of the range must be smaller or equal to the end of the range. The CEM must take best-effort actions to proactively achieve this target.", - ) - - -class FRBCLeakageBehaviourElement(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - fill_level_range: NumberRange = Field( - ..., - description="The fill level range for which this FRBC.LeakageBehaviourElement applies. The start of the range must be less than the end of the range.", - ) - leakage_rate: float = Field( - ..., - description="Indicates how fast the momentary fill level will decrease per second due to leakage within the given range of the fill level.", - ) - - -class FRBCOperationModeElement(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - fill_level_range: NumberRange = Field( - ..., - description="The range of the fill level for which this FRBC.OperationModeElement applies. The start of the NumberRange shall be smaller than the end of the NumberRange.", - ) - fill_rate: NumberRange = Field( - ..., - description="Indicates the change in fill_level per second. The lower_boundary of the NumberRange is associated with an operation_mode_factor of 0, the upper_boundary is associated with an operation_mode_factor of 1. ", - ) - power_ranges: List[PowerRange] = Field( - ..., - description="The power produced or consumed by this operation mode. The start of each PowerRange is associated with an operation_mode_factor of 0, the end is associated with an operation_mode_factor of 1. In the array there must be at least one PowerRange, and at most one PowerRange per CommodityQuantity.", - max_items=10, - min_items=1, - ) - running_costs: Optional[NumberRange] = Field( - None, - description="Additional costs per second (e.g. wear, services) associated with this operation mode in the currency defined by the ResourceManagerDetails, excluding the commodity cost. The range is expressing uncertainty and is not linked to the operation_mode_factor.", - ) - - -class FRBCOperationMode(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - id: ID = Field( - ..., - description="ID of the FRBC.OperationMode. Must be unique in the scope of the FRBC.ActuatorDescription in which it is used.", - ) - diagnostic_label: Optional[str] = Field( - None, - description="Human readable name/description of the FRBC.OperationMode. This element is only intended for diagnostic purposes and not for HMI applications.", - ) - elements: List[FRBCOperationModeElement] = Field( - ..., - description="List of FRBC.OperationModeElements, which describe the properties of this FRBC.OperationMode depending on the fill_level. The fill_level_ranges of the items in the Array must be contiguous.", - max_items=100, - min_items=1, - ) - abnormal_condition_only: bool = Field( - ..., - description="Indicates if this FRBC.OperationMode may only be used during an abnormal condition", - ) - - -class FRBCActuatorDescription(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - id: ID = Field( - ..., - description="ID of the Actuator. Must be unique in the scope of the Resource Manager, for at least the duration of the session between Resource Manager and CEM.", - ) - diagnostic_label: Optional[str] = Field( - None, - description="Human readable name/description for the actuator. This element is only intended for diagnostic purposes and not for HMI applications.", - ) - supported_commodities: List[Commodity] = Field( - ..., description="List of all supported Commodities.", max_items=4, min_items=1 - ) - operation_modes: List[FRBCOperationMode] = Field( - ..., - description="Provided FRBC.OperationModes associated with this actuator", - max_items=100, - min_items=1, - ) - transitions: List[Transition] = Field( - ..., - description="Possible transitions between FRBC.OperationModes associated with this actuator.", - max_items=1000, - min_items=0, - ) - timers: List[Timer] = Field( - ..., - description="List of Timers associated with this actuator", - max_items=1000, - min_items=0, - ) - - -class FRBCStorageDescription(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - diagnostic_label: Optional[str] = Field( - None, - description="Human readable name/description of the storage (e.g. hot water buffer or battery). This element is only intended for diagnostic purposes and not for HMI applications.", - ) - fill_level_label: Optional[str] = Field( - None, - description="Human readable description of the (physical) units associated with the fill_level (e.g. degrees Celsius or percentage state of charge). This element is only intended for diagnostic purposes and not for HMI applications.", - ) - provides_leakage_behaviour: bool = Field( - ..., - description="Indicates whether the Storage could provide details of power leakage behaviour through the FRBC.LeakageBehaviour.", - ) - provides_fill_level_target_profile: bool = Field( - ..., - description="Indicates whether the Storage could provide a target profile for the fill level through the FRBC.FillLevelTargetProfile.", - ) - provides_usage_forecast: bool = Field( - ..., - description="Indicates whether the Storage could provide a UsageForecast through the FRBC.UsageForecast.", - ) - fill_level_range: NumberRange = Field( - ..., - description="The range in which the fill_level should remain. It is expected of the CEM to keep the fill_level within this range. When the fill_level is not within this range, the Resource Manager can ignore instructions from the CEM (except during abnormal conditions). ", - ) - - -class FRBCUsageForecastElement(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - duration: Duration = Field( - ..., description="Indicator for how long the given usage_rate is valid." - ) - usage_rate_upper_limit: Optional[float] = Field( - None, - description="The upper limit of the range with a 100\xa0% probability that the usage rate is within that range.", - ) - usage_rate_upper_95PPR: Optional[float] = Field( - None, - description="The upper limit of the range with a 95\xa0% probability that the usage rate is within that range. ", - ) - usage_rate_upper_68PPR: Optional[float] = Field( - None, - description="The upper limit of the range with a 68\xa0% probability that the usage rate is within that range", - ) - usage_rate_expected: float = Field( - ..., - description="The most likely value for the usage rate; the expected increase or decrease of the fill_level per second", - ) - usage_rate_lower_68PPR: Optional[float] = Field( - None, - description="The lower limit of the range with a 68\xa0% probability that the usage rate is within that range", - ) - usage_rate_lower_95PPR: Optional[float] = Field( - None, - description="The lower limit of the range with a 95\xa0% probability that the usage rate is within that range", - ) - usage_rate_lower_limit: Optional[float] = Field( - None, - description="The lower limit of the range with a 100\xa0% probability that the usage rate is within that range", - ) diff --git a/src/flexmeasures_client/s2/python_s2_protocol/common/messages.py b/src/flexmeasures_client/s2/python_s2_protocol/common/messages.py deleted file mode 100644 index ef99d14..0000000 --- a/src/flexmeasures_client/s2/python_s2_protocol/common/messages.py +++ /dev/null @@ -1,221 +0,0 @@ -# flake8: noqa - -from __future__ import annotations - -from datetime import datetime -from typing import List, Optional - -from pydantic import BaseModel, Extra, Field - -from flexmeasures_client.s2.python_s2_protocol.common.schemas import ( - ID, - CommodityQuantity, - ControlType, - Currency, - Duration, - EnergyManagementRole, - InstructionStatus, - PowerForecastElement, - PowerValue, - ReceptionStatusValues, - RevokableObjects, - Role, - SessionRequestType, -) - - -class Handshake(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("Handshake", const=True) - message_id: ID = Field(..., description="ID of this message") - role: EnergyManagementRole = Field( - ..., description="The role of the sender of this message" - ) - supported_protocol_versions: Optional[List[str]] = Field( - None, - description="Protocol versions supported by the sender of this message. This field is mandatory for the RM, but optional for the CEM.", - min_items=1, - ) - - -class HandshakeResponse(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("HandshakeResponse", const=True) - message_id: ID = Field(..., description="ID of this message") - selected_protocol_version: str = Field( - ..., description="The protocol version the CEM selected for this session" - ) - - -class InstructionStatusUpdate(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("InstructionStatusUpdate", const=True) - message_id: ID = Field(..., description="ID of this message") - instruction_id: ID = Field( - ..., description="ID of this instruction (as provided by the CEM) " - ) - status_type: InstructionStatus = Field( - ..., description="Present status of this instruction." - ) - timestamp: datetime = Field( - ..., description="Timestamp when status_type has changed the last time." - ) - - -class PowerForecast(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("PowerForecast", const=True) - message_id: ID = Field(..., description="ID of this message") - start_time: datetime = Field( - ..., description="Start time of time period that is covered by the profile." - ) - elements: List[PowerForecastElement] = Field( - ..., - description="Elements of which this forecast consists. Contains at least one element. Elements must be placed in chronological order.", - max_items=288, - min_items=1, - ) - - -class PowerMeasurement(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("PowerMeasurement", const=True) - message_id: ID = Field(..., description="ID of this message") - measurement_timestamp: datetime = Field( - ..., description="Timestamp when PowerValues were measured." - ) - values: List[PowerValue] = Field( - ..., - description="Array of measured PowerValues. Must contain at least one item and at most one item per ‘commodity_quantity’ (defined inside the PowerValue).", # noqa: E501 - max_items=10, - min_items=1, - ) - - -class ReceptionStatus(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("ReceptionStatus", const=True) - subject_message_id: ID = Field( - ..., description="The message this ReceptionStatus refers to" - ) - status: ReceptionStatusValues = Field( - ..., description="Enumeration of status values" - ) - diagnostic_label: Optional[str] = Field( - None, - description="Diagnostic label that can be used to provide additional information for debugging. However, not for HMI purposes.", # noqa: E501 - ) - - -class ResourceManagerDetails(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("ResourceManagerDetails", const=True) - message_id: ID = Field(..., description="ID of this message") - resource_id: ID = Field( - ..., - description="Identifier of the Resource Manager. Must be unique within the scope of the CEM.", # noqa: E501 - ) - name: Optional[str] = Field(None, description="Human readable name given by user") - roles: List[Role] = Field( - ..., - description="Each Resource Manager provides one or more energy Roles", - max_items=3, - min_items=1, - ) - manufacturer: Optional[str] = Field(None, description="Name of Manufacturer") - model: Optional[str] = Field( - None, - description="Name of the model of the device (provided by the manufacturer)", - ) - serial_number: Optional[str] = Field( - None, description="Serial number of the device (provided by the manufacturer)" - ) - firmware_version: Optional[str] = Field( - None, - description="Version identifier of the firmware used in the device (provided by the manufacturer)", # noqa: E501 - ) - instruction_processing_delay: Duration = Field( - ..., - description="The average time the combination of Resource Manager and HBES/BACS/SASS or (Smart) device needs to process and execute an instruction", # noqa: E501 - ) - available_control_types: List[ControlType] = Field( - ..., - description="The control types supported by this Resource Manager.", - max_items=5, - min_items=1, - ) - currency: Optional[Currency] = Field( - None, - description="Currency to be used for all information regarding costs. Mandatory if cost information is published.", # noqa: E501 - ) - provides_forecast: bool = Field( - ..., - description="Indicates whether the ResourceManager is able to provide PowerForecasts", # noqa: E501 - ) - provides_power_measurement_types: List[CommodityQuantity] = Field( - ..., - description="Array of all CommodityQuantities that this Resource Manager can provide measurements for. ", # noqa: E501 - max_items=10, - min_items=1, - ) - - -class RevokeObject(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("RevokeObject", const=True) - message_id: ID = Field(..., description="ID of this message") - object_type: RevokableObjects = Field( - ..., description="The type of object that needs to be revoked" - ) - object_id: ID = Field(..., description="The ID of object that needs to be revoked") - - -class SelectControlType(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("SelectControlType", const=True) - message_id: ID = Field(..., description="ID of this message") - control_type: ControlType = Field( - ..., - description="The ControlType to activate. Must be one of the available ControlTypes as defined in the ResourceManagerDetails", - ) - - -class SessionRequest(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - message_type: str = Field("SessionRequest", const=True) - message_id: ID = Field(..., description="ID of this message") - request: SessionRequestType = Field(..., description="The type of request") - diagnostic_label: Optional[str] = Field( - None, - description="Optional field for a human readible descirption for debugging purposes", - ) diff --git a/src/flexmeasures_client/s2/python_s2_protocol/common/schemas.py b/src/flexmeasures_client/s2/python_s2_protocol/common/schemas.py deleted file mode 100644 index bcb11fd..0000000 --- a/src/flexmeasures_client/s2/python_s2_protocol/common/schemas.py +++ /dev/null @@ -1,387 +0,0 @@ -# flake8: noqa - -from __future__ import annotations - -from enum import Enum -from typing import List, Optional - -from pydantic import ( - BaseModel, - ConstrainedInt, - ConstrainedStr, - Extra, - Field, - conint, - constr, -) - -uuid_constr: ConstrainedStr = constr(regex=r"[a-zA-Z0-9\-_:]{2,64}") -non_negative_int_constr: ConstrainedInt = conint(ge=0) - - -class ID(BaseModel): - class Config: - validate_assignment = True - - __root__: uuid_constr = Field( - ..., description="An identifier expressed as a UUID", title="ID" - ) - - -class Duration(BaseModel): - class Config: - validate_assignment = True - - __root__: non_negative_int_constr = Field( - ..., description="Duration in milliseconds", title="Duration" - ) - - -class NumberRange(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - start_of_range: float = Field( - ..., description="Number that defines the start of the range" - ) - end_of_range: float = Field( - ..., description="Number that defines the end of the range" - ) - - -class Commodity(Enum): - GAS = "GAS" - HEAT = "HEAT" - ELECTRICITY = "ELECTRICITY" - OIL = "OIL" - - -class CommodityQuantity(Enum): - ELECTRIC_POWER_L1 = "ELECTRIC.POWER.L1" - ELECTRIC_POWER_L2 = "ELECTRIC.POWER.L2" - ELECTRIC_POWER_L3 = "ELECTRIC.POWER.L3" - ELECTRIC_POWER_3_PHASE_SYMMETRIC = "ELECTRIC.POWER.3_PHASE_SYMMETRIC" - NATURAL_GAS_FLOW_RATE = "NATURAL_GAS.FLOW_RATE" - HYDROGEN_FLOW_RATE = "HYDROGEN.FLOW_RATE" - HEAT_TEMPERATURE = "HEAT.TEMPERATURE" - HEAT_FLOW_RATE = "HEAT.FLOW_RATE" - HEAT_THERMAL_POWER = "HEAT.THERMAL_POWER" - OIL_FLOW_RATE = "OIL.FLOW_RATE" - - -class Timer(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - id: ID = Field( - ..., - description="ID of the Timer. Must be unique in the scope of the OMBC.SystemDescription, FRBC.ActuatorDescription or DDBC.ActuatorDescription in which it is used.", - ) - diagnostic_label: Optional[str] = Field( - None, - description="Human readable name/description of the Timer. This element is only intended for diagnostic purposes and not for HMI applications.", - ) - duration: Duration = Field( - ..., - description="The time it takes for the Timer to finish after it has been started", - ) - - -class Transition(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - id: ID = Field( - ..., - description="ID of the Transition. Must be unique in the scope of the OMBC.SystemDescription, FRBC.ActuatorDescription or DDBC.ActuatorDescription in which it is used.", - ) - from_: ID = Field( - ..., - alias="from", - description="ID of the OperationMode (exact type differs per ControlType) that should be switched from.", - ) - to: ID = Field( - ..., - description="ID of the OperationMode (exact type differs per ControlType) that will be switched to.", - ) - start_timers: List[ID] = Field( - ..., - description="List of IDs of Timers that will be (re)started when this transition is initiated", - max_items=1000, - min_items=0, - ) - blocking_timers: List[ID] = Field( - ..., - description="List of IDs of Timers that block this Transition from initiating while at least one of these Timers is not yet finished", - max_items=1000, - min_items=0, - ) - transition_costs: Optional[float] = Field( - None, - description="Absolute costs for going through this Transition in the currency as described in the ResourceManagerDetails.", - ) - transition_duration: Optional[Duration] = Field( - None, - description="Indicates the time between the initiation of this Transition, and the time at which the device behaves according to the Operation Mode which is defined in the ‘to’ data element. When no value is provided it is assumed the transition duration is negligible.", - ) - abnormal_condition_only: bool = Field( - ..., - description="Indicates if this Transition may only be used during an abnormal condition (see Clause )", - ) - - -class PowerRange(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - start_of_range: float = Field( - ..., description="Power value that defines the start of the range." - ) - end_of_range: float = Field( - ..., description="Power value that defines the end of the range." - ) - commodity_quantity: CommodityQuantity = Field( - ..., description="The power quantity the values refer to" - ) - - -class EnergyManagementRole(Enum): - CEM = "CEM" - RM = "RM" - - -class InstructionStatus(Enum): - NEW = "NEW" - ACCEPTED = "ACCEPTED" - REJECTED = "REJECTED" - REVOKED = "REVOKED" - STARTED = "STARTED" - SUCCEEDED = "SUCCEEDED" - ABORTED = "ABORTED" - - -class PowerForecastValue(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - value_upper_limit: Optional[float] = Field( - None, - description="The upper boundary of the range with 100\xa0% certainty the power value is in it", - ) - value_upper_95PPR: Optional[float] = Field( - None, - description="The upper boundary of the range with 95\xa0% certainty the power value is in it", - ) - value_upper_68PPR: Optional[float] = Field( - None, - description="The upper boundary of the range with 68\xa0% certainty the power value is in it", - ) - value_expected: float = Field(..., description="The expected power value.") - value_lower_68PPR: Optional[float] = Field( - None, - description="The lower boundary of the range with 68\xa0% certainty the power value is in it", - ) - value_lower_95PPR: Optional[float] = Field( - None, - description="The lower boundary of the range with 95\xa0% certainty the power value is in it", - ) - value_lower_limit: Optional[float] = Field( - None, - description="The lower boundary of the range with 100\xa0% certainty the power value is in it", - ) - commodity_quantity: CommodityQuantity = Field( - ..., description="The power quantity the value refers to" - ) - - -class PowerForecastElement(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - duration: Duration = Field(..., description="Duration of the PowerForecastElement") - power_values: List[PowerForecastValue] = Field( - ..., - description="The values of power that are expected for the given period of time. There shall be at least one PowerForecastValue, and at most one PowerForecastValue per CommodityQuantity.", - max_items=10, - min_items=1, - ) - - -class PowerValue(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - commodity_quantity: CommodityQuantity = Field( - ..., description="The power quantity the value refers to" - ) - value: float = Field( - ..., - description="Power value expressed in the unit associated with the CommodityQuantity", - ) - - -class ReceptionStatusValues(Enum): - INVALID_DATA = "INVALID_DATA" - INVALID_MESSAGE = "INVALID_MESSAGE" - INVALID_CONTENT = "INVALID_CONTENT" - TEMPORARY_ERROR = "TEMPORARY_ERROR" - PERMANENT_ERROR = "PERMANENT_ERROR" - OK = "OK" - - -class ControlType(Enum): - POWER_ENVELOPE_BASED_CONTROL = "POWER_ENVELOPE_BASED_CONTROL" - POWER_PROFILE_BASED_CONTROL = "POWER_PROFILE_BASED_CONTROL" - OPERATION_MODE_BASED_CONTROL = "OPERATION_MODE_BASED_CONTROL" - FILL_RATE_BASED_CONTROL = "FILL_RATE_BASED_CONTROL" - DEMAND_DRIVEN_BASED_CONTROL = "DEMAND_DRIVEN_BASED_CONTROL" - NOT_CONTROLABLE = "NOT_CONTROLABLE" - NO_SELECTION = "NO_SELECTION" - - -class Currency(Enum): - AED = "AED" - ANG = "ANG" - AUD = "AUD" - CHE = "CHE" - CHF = "CHF" - CHW = "CHW" - EUR = "EUR" - GBP = "GBP" - LBP = "LBP" - LKR = "LKR" - LRD = "LRD" - LSL = "LSL" - LYD = "LYD" - MAD = "MAD" - MDL = "MDL" - MGA = "MGA" - MKD = "MKD" - MMK = "MMK" - MNT = "MNT" - MOP = "MOP" - MRO = "MRO" - MUR = "MUR" - MVR = "MVR" - MWK = "MWK" - MXN = "MXN" - MXV = "MXV" - MYR = "MYR" - MZN = "MZN" - NAD = "NAD" - NGN = "NGN" - NIO = "NIO" - NOK = "NOK" - NPR = "NPR" - NZD = "NZD" - OMR = "OMR" - PAB = "PAB" - PEN = "PEN" - PGK = "PGK" - PHP = "PHP" - PKR = "PKR" - PLN = "PLN" - PYG = "PYG" - QAR = "QAR" - RON = "RON" - RSD = "RSD" - RUB = "RUB" - RWF = "RWF" - SAR = "SAR" - SBD = "SBD" - SCR = "SCR" - SDG = "SDG" - SEK = "SEK" - SGD = "SGD" - SHP = "SHP" - SLL = "SLL" - SOS = "SOS" - SRD = "SRD" - SSP = "SSP" - STD = "STD" - SYP = "SYP" - SZL = "SZL" - THB = "THB" - TJS = "TJS" - TMT = "TMT" - TND = "TND" - TOP = "TOP" - TRY = "TRY" - TTD = "TTD" - TWD = "TWD" - TZS = "TZS" - UAH = "UAH" - UGX = "UGX" - USD = "USD" - USN = "USN" - UYI = "UYI" - UYU = "UYU" - UZS = "UZS" - VEF = "VEF" - VND = "VND" - VUV = "VUV" - WST = "WST" - XAG = "XAG" - XAU = "XAU" - XBA = "XBA" - XBB = "XBB" - XBC = "XBC" - XBD = "XBD" - XCD = "XCD" - XOF = "XOF" - XPD = "XPD" - XPF = "XPF" - XPT = "XPT" - XSU = "XSU" - XTS = "XTS" - XUA = "XUA" - XXX = "XXX" - YER = "YER" - ZAR = "ZAR" - ZMW = "ZMW" - ZWL = "ZWL" - - -class RoleType(Enum): - ENERGY_PRODUCER = "ENERGY_PRODUCER" - ENERGY_CONSUMER = "ENERGY_CONSUMER" - ENERGY_STORAGE = "ENERGY_STORAGE" - - -class Role(BaseModel): - class Config: - extra = Extra.forbid - validate_assignment = True - - role: RoleType = Field( - ..., description="Role type of the Resource Manager for the given commodity" - ) - commodity: Commodity = Field(..., description="Commodity the role refers to.") - - -class RevokableObjects(Enum): - PEBC_PowerConstraints = "PEBC.PowerConstraints" - PEBC_EnergyConstraint = "PEBC.EnergyConstraint" - PEBC_Instruction = "PEBC.Instruction" - PPBC_PowerProfileDefinition = "PPBC.PowerProfileDefinition" - PPBC_ScheduleInstruction = "PPBC.ScheduleInstruction" - PPBC_StartInterruptionInstruction = "PPBC.StartInterruptionInstruction" - PPBC_EndInterruptionInstruction = "PPBC.EndInterruptionInstruction" - OMBC_SystemDescription = "OMBC.SystemDescription" - OMBC_Instruction = "OMBC.Instruction" - FRBC_SystemDescription = "FRBC.SystemDescription" - FRBC_Instruction = "FRBC.Instruction" - DDBC_SystemDescription = "DDBC.SystemDescription" - DDBC_Instruction = "DDBC.Instruction" - - -class SessionRequestType(Enum): - RECONNECT = "RECONNECT" - TERMINATE = "TERMINATE" diff --git a/src/flexmeasures_client/s2/utils.py b/src/flexmeasures_client/s2/utils.py index f71f738..42f0347 100644 --- a/src/flexmeasures_client/s2/utils.py +++ b/src/flexmeasures_client/s2/utils.py @@ -5,11 +5,7 @@ from uuid import uuid4 import pydantic - -from flexmeasures_client.s2.python_s2_protocol.common.messages import ReceptionStatus -from flexmeasures_client.s2.python_s2_protocol.common.schemas import ( - ReceptionStatusValues, -) +from s2python.common import ReceptionStatus, ReceptionStatusValues KT = TypeVar("KT") VT = TypeVar("VT") @@ -59,9 +55,9 @@ def get_message_id(message: pydantic.BaseModel) -> str | None: ReceptionStatus. """ if hasattr(message, "message_id"): - return message.message_id.__root__ + return str(message.message_id) elif hasattr(message, "subject_message_id"): - return message.subject_message_id.__root__ + return str(message.subject_message_id) return None @@ -74,5 +70,5 @@ def get_reception_status( `subject_message`. By default, the status ReceptionStatusValues.OK is sent. """ return ReceptionStatus( - subject_message_id=subject_message.message_id.__root__, status=status + subject_message_id=str(subject_message.message_id), status=status ) diff --git a/tests/test_frbc_utils.py b/tests/test_frbc_utils.py index e31e4dd..34d5fa3 100644 --- a/tests/test_frbc_utils.py +++ b/tests/test_frbc_utils.py @@ -3,20 +3,13 @@ import math import pytest +from s2python.common import CommodityQuantity, NumberRange, PowerRange +from s2python.frbc import FRBCOperationMode, FRBCOperationModeElement from flexmeasures_client.s2.control_types.FRBC.utils import ( compute_factor, get_unique_id, ) -from flexmeasures_client.s2.python_s2_protocol.common.schemas import ( - CommodityQuantity, - NumberRange, - PowerRange, -) -from flexmeasures_client.s2.python_s2_protocol.FRBC.schemas import ( - FRBCOperationMode, - FRBCOperationModeElement, -) @pytest.mark.parametrize( diff --git a/tests/test_s2_coordinator.py b/tests/test_s2_coordinator.py index 8f9133d..9903e4a 100644 --- a/tests/test_s2_coordinator.py +++ b/tests/test_s2_coordinator.py @@ -3,35 +3,31 @@ from datetime import datetime import pytest - -from flexmeasures_client.s2.cem import CEM -from flexmeasures_client.s2.control_types.FRBC import FRBCTest -from flexmeasures_client.s2.python_s2_protocol.common.messages import ( - EnergyManagementRole, - Handshake, - ReceptionStatus, - ReceptionStatusValues, - ResourceManagerDetails, -) -from flexmeasures_client.s2.python_s2_protocol.common.schemas import ( +from s2python.common import ( Commodity, CommodityQuantity, ControlType, Duration, + EnergyManagementRole, + Handshake, NumberRange, PowerRange, + ReceptionStatus, + ReceptionStatusValues, + ResourceManagerDetails, Role, RoleType, ) -from flexmeasures_client.s2.python_s2_protocol.FRBC.messages import ( - FRBCSystemDescription, -) -from flexmeasures_client.s2.python_s2_protocol.FRBC.schemas import ( +from s2python.frbc import ( FRBCActuatorDescription, FRBCOperationMode, FRBCOperationModeElement, FRBCStorageDescription, + FRBCSystemDescription, ) + +from flexmeasures_client.s2.cem import CEM +from flexmeasures_client.s2.control_types.FRBC import FRBCTest from flexmeasures_client.s2.utils import get_unique_id @@ -94,7 +90,7 @@ async def test_cem(): # TODO: move into different test functions assert ( cem._resource_manager_details == resource_manager_details_message ), "CEM should store the resource_manager_details" - assert cem._control_type == ControlType.NO_SELECTION, ( + assert cem.control_type == ControlType.NO_SELECTION, ( "CEM control type should switch to ControlType.NO_SELECTION," "independently of the original type" ) @@ -106,7 +102,7 @@ async def test_cem(): # TODO: move into different test functions await cem.activate_control_type(ControlType.FILL_RATE_BASED_CONTROL) message = await cem.get_message() - assert cem._control_type == ControlType.NO_SELECTION, ( + assert cem.control_type == ControlType.NO_SELECTION, ( "the control type should still be NO_SELECTION (rather than FRBC)," " because the RM has not yet confirmed FRBC activation" ) @@ -118,7 +114,7 @@ async def test_cem(): # TODO: move into different test functions await cem.handle_message(response) assert ( - cem._control_type == ControlType.FILL_RATE_BASED_CONTROL + cem.control_type == ControlType.FILL_RATE_BASED_CONTROL ), "after a positive ResponseStatus, the status changes from NO_SELECTION to FRBC" ######## @@ -172,7 +168,7 @@ async def test_cem(): # TODO: move into different test functions assert ( cem._control_types_handlers[ ControlType.FILL_RATE_BASED_CONTROL - ]._system_description_history[system_description_message.message_id.__root__] + ]._system_description_history[str(system_description_message.message_id)] == system_description_message ), ( "the FRBC.SystemDescription message should be stored"