Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 389 publish sensor data api documentation #390

Merged
merged 99 commits into from Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
f6f1b91
Move sensor data API from dev to v2 and use flask-classful
Flix6x Mar 7, 2022
f92d182
Publish dev API
Flix6x Mar 8, 2022
84f4169
Add quickrefs
Flix6x Mar 8, 2022
9eb7299
Add type annotations
Flix6x Mar 8, 2022
ea7a77d
Refactor use of auth_required in SensorAPI
Flix6x Mar 8, 2022
9d2f329
Add type annotations
Flix6x Mar 8, 2022
25ba2de
Add more quickrefs
Flix6x Mar 8, 2022
0126169
Indentation
Flix6x Mar 8, 2022
7ce3f14
valid JSON, rather than a python dict
Flix6x Mar 8, 2022
cb205ba
Move import to its preferred position
Flix6x Mar 8, 2022
841f1d3
Rename and add type annotation
Flix6x Mar 8, 2022
f126561
Allow AssetIdField and SensorIdField to be used for API requests, too…
Flix6x Mar 8, 2022
2e3bf11
Update tutorial for posting data
Flix6x Mar 8, 2022
e6db62f
Implement [GET] sensorData
Flix6x Mar 8, 2022
de46436
Add example GET request
Flix6x Mar 8, 2022
2ab771b
Update docstring for unit conversion
Flix6x Mar 8, 2022
1a7414e
Respect the passed horizon and prior fields
Flix6x Mar 8, 2022
70e3793
Convert NaN to null (otherwise invalid JSON)
Flix6x Mar 8, 2022
2ef234f
Pass event resolution to enable additional unit conversions
Flix6x Mar 8, 2022
bc5ffbe
Smarter unit conversion for BeliefsSeries
Flix6x Mar 8, 2022
3e85a5e
Revert to auth_token_required instead of login_required
Flix6x Mar 8, 2022
5738a29
Deserialize to BeliefsDataFrame using post_load decorator, and run th…
Flix6x Mar 8, 2022
9cd8f9b
Move serialization logic to schema
Flix6x Mar 8, 2022
c76766e
Fix tutorial for posting data
Flix6x Mar 8, 2022
271c435
Introduce util function to check for a valid price unit
Flix6x Mar 8, 2022
56a5f25
Comment out tutorial sections on posting data for multiple connection…
Flix6x Mar 8, 2022
8a14c64
Update tutorial for posting data: change connection to sensor and sto…
Flix6x Mar 8, 2022
e878f5c
Add validation based on POST message type
Flix6x Mar 8, 2022
dfa64b8
Publish documentation for SensorDataAPI
Flix6x Mar 8, 2022
7de8b6f
Add validation based on GET message type
Flix6x Mar 8, 2022
c0f0bee
mypy
Flix6x Mar 8, 2022
29d3381
Update getting-started.rst
Flix6x Mar 8, 2022
852bee8
Undoc old endpoints
Flix6x Mar 8, 2022
eef5d42
Rename with_appcontext_if_needed
Flix6x Mar 10, 2022
83b774e
refactor auth logic - separate checking access from the permission_re…
nhoening Mar 10, 2022
52b3ba9
add modern auth for sensor data get & post, remove checking account r…
nhoening Mar 12, 2022
9c0695e
more freedom to create inside accounts: allow all account members to …
nhoening Mar 12, 2022
bc9a100
use a user from the same account as the sensor for sensor data tests
nhoening Mar 12, 2022
2f31a17
adapt asset tests w.r.t. to a change in dev api conftest
nhoening Mar 12, 2022
352ae60
Remove unneeded import
Flix6x Mar 11, 2022
210ecbc
GET endpoints work for logged-in users when tried in the browser with…
Flix6x Mar 11, 2022
b76ebdb
Add docstring
Flix6x Mar 11, 2022
9b3f281
Make type a required field
Flix6x Mar 11, 2022
7b67a1f
Move @route to top
Flix6x Mar 14, 2022
2037394
SensorDataDescriptionSchema doesn't need the `type` field
Flix6x Mar 14, 2022
b17a9b8
Revert "Undoc old endpoints"
Flix6x Mar 14, 2022
4e804ba
Move sensor data API documentation to v3
Flix6x Mar 14, 2022
24ffc1a
Move sensor data API to v3
Flix6x Mar 14, 2022
77ffdaf
Move user API to class for v3
Flix6x Mar 14, 2022
daa43c6
Move user UI to v3
Flix6x Mar 14, 2022
3abb649
Update user API tests
Flix6x Mar 14, 2022
c4ea13b
Move user API tests to v3 module
Flix6x Mar 14, 2022
41371c7
Separate view for user index
Flix6x Mar 14, 2022
d2a2e5f
Fix test
Flix6x Mar 14, 2022
0c2d199
Update documentation tutorials to v3
Flix6x Mar 14, 2022
482cde9
No trailing slash
Flix6x Mar 14, 2022
820521e
Make `type` field in v3
Flix6x Mar 14, 2022
0fc14b6
Add v3 to API index
Flix6x Mar 14, 2022
70a9406
Update introduction
Flix6x Mar 14, 2022
281af3b
Fix comment about where to request auth token
Flix6x Mar 14, 2022
d14d85e
flake8
Flix6x Mar 14, 2022
89e8dff
black
Flix6x Mar 14, 2022
2f2cee0
Revert some documentation changes that are ahead of code changes
Flix6x Mar 14, 2022
abfc41a
Use one common base route
Flix6x Mar 14, 2022
3fec582
Prefer plural base route
Flix6x Mar 14, 2022
80ed32d
different shade of black
Flix6x Mar 14, 2022
a26fad1
typos
Flix6x Mar 15, 2022
6f34010
Transition SensorDataAPI:post to SensorAPI:post_data and SensorDataAP…
Flix6x Mar 15, 2022
0f2a8f0
Rename module
Flix6x Mar 15, 2022
bf277b0
Update documentation for moving from SensorDataAPI:post to SensorAPI:…
Flix6x Mar 15, 2022
dcb46f3
Update introduction
Flix6x Mar 15, 2022
d392423
API changelog entry
Flix6x Mar 15, 2022
64e1100
Implement /sensors [GET] to replace getConnection
Flix6x Mar 15, 2022
c06d768
Complete docstring
Flix6x Mar 15, 2022
257b5b5
Add extra tips to API changelog on how to upgrade from v2 to v3
Flix6x Mar 15, 2022
b889992
Fix check for belief timing against message type
Flix6x Mar 15, 2022
6037d60
No need to use implementations folder anymore
Flix6x Mar 16, 2022
16130a1
Contain endpoint logic within SensorAPI class
Flix6x Mar 16, 2022
1b6394f
Contain endpoint logic within UserAPI class
Flix6x Mar 16, 2022
c16a3bf
Fix mock email and type annotations
Flix6x Mar 17, 2022
d4e99db
Initialize schemas within SensorAPI and UserAPI class
Flix6x Mar 17, 2022
cdb7704
Typos
Flix6x Mar 17, 2022
4c982bb
Fix 404 error message
Flix6x Mar 17, 2022
ea1e1b6
Remove unused variable declaration
Flix6x Mar 17, 2022
38d3bc1
Clarify use of headers in test
Flix6x Mar 17, 2022
9d98f71
Concerning patching user ids and user account ids, set dump_only for …
Flix6x Mar 17, 2022
ca3154f
Add test cases for unexpected fields
Flix6x Mar 18, 2022
71add34
Also move patch endpoint logic into UserAPI class; this one was a bit…
Flix6x Mar 18, 2022
1f07045
Move test utils from dev to v3_0
Flix6x Mar 18, 2022
d8b2790
Finish moving sensor data tests from dev to v3_0
Flix6x Mar 18, 2022
0589009
Fix asset tests in dev API by moving back some conftest logic
Flix6x Mar 18, 2022
3743032
Instantiate schemas outside of endpoint logic to minimize response time
Flix6x Mar 22, 2022
bf10bf3
Add mentions of http status to docstrings
Flix6x Mar 22, 2022
f159094
Undocument getService from the API introduction
Flix6x Mar 22, 2022
8572b8f
Undocument USEF roles in introduction
Flix6x Mar 22, 2022
1c013e9
Use "sensor" instead of "connection" in introduction
Flix6x Mar 22, 2022
dd7c59e
Undocument group notation in introduction
Flix6x Mar 22, 2022
c808fe3
Update API changelog for rewrite of introduction
Flix6x Mar 22, 2022
0358dfa
Update API changelog entry date
Flix6x Mar 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 8 additions & 1 deletion documentation/api/change_log.rst
Expand Up @@ -6,7 +6,7 @@ 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-0 | 2022-03-15
v3.0-0 | 2022-03-22
"""""""""""""""""""

- Added REST endpoint for listing sensors: `/sensors` (GET).
Expand All @@ -24,6 +24,13 @@ v3.0-0 | 2022-03-15
- *postWeatherData* -> use `/sensors/data` (POST) instead
- *restoreData*

- Changed the Introduction section:

- Rewrote the section on service listing for API versions to refer to the public documentation.
- Rewrote the section on entity addresses to refer to *sensors* instead of *connections*.
- Rewrote the sections on roles and sources into a combined section that refers to account roles rather than USEF roles.
- Deprecated the section on group notation.

v2.0-4 | 2022-01-04
"""""""""""""""""""

Expand Down
131 changes: 12 additions & 119 deletions documentation/api/introduction.rst
Expand Up @@ -41,7 +41,7 @@ Let's see what the ``/api`` endpoint returns:
>>> res = requests.get("https://company.flexmeasures.io/api")
>>> res.json()
{'flexmeasures_version': '0.9.0',
'message': 'For these API versions a public endpoint is available, listing its service. For example: /api/v2_0/getService and /api/v3_0/getService. An authentication token can be requested at: /api/requestAuthToken',
'message': 'For these API versions endpoints are available. An authentication token can be requested at: /api/requestAuthToken. For a list of services, see https://flexmeasures.readthedocs.io',
'status': 200,
'versions': ['v1', 'v1_1', 'v1_2', 'v1_3', 'v2_0', 'v3_0']
}
Expand All @@ -53,37 +53,7 @@ So this tells us which API versions exist. For instance, we know that the latest
https://company.flexmeasures.io/api/v3_0


Also, we can see that a list of endpoints which are available at (a version of) the FlexMeasures web service can be obtained by sending a ``getService`` request. An optional field "access" can be used to specify a user role for which to obtain only the relevant services.

**Example request**

Let's ask which endpoints are available for meter data companies (MDC):

.. code-block:: html

https://company.flexmeasures.io/api/v2_0/getService?access=MDC


**Example response**

.. code-block:: json

{
"type": "GetServiceResponse",
"version": "1.0",
"services": [
{
"name": "getMeterData",
"access": ["Aggregator", "Supplier", "MDC", "DSO", "Prosumer", "ESCo"],
"description": "Request meter reading"
},
{
"name": "postMeterData",
"access": ["MDC"],
"description": "Send meter reading"
}
]
}
Also, we can see that a list of endpoints is available on https://flexmeasures.readthedocs.io for each of these versions.

.. _api_auth:

Expand Down Expand Up @@ -131,28 +101,13 @@ which gives a response like this if the credentials are correct:
.. note:: Each access token has a limited lifetime, see :ref:`auth`.


Roles
-----

We distinguish the following roles with different access rights to the individual services. Capitalised roles are defined by USEF:

- public
- user
- admin
- Aggregator
- Supplier: an energy retailer (see :ref:`supplier`)
- Prosumer: owner of a grid connection (see :ref:`prosumer`)
- ESCo: an energy service company (see :ref:`esco`)
- MDC: a meter data company (see :ref:`mdc`)
- DSO: a distribution system operator (see :ref:`dso`)

.. _sources:

Sources
-------

Requests for data may limit the data selection by specifying a source, for example, a specific user.
USEF roles are also valid source selectors.
Account roles are also valid source selectors.
For example, to obtain data originating from either a meter data company or user 42, include the following:

.. code-block:: json
Expand All @@ -161,6 +116,8 @@ For example, to obtain data originating from either a meter data company or user
"sources": ["MDC", "42"],
}

Here, "MDC" is the name of the account role for meter data companies.

Notation
--------
All requests and responses to and from the web service should be valid JSON messages.
Expand All @@ -179,27 +136,26 @@ Throughout this document, keys are written in singular if a single value is list

The API, however, does not distinguish between singular and plural key notation.

Connections and entity addresses
Sensors and entity addresses
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gives the impression that this is something you have to know to use the API. But at this point, you don't have to know it. I suggest giving a bit more information about the context in which this info might be useful.

All in all, the whole "Notation" section in this API introduction could use a helicopter perspective on how to put the content into perspective, but that is maybe another PR.

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

A connection represents an end point of the grid, at which an electricity sensor (power meter) is located.
Connections should be identified with an entity address following the EA1 addressing scheme prescribed by USEF[1],
which is mostly taken from IETF RFC 3720 [2]:
All sensors are identified with an entity address following the EA1 addressing scheme prescribed by USEF[1],
which is mostly taken from IETF RFC 3720 [2].

This is the complete structure of an EA1 address:

.. code-block:: json

{
"connection": "ea1.{date code}.{reversed domain name}:{locally unique string}"
"sensor": "ea1.{date code}.{reversed domain name}:{locally unique string}"
}

Here is a full example for a FlexMeasures connection address:
Here is a full example for an entity address of a sensor in FlexMeasures:

.. code-block:: json

{
"connection": "ea1.2021-02.io.flexmeasures.company:fm1.73"
"sensor": "ea1.2021-02.io.flexmeasures.company:fm1.73"
}

where FlexMeasures runs at `company.flexmeasures.io` (which the current domain owner started using in February 2021), and the locally unique string uses the `fm1` scheme (see below) to identify sensor ID 73.
Expand Down Expand Up @@ -254,7 +210,7 @@ It uses the fact that all FlexMeasures sensors have unique IDs.
.. todo:: UDI events are not yet modelled in the fm1 scheme

The ``fm0`` scheme is the original scheme.
It identified different types of sensors (such as connections, weather sensors and markets) in different ways.
It identified different types of sensors (such as grid connections, weather sensors and markets) in different ways.
The ``fm0`` scheme has been deprecated for the most part and is no longer supported officially.
Only UDI events still need to be sent using the fm0 scheme.

Expand All @@ -264,69 +220,6 @@ Only UDI events still need to be sent using the fm0 scheme.
ea1.2021-01.io.flexmeasures:fm0.<owner_id>:<sensor_id>:<event_id>:<event_type>


Groups
^^^^^^

Data such as measurements, load prognoses and tariffs are usually stated per group of connections.
When the attributes "start", "duration" and "unit" are stated outside of "groups" they are inherited by each of the individual groups. For example:

.. code-block:: json

{
"groups": [
{
"connections": [
"ea1.2021-02.io.flexmeasures.company:fm1.71",
"ea1.2021-02.io.flexmeasures.company:fm1.72"
],
"values": [
306.66,
306.66,
0,
0,
306.66,
306.66
]
},
{
"connection": "ea1.2021-02.io.flexmeasures.company:fm1.73"
"values": [
306.66,
0,
0,
0,
306.66,
306.66
]
}
],
"start": "2016-05-01T12:45:00Z",
"duration": "PT1H30M",
"unit": "MW"
}

In case of a single group of connections, the message may be flattened to:

.. code-block:: json

{
"connections": [
"ea1.2021-02.io.flexmeasures.company:fm1.71",
"ea1.2021-02.io.flexmeasures.company:fm1.72"
],
"values": [
306.66,
306.66,
0,
0,
306.66,
306.66
],
"start": "2016-05-01T12:45:00Z",
"duration": "PT1H30M",
"unit": "MW"
}

Timeseries
^^^^^^^^^^

Expand Down
30 changes: 27 additions & 3 deletions flexmeasures/api/v3_0/sensors.py
Expand Up @@ -17,6 +17,11 @@
from flexmeasures.data.schemas.sensors import SensorSchema
from flexmeasures.data.services.sensors import get_sensors

# Instantiate schemas outside of endpoint logic to minimize response time
get_sensor_schema = GetSensorDataSchema()
post_sensor_schema = PostSensorDataSchema()
sensors_schema = SensorSchema(many=True)


class SensorAPI(FlaskView):

