diff --git a/README.rst b/README.rst index 2d4fd955..3785061d 100644 --- a/README.rst +++ b/README.rst @@ -141,7 +141,7 @@ Making Changes & Contributing Install the project locally (in a virtual environment of your choice):: - pip install -e + pip install -e . Running tests locally is crucial as well. Staying close to the CI workflow:: diff --git a/src/flexmeasures_client/client.py b/src/flexmeasures_client/client.py index 0360d38e..c2b6dd0e 100644 --- a/src/flexmeasures_client/client.py +++ b/src/flexmeasures_client/client.py @@ -269,6 +269,7 @@ async def post_measurements( prior: str | None = None, ): """ + This function raises a ContentTypeError when the response is not a dictionary. Post sensor data for the given time range. This function raises a ValueError when an unhandled status code is returned """ @@ -321,6 +322,40 @@ async def get_schedule( ) return schedule + async def get_account(self) -> dict | None: + """Get the organisation account of the current user. + + :returns: organisation account as dictionary, for example: + { + "id": 1, + "name": "Positive Design", + } + """ + + users, status = await self.request(uri="users", method="GET") + check_for_status(status, 200) + + account_id = None + for user in users: + if user["email"] == self.email: + account_id = user["account_id"] + if account_id is None: + raise NotImplementedError( + "User does not seem to belong to account, which should not be possible." + ) + # Force account to be a dictionary + + account, status = await self.request( + uri=f"accounts/{account_id}", + method="GET", + ) + if not isinstance(account, dict): + raise ContentTypeError( + f"Expected an account dictionary! but got {type(account)}", + ) + check_for_status(status, 200) + return account + async def get_assets(self) -> list[dict]: """Get all the assets available to the current user. diff --git a/src/flexmeasures_client/s2/cem.py b/src/flexmeasures_client/s2/cem.py index c64bb828..56384dea 100644 --- a/src/flexmeasures_client/s2/cem.py +++ b/src/flexmeasures_client/s2/cem.py @@ -42,7 +42,10 @@ class CEM(Handler): _sending_queue: Queue[pydantic.BaseModel] def __init__( - self, fm_client: FlexMeasuresClient, logger: Logger | None = None + self, + sensor_id: int, + fm_client: FlexMeasuresClient, + logger: Logger | None = None, ) -> None: """ Customer Energy Manager (CEM) @@ -223,9 +226,7 @@ def handle_handshake(self, message: Handshake): def handle_resource_manager_details(self, message: ResourceManagerDetails): self._resource_manager_details = message - if ( - not self._control_type - ): # initializing. TODO: check if sending resource_manager_details + if not self._control_type: # TODO: check if sending resource_manager_details # resets control type self._control_type = ControlType.NO_SELECTION diff --git a/src/flexmeasures_client/s2/control_types/PPBC/__init__.py b/src/flexmeasures_client/s2/control_types/PPBC/__init__.py new file mode 100644 index 00000000..2c989303 --- /dev/null +++ b/src/flexmeasures_client/s2/control_types/PPBC/__init__.py @@ -0,0 +1,4 @@ +# from s2python.ppbc import PPBCScheduleInstruction + +# 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/PPBC/ppbc_simple.py b/src/flexmeasures_client/s2/control_types/PPBC/ppbc_simple.py new file mode 100644 index 00000000..2e98863c --- /dev/null +++ b/src/flexmeasures_client/s2/control_types/PPBC/ppbc_simple.py @@ -0,0 +1,52 @@ +""" +This module contains the PPBC simple control type. +""" + +from datetime import datetime, timedelta + +import pytz +from s2python.ppbc import PPBCScheduleInstruction + +from flexmeasures_client.s2.control_types.PPBC import PPBC + + +class PPBCSimple(PPBC): + _power_sensor_id: int + _price_sensor_id: int + _schedule_duration: timedelta + _valid_from_shift: timedelta + + def __init__( + self, + power_sensor_id: int, + price_sensor_id: int, + timezone: str = "UTC", + schedule_duration: timedelta = timedelta(hours=12), + max_size: int = 100, + valid_from_shift: timedelta = timedelta(days=1), + ) -> None: + super().__init__(max_size) + self._power_sensor_id = power_sensor_id + self._price_sensor_id = price_sensor_id + self._schedule_duration = schedule_duration + self._timezone = pytz.timezone(timezone) + + # delay the start of the schedule from the time `valid_from` + # of the PPBC.SystemDescritption. + self._valid_from_shift = valid_from_shift + + def now(self): + return self._timezone.localize(datetime.now()) + + async def send_schedule_instruction(self, instruction: PPBCScheduleInstruction): + await self._fm_client.post_schedule( + self._power_sensor_id, + start=self.now(), + values=instruction.power_values, + unit="MW", + duration=self._schedule_duration, + price_sensor_id=self._price_sensor_id, + price_values=instruction.price_values, + price_unit="EUR/MWh", + valid_from=self.now() + self._valid_from_shift, + ) diff --git a/src/flexmeasures_client/s2/control_types/PPBC/utils.py b/src/flexmeasures_client/s2/control_types/PPBC/utils.py new file mode 100644 index 00000000..e69de29b diff --git a/src/flexmeasures_client/s2/script/demo_setup.py b/src/flexmeasures_client/s2/script/demo_setup.py index 3524c037..6c3fb430 100644 --- a/src/flexmeasures_client/s2/script/demo_setup.py +++ b/src/flexmeasures_client/s2/script/demo_setup.py @@ -3,8 +3,8 @@ from flexmeasures_client.client import FlexMeasuresClient client = FlexMeasuresClient( - email="admin@admin.com", - password="admin", + email="toy-user@flexmeasures.io", + password="toy-password", host="localhost:5000", ) diff --git a/src/flexmeasures_client/s2/script/websockets_client.py b/src/flexmeasures_client/s2/script/websockets_client.py index cf3f0cc3..9773cae3 100644 --- a/src/flexmeasures_client/s2/script/websockets_client.py +++ b/src/flexmeasures_client/s2/script/websockets_client.py @@ -3,34 +3,51 @@ import aiohttp import pytz - -from flexmeasures_client.s2.python_s2_protocol.common.messages import ( - 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 ( - FRBCStorageStatus, - FRBCSystemDescription, -) -from flexmeasures_client.s2.python_s2_protocol.FRBC.schemas import ( +from s2python.frbc import ( FRBCActuatorDescription, FRBCOperationMode, FRBCOperationModeElement, FRBCStorageDescription, + FRBCStorageStatus, + FRBCSystemDescription, ) + +# from flexmeasures_client.s2.python_s2_protocol.common.schemas import ( +# Commodity, +# CommodityQuantity, +# ControlType, +# Duration, +# EnergyManagementRole, +# NumberRange, +# PowerRange, +# Role, +# RoleType, +# ) +# from flexmeasures_client.s2.python_s2_protocol.FRBC.messages import ( +# FRBCStorageStatus, +# FRBCSystemDescription, +# ) +# from flexmeasures_client.s2.python_s2_protocol.FRBC.schemas import ( +# FRBCActuatorDescription, +# FRBCOperationMode, +# FRBCOperationModeElement, +# FRBCStorageDescription, +# ) from flexmeasures_client.s2.utils import get_unique_id @@ -59,7 +76,7 @@ async def main_s2(): roles=[ Role(role=RoleType.ENERGY_STORAGE, commodity=Commodity.ELECTRICITY) ], - instruction_processing_delay=Duration(__root__=1.0), + instruction_processing_delay=Duration(__root__=1), available_control_types=[ ControlType.FILL_RATE_BASED_CONTROL, ControlType.NO_SELECTION, diff --git a/src/flexmeasures_client/s2/script/websockets_server.py b/src/flexmeasures_client/s2/script/websockets_server.py index 8591f6a8..c65f9373 100644 --- a/src/flexmeasures_client/s2/script/websockets_server.py +++ b/src/flexmeasures_client/s2/script/websockets_server.py @@ -3,11 +3,13 @@ import aiohttp from aiohttp import web +from s2python.common import ControlType from flexmeasures_client.client import FlexMeasuresClient from flexmeasures_client.s2.cem import CEM from flexmeasures_client.s2.control_types.FRBC.frbc_simple import FRBCSimple -from flexmeasures_client.s2.python_s2_protocol.common.schemas import ControlType + +# from flexmeasures_client.s2.python_s2_protocol.common.schemas import ControlType async def rm_details_watchdog(ws, cem: CEM): @@ -71,8 +73,10 @@ async def websocket_handler(request): fm_client = FlexMeasuresClient("toy-password", "toy-user@flexmeasures.io") - cem = CEM(sensor_id=1, fm_client=fm_client) - frbc = FRBCSimple(power_sensor_id=1, price_sensor_id=2) + cem = CEM(fm_client=fm_client) + frbc = FRBCSimple( + power_sensor_id=1, price_sensor_id=2, soc_sensor_id=3, rm_discharge_sensor_id=4 + ) cem.register_control_type(frbc) # create "parallel" tasks for the message producer and consumer diff --git a/tests/test_client.py b/tests/test_client.py index d7f774db..6ffbf487 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -495,6 +495,56 @@ async def test_trigger_and_get_schedule() -> None: assert schedule["values"] == [2.15, 3, 2] +@pytest.mark.asyncio +async def test_get_account() -> None: + with aioresponses() as m: + m.get( + "http://localhost:5000/api/v3_0/users", + status=200, + payload=[ + { + "account_id": 1, + "active": True, + "email": "toy-user@flexmeasures.io", + "id": 39, + "username": "toy-user", + }, + { + "account_id": 1, + "active": True, + "email": "toy-colleague@flexmeasures.io", + "id": 40, + "username": "toy-colleague", + }, + { + "account_id": 2, + "active": True, + "email": "toy-client@flexmeasures.io", + "id": 41, + "username": "toy-client", + }, + ], + ) + m.get( + "http://localhost:5000/api/v3_0/accounts/1", + status=200, + payload={ + "id": 1, + "name": "Positive Design", + }, + ) + flexmeasures_client = FlexMeasuresClient( + host="localhost", + port=5000, + email="toy-user@flexmeasures.io", + password="toy-password", + ) + flexmeasures_client.access_token = "test-token" + account = await flexmeasures_client.get_account() + assert account["id"] == 1 + assert account["name"] == "Positive Design" + + @pytest.mark.asyncio async def test_get_sensor_data() -> None: with aioresponses() as m: diff --git a/tests/test_s2_coordinator.py b/tests/test_s2_coordinator.py index 9903e4ac..cfb1c0f0 100644 --- a/tests/test_s2_coordinator.py +++ b/tests/test_s2_coordinator.py @@ -33,7 +33,7 @@ @pytest.mark.asyncio async def test_cem(): # TODO: move into different test functions - cem = CEM(fm_client=None) + cem = CEM(sensor_id=1, fm_client=None) frbc = FRBCTest() cem.register_control_type(frbc)