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
6 changes: 6 additions & 0 deletions docs/HEMS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,9 @@ In the third terminal, run the client script using the `/examples/HEMS` folder a

cd examples/HEMS
python3 HEMS_setup.py

.. note::
Report generation (see :ref:`hems-tutorial` note above) shells out to a ``flexmeasures`` CLI process, which by default is expected on ``PATH`` and configured against the same database as the server. If your FlexMeasures server runs elsewhere (e.g. inside a Docker Compose service), point report generation at it instead via two environment variables:

- ``FLEXMEASURES_CLI_CMD``: the command used to invoke the CLI, e.g. ``"docker compose exec -T server flexmeasures"``.
- ``FLEXMEASURES_CLI_CONFIG_DIR``: the directory the CLI process sees the ``examples/HEMS/configs/`` files at, if different from their local path (e.g. because that directory is bind-mounted into a container at a different path).
11 changes: 8 additions & 3 deletions examples/HEMS/reporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
pv_name,
)
from utils.asset_utils import find_sensors_by_asset
from utils.reporter_utils import fill_reporter_params, run_report_cmd
from utils.reporter_utils import (
cli_command_prefix,
fill_reporter_params,
run_report_cmd,
)

from flexmeasures_client import FlexMeasuresClient

Expand All @@ -22,8 +26,9 @@ async def create_reports(
"""Generate reports using FlexMeasures CLI."""
print("Generating reports...")

# Check if flexmeasures CLI is available
check_cmd = ["which", "flexmeasures"]
# Check if the configured FlexMeasures CLI command is available
# (only meaningful to check the first token, e.g. "flexmeasures" or "docker")
check_cmd = ["which", cli_command_prefix()[0]]
check_result = subprocess.run(check_cmd, capture_output=True, text=True)

if check_result.returncode != 0:
Expand Down
43 changes: 40 additions & 3 deletions examples/HEMS/utils/reporter_utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
import json
import os
import shlex
import subprocess
from pathlib import Path

BASE_DIR = Path(__file__).parent.parent


def cli_command_prefix() -> list[str]:
"""
The command used to invoke the FlexMeasures CLI, split into argv tokens.

Defaults to a plain ``flexmeasures`` on PATH, which requires the CLI to be
installed locally and configured to talk to the same database as the
server the client is scripting against (see the "Running the Tutorial"
instructions in docs/HEMS.rst).

Override via the ``FLEXMEASURES_CLI_CMD`` environment variable to run the
CLI elsewhere, e.g. inside a Docker Compose service:

FLEXMEASURES_CLI_CMD="docker compose -f /path/to/docker-compose.yml exec -T server flexmeasures"
"""
return shlex.split(os.environ.get("FLEXMEASURES_CLI_CMD", "flexmeasures"))


def _cli_config_path(local_path: str) -> str:
"""
Translate a local config/parameter file path to the path the CLI process
will see it at, when that process runs somewhere other than this host
(e.g. inside a container with the ``configs/`` directory bind-mounted
elsewhere). Controlled via ``FLEXMEASURES_CLI_CONFIG_DIR``; a no-op if unset.
"""
remote_dir = os.environ.get("FLEXMEASURES_CLI_CONFIG_DIR")
if not remote_dir:
return local_path
return os.path.join(remote_dir, os.path.basename(local_path))


def fill_reporter_params(
input_sensors: list[dict],
output_sensors: list[dict] | dict,
Expand Down Expand Up @@ -75,16 +106,22 @@ def run_report_cmd(reporter_map: dict, start: str, end: str) -> bool:
start (str): Start time of the report period (ISO 8601 format).
end (str): End time of the report period (ISO 8601 format).
"""
config_path = os.path.join(
BASE_DIR, f"configs/{reporter_map['name']}_reporter_config.json"
)
param_path = os.path.join(
BASE_DIR, f"configs/{reporter_map['name']}_reporter_param.json"
)
cmd = [
"flexmeasures",
*cli_command_prefix(),
"add",
"report",
"--reporter",
reporter_map["reporter"],
"--config",
os.path.join(BASE_DIR, f"configs/{reporter_map['name']}_reporter_config.json"),
_cli_config_path(config_path),
"--parameters",
os.path.join(BASE_DIR, f"configs/{reporter_map['name']}_reporter_param.json"),
_cli_config_path(param_path),
"--start",
start,
"--end",
Expand Down
9 changes: 6 additions & 3 deletions src/flexmeasures_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,8 +604,10 @@ async def _post_sensor_data_file(
allow_redirects=False,
)

# Check response status
if response.status != 200:
# Check response status (any 2xx is success, e.g. 202 Accepted
# for asynchronous processing, matching check_for_status()'s
# convention used elsewhere in this client)
if not (200 <= response.status < 300):
try:
error_data = await response.json()
error_message = f"Request failed with status code {response.status}: {error_data}"
Expand All @@ -619,7 +621,8 @@ async def _post_sensor_data_file(
# Parse response
response_data = await response.json()
self.logger.info(
f"File uploaded successfully: {os.path.basename(file_path)}"
f"File uploaded successfully: {os.path.basename(file_path)} "
f"(status {response.status})"
)
return response_data, response.status

Expand Down
30 changes: 30 additions & 0 deletions tests/client/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,36 @@ async def test_post_sensor_data_with_file():
os.unlink(csv_path)


@pytest.mark.asyncio
async def test_post_sensor_data_with_file_accepted():
"""202 Accepted (asynchronous processing) is treated as success, not an error."""
csv_path = "/tmp/test_sensor_data_accepted.csv"
with open(csv_path, "w") as f:
f.write("datetime,value\n2023-01-01T00:00+00:00,1.0\n")

try:
with aioresponses() as m:
client = FlexMeasuresClient(email="test@test.test", password="test")
client.access_token = "test-token"
m.post(
"http://localhost:5000/api/v3_0/sensors/1/data/upload",
status=202,
payload={
"job_id": "test-job-id",
"message": "Sensor data has been accepted for processing.",
"status": "ACCEPTED",
},
)
response_data, status = await client.post_sensor_data(
sensor_id=1,
file_path=csv_path,
)
assert status == 202
await client.close()
finally:
os.unlink(csv_path)


@pytest.mark.asyncio
async def test_post_sensor_data_json_with_prior():
"""prior parameter is included in payload."""
Expand Down
Loading