Skip to content
Draft
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 README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::
Expand Down
35 changes: 35 additions & 0 deletions src/flexmeasures_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand Down Expand Up @@ -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.

Expand Down
9 changes: 5 additions & 4 deletions src/flexmeasures_client/s2/cem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions src/flexmeasures_client/s2/control_types/PPBC/__init__.py
Original file line number Diff line number Diff line change
@@ -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
52 changes: 52 additions & 0 deletions src/flexmeasures_client/s2/control_types/PPBC/ppbc_simple.py
Original file line number Diff line number Diff line change
@@ -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,
)
Empty file.
4 changes: 2 additions & 2 deletions src/flexmeasures_client/s2/script/demo_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)

Expand Down
45 changes: 31 additions & 14 deletions src/flexmeasures_client/s2/script/websockets_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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,
Expand Down
10 changes: 7 additions & 3 deletions src/flexmeasures_client/s2/script/websockets_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_s2_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading