diff --git a/documentation/api/change_log.rst b/documentation/api/change_log.rst index aac8e3694..eedc66724 100644 --- a/documentation/api/change_log.rst +++ b/documentation/api/change_log.rst @@ -6,11 +6,12 @@ API change log .. note:: The FlexMeasures API follows its own versioning scheme. This is also reflected in the URL, allowing developers to upgrade at their own pace. v3.0-11 | 2023-08-02 -""""""""""""""""""" +"""""""""""""""""""" - Added REST endpoint for fetching one sensor: `/sensors/` (GET) - Added REST endpoint for adding a sensor: `/sensors` (POST) - Added REST endpoint for patching a sensor: `/sensors/` (PATCH) +- Added REST endpoint for deleting a sensor: `/sensors/` (DELETE) v3.0-10 | 2023-06-12 """""""""""""""""""" diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 2149619f9..63d58f95c 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -18,7 +18,7 @@ New features * Users on FlexMeasures servers in play mode (``FLEXMEASURES_MODE = "play"``) can use the ``sensors_to_show`` attribute to show any sensor on their asset pages, rather than only sensors registered to assets in their own account or to public assets [see `PR #740 `_] * Having percentages within the [0, 100] domain is such a common use case that we now always include it in sensor charts with % units, making it easier to read off individual charts and also to compare across charts [see `PR #739 `_] * DataSource table now allows storing arbitrary attributes as a JSON (without content validation), similar to the Sensor and GenericAsset tables [see `PR #750 `_] -* Added API endpoints `/sensors/` for fetching a single sensor, `/sensors` (POST) for adding a sensor and `/sensor/` (PATCH) for updating a sensor. [see `PR #759 `_] and [see `PR #767 `_] and [see `PR #773 `_] +* Added API endpoints `/sensors/` for fetching a single sensor, `/sensors` (POST) for adding a sensor, `/sensors/` (PATCH) for updating a sensor and `/sensors/` (DELETE) for deleting a sensor. [see `PR #759 `_] and [see `PR #767 `_] and [see `PR #773 `_] and [see `PR #784 `_] * The CLI now allows to set lists and dicts as asset & sensor attributes (formerly only single values) [see `PR #762 `_] * Add `ProcessScheduler` class to optimize the starting time of processes one of the policies developed (INFLEXIBLE, SHIFTABLE and BREAKABLE), accessible via the CLI command `flexmeasures add schedule for-process` [see `PR #729 `_ and `PR #768 `_] diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py index be1d42fe1..c9e3ecd62 100644 --- a/flexmeasures/api/v3_0/sensors.py +++ b/flexmeasures/api/v3_0/sensors.py @@ -1,5 +1,6 @@ from __future__ import annotations + from datetime import datetime, timedelta from flask import current_app @@ -31,7 +32,7 @@ from flexmeasures.data import db from flexmeasures.data.models.user import Account from flexmeasures.data.models.generic_assets import GenericAsset -from flexmeasures.data.models.time_series import Sensor +from flexmeasures.data.models.time_series import Sensor, TimedBelief from flexmeasures.data.queries.utils import simplify_index from flexmeasures.data.schemas.sensors import SensorSchema, SensorIdField from flexmeasures.data.schemas.times import AwareDateTimeField, PlanningDurationField @@ -649,3 +650,33 @@ def patch(self, sensor_data: dict, id: int, db_sensor: Sensor): db.session.add(db_sensor) db.session.commit() return sensor_schema.dump(db_sensor), 200 + + @route("/", methods=["DELETE"]) + @use_kwargs({"sensor": SensorIdField(data_key="id")}, location="path") + @permission_required_for_context("delete", ctx_arg_name="sensor") + @as_json + def delete(self, id: int, sensor: Sensor): + """Delete a sensor given its identifier. + + .. :quickref: Sensor; Delete a sensor + + This endpoint deletes an existing sensor, as well as all measurements recorded for it. + + :reqheader Authorization: The authentication token + :reqheader Content-Type: application/json + :resheader Content-Type: application/json + :status 204: DELETED + :status 400: INVALID_REQUEST, REQUIRED_INFO_MISSING, UNEXPECTED_PARAMS + :status 401: UNAUTHORIZED + :status 403: INVALID_SENDER + :status 422: UNPROCESSABLE_ENTITY + """ + + """Delete time series data.""" + TimedBelief.query.filter(TimedBelief.sensor_id == sensor.id).delete() + + sensor_name = sensor.name + db.session.delete(sensor) + db.session.commit() + current_app.logger.info("Deleted sensor '%s'." % sensor_name) + return {}, 204 diff --git a/flexmeasures/api/v3_0/tests/test_sensors_api.py b/flexmeasures/api/v3_0/tests/test_sensors_api.py index 7244be33e..03276b0ee 100644 --- a/flexmeasures/api/v3_0/tests/test_sensors_api.py +++ b/flexmeasures/api/v3_0/tests/test_sensors_api.py @@ -3,7 +3,7 @@ import pytest from flask import url_for - +from flexmeasures.data.models.time_series import TimedBelief from flexmeasures import Sensor from flexmeasures.api.tests.utils import get_auth_token from flexmeasures.api.v3_0.tests.utils import get_sensor_post_data @@ -172,3 +172,28 @@ def test_patch_sensor_from_unrelated_account(client, setup_api_test_data): assert response.status_code == 403 assert response.json["status"] == "INVALID_SENDER" + + +def test_delete_a_sensor(client, setup_api_test_data): + + existing_sensor_id = setup_api_test_data["some temperature sensor"].id + headers = make_headers_for("test_admin_user@seita.nl", client) + sensor_data = TimedBelief.query.filter( + TimedBelief.sensor_id == existing_sensor_id + ).all() + sensor_count = len(Sensor.query.all()) + + assert isinstance(sensor_data[0].event_value, float) + + delete_sensor_response = client.delete( + url_for("SensorAPI:delete", id=existing_sensor_id), + headers=headers, + ) + assert delete_sensor_response.status_code == 204 + deleted_sensor = Sensor.query.filter_by(id=existing_sensor_id).one_or_none() + assert deleted_sensor is None + assert ( + TimedBelief.query.filter(TimedBelief.sensor_id == existing_sensor_id).all() + == [] + ) + assert len(Sensor.query.all()) == sensor_count - 1