Expand Down Expand Up @@ -68,13 +73,14 @@ def index(self, account: Account):
:status 400: INVALID_REQUEST
:status 401: UNAUTHORIZED
:status 403: INVALID_SENDER
:status 422: UNPROCESSABLE_ENTITY
"""
sensors = get_sensors(account_name=account.name)
return SensorSchema(many=True).dump(sensors), 200
return sensors_schema.dump(sensors), 200

@route("/data", methods=["POST"])
@use_args(
PostSensorDataSchema(),
post_sensor_schema,
location="json",
)
def post_data(self, bdf: BeliefsDataFrame):
Expand Down Expand Up @@ -102,13 +108,22 @@ def post_data(self, bdf: BeliefsDataFrame):
The unit has to be convertible to the sensor's unit.
The resolution of the data has to match the sensor's required resolution, but
FlexMeasures will attempt to upsample lower resolutions.

:reqheader Authorization: The authentication token
:reqheader Content-Type: application/json
:resheader Content-Type: application/json
:status 200: PROCESSED
:status 400: INVALID_REQUEST
:status 401: UNAUTHORIZED
:status 403: INVALID_SENDER
:status 422: UNPROCESSABLE_ENTITY
"""
response, code = save_and_enqueue(bdf)
return response, code

@route("/data", methods=["GET"])
@use_args(
GetSensorDataSchema(),
get_sensor_schema,
location="query",
)
def get_data(self, response: dict):
Expand All @@ -128,5 +143,14 @@ def get_data(self, response: dict):
}

The unit has to be convertible from the sensor's unit.

:reqheader Authorization: The authentication token
:reqheader Content-Type: application/json
:resheader Content-Type: application/json
:status 200: PROCESSED
:status 400: INVALID_REQUEST
:status 401: UNAUTHORIZED
:status 403: INVALID_SENDER
:status 422: UNPROCESSABLE_ENTITY
"""
return json.dumps(response)
16 changes: 12 additions & 4 deletions flexmeasures/api/v3_0/users.py
Expand Up @@ -24,6 +24,11 @@
Both POST (to create) and DELETE are not accessible via the API, but as CLI functions.
"""

# Instantiate schemas outside of endpoint logic to minimize response time
user_schema = UserSchema()
users_schema = UserSchema(many=True)
partial_user_schema = UserSchema(partial=True)


class UserAPI(FlaskView):
route_base = "/users"
Expand Down Expand Up @@ -78,9 +83,10 @@ def index(self, account: Account, include_inactive: bool = False):
:status 400: INVALID_REQUEST
:status 401: UNAUTHORIZED
:status 403: INVALID_SENDER
:status 422: UNPROCESSABLE_ENTITY
"""
users = get_users(account_name=account.name, only_active=not include_inactive)
return UserSchema(many=True).dump(users), 200
return users_schema.dump(users), 200

@route("/<id>")
@use_kwargs({"user": UserIdField(data_key="id")}, location="path")
Expand Down Expand Up @@ -115,11 +121,12 @@ def get(self, id: int, user: UserModel):
:status 400: INVALID_REQUEST, REQUIRED_INFO_MISSING, UNEXPECTED_PARAMS
:status 401: UNAUTHORIZED
:status 403: INVALID_SENDER
:status 422: UNPROCESSABLE_ENTITY
"""
return UserSchema().dump(user), 200
return user_schema.dump(user), 200

@route("/<id>", methods=["PATCH"])
@use_kwargs(UserSchema(partial=True))
@use_kwargs(partial_user_schema)
@use_kwargs({"user": UserIdField(data_key="id")}, location="path")
@permission_required_for_context("update", arg_name="user")
@as_json
Expand Down Expand Up @@ -191,7 +198,7 @@ def patch(self, id: int, user: UserModel, **user_data):
dict(message="Duplicate user already exists", detail=ie._message()),
400,
)
return UserSchema().dump(user), 200
return user_schema.dump(user), 200

@route("/<id>/password-reset", methods=["PATCH"])
@use_kwargs({"user": UserIdField(data_key="id")}, location="path")
Expand All @@ -216,6 +223,7 @@ def reset_user_password(self, id: int, user: UserModel):
:status 400: INVALID_REQUEST, REQUIRED_INFO_MISSING, UNEXPECTED_PARAMS
:status 401: UNAUTHORIZED
:status 403: INVALID_SENDER
:status 422: UNPROCESSABLE_ENTITY
"""
set_random_password(user)
remove_cookie_and_token_access(user)
Expand Down