Skip to content

Commit

Permalink
Issue 6 entity address scheme improvements 2 (#81)
Browse files Browse the repository at this point in the history
* Rename entity type for weather sensors

* Update domain registration year in docstring

* Build and parse sensor entity addresses

* Fix pass-through of error response

* Add entity address properties to Market and WeatherSensor

* Make test util function more flexible

* Add marshmallow schema for sensors

* Improve test legibility

* Move setup of test WeatherSensors to higher conftest.py

* Better regex for date specification

* Test marshmallow schema for sensors

* mypy

* Fix variable naming of test util

* Update CLI command

* Fix tests with deprecation of sqlalchemy RowProxy in 1.4

* Prefer localhost over 127.0.0.1 in entity addresses and strip www.

* Introduce fm1 scheme for local part of entity addresses

* add docstrings to our API documentation, more explicit handling of no regex matches in parsing ea addresses

* also test parsing a fm1 type address

* review comments: typos, nomenclature

* implement review comments

Co-authored-by: F.N. Claessen <felix@seita.nl>
Co-authored-by: Nicolas Höning <nicolas@seita.nl>
Co-authored-by: Nicolas Höning <iam@nicolashoening.de>
  • Loading branch information
4 people committed May 28, 2021
1 parent 908f8bb commit 927aba9
Show file tree
Hide file tree
Showing 31 changed files with 775 additions and 279 deletions.
4 changes: 2 additions & 2 deletions Makefile
Expand Up @@ -15,13 +15,13 @@ test:
# ---- Documentation ---

update-docs:
pip3 install sphinx==3.5.4 sphinxcontrib.httpdomain # sphinx4 is not supported yet by sphinx-contrib/httpdomain, see https://github.com/sphinx-contrib/httpdomain/issues/46
pip3 install sphinx==3.5.4 sphinxcontrib.httpdomain # sphinx4 is not supported yet by sphinx-contrib/httpdomain, see https://github.com/sphinx-contrib/httpdomain/issues/46
cd documentation; make clean; make html; cd ..

update-docs-pdf:
@echo "NOTE: PDF documentation requires packages (on Debian: latexmk texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended)"
@echo "NOTE: Currently, the docs require some pictures which are not in the git repo atm. Ask the devs."
pip3 install sphinx sphinxcontrib.httpdomain
pip3 install sphinx sphinx-rtd-theme sphinxcontrib.httpdomain
cd documentation; make clean; make latexpdf; make latexpdf; cd .. # make latexpdf can require two passes

# ---- Installation ---
Expand Down
10 changes: 9 additions & 1 deletion documentation/api/change_log.rst
Expand Up @@ -6,6 +6,14 @@ API change log
.. note:: The FlexMeasures API follows its own versioning scheme. This also reflects in the URL, allowing developers to upgrade at their own pace.


v2.0-3 | 2021-05-XX
"""""""""""""""""

- Updated all entity addresses in documentation according to the fm0 scheme, preserving backwards compatibility.
- Introduced the fm1 scheme for entity addresses for connections, markets, weather sensors and sensors.



v2.0-2 | 2021-04-02
"""""""""""""""""

Expand All @@ -31,7 +39,7 @@ v2.0-0 | 2020-11-14
- REST endpoints for managing assets: `/assets/` (GET, POST) and `/asset/<id>` (GET, PATCH, DELETE).


v1.3.9 | 2021-04-XX
v1.3.9 | 2021-04-21
"""""""""""""""""

*Affects all versions since v1.0*.
Expand Down
73 changes: 50 additions & 23 deletions documentation/api/introduction.rst
Expand Up @@ -179,8 +179,8 @@ 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
^^^^^^^^^^^
Connections and entity addresses
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Connections are end points of the grid at which an asset is located.
Connections should be identified with an entity address following the EA1 addressing scheme prescribed by USEF[1],
Expand All @@ -199,47 +199,74 @@ Here is a full example for a FlexMeasures connection address:
.. code-block:: json
{
"connection": "ea1.2021-02.io.flexmeasures.company:30:73"
"connection": "ea1.2021-02.io.flexmeasures.company:fm0.30:73"
}
where FlexMeasures runs at `company.flexmeasures.io` and the owner ID is 30 and the asset ID is 73.
The owner ID is optional. Both the owner ID and the asset ID, as well as the full entity address can be obtained on the asset's listing after logging in:
where FlexMeasures runs at `company.flexmeasures.io` (which the current domain owner started using in February 2021), and the locally unique string is of scheme `fm0` (see below) and the asset ID is 73. The asset's owner ID is 30, but this part is optional.

Both the owner ID and the asset ID, as well as the full entity address can be obtained on the asset's listing:

.. code-block:: html

https://company.flexmeasures.io/assets


Entity address structure
""""""""""""""""""""""""""
Some deeper explanations about an entity address:

- "ea1" is a constant, indicating this is a type 1 USEF entity address
- The date code "must be a date during which the naming authority owned the domain name used in this format, and should be the first month in which the domain name was owned by this naming authority at 00:01 GMT of the first day of the month.
- The reversed domain name is taken from the naming authority (person or organization) creating this entity address
- The locally unique string can be used for local purposes, and FlexMeasures uses it to identify the resource (more information in parse_entity_address).
- The locally unique string can be used for local purposes, and FlexMeasures uses it to identify the resource.
Fields in the locally unique string are separated by colons, see for other examples
IETF RFC 3721, page 6 [3]. While [2] says it's possible to use dashes, dots or colons as separators, we might use dashes and dots in
latitude/longitude coordinates of sensors, so we settle on colons.


[1] https://www.usef.energy/app/uploads/2020/01/USEF-Flex-Trading-Protocol-Specifications-1.01.pdf

[2] https://tools.ietf.org/html/rfc3720

[3] https://tools.ietf.org/html/rfc3721

Notation for simulation
"""""""""""""""""""""""

For version 1 of the API, the following simplified addressing scheme may be used:
Types of asset identifications used in FlexMeasures
""""""""""""""""""""""""""""""""""""""""""

.. code-block:: json
FlexMeasures expects the locally unique string string to contain information in
a certain structure. We distinguish type ``fm0`` and type ``fm1`` FlexMeasures entity addresses.

{
"connection": "<owner-id>:<asset-id>"
}
The ``fm0`` scheme is the original scheme. It identifies connected assets, weather stations, markets and UDI events in different ways.

or even simpler:
Examples for the fm0 scheme:

.. code-block:: json
- connection = ea1.2021-01.localhost:fm0.40:30
- connection = ea1.2021-01.io.flexmeasures:fm0.<owner_id>:<asset_id>
- weather_sensor = ea1.2021-01.io.flexmeasures:fm0.temperature:52:73.0
- weather_sensor = ea1.2021-01.io.flexmeasures:fm0.<sensor_type>:<latitude>:<longitude>
- market = ea1.2021-01.io.flexmeasures:fm0.epex_da
- market = ea1.2021-01.io.flexmeasures:fm0.<market_name>
- event = ea1.2021-01.io.flexmeasures:fm0.40:30:302:soc
- event = ea1.2021-01.io.flexmeasures:fm0.<owner_id>:<asset_id>:<event_id>:<event_type>

This scheme is explicit but also a little cumbersome to use, as one needs to look up the type or even owner (for assets), and weather sensors are identified by coordinates.
For the fm0 scheme, the 'fm0.' part is optional, for backwards compatibility.


The ``fm1`` scheme is the latest version, currently under development. It works with the database structure
we are developing in the background, where all connected sensors have unique IDs. This makes it more straightforward (the scheme works the same way for all types of sensors), if less explicit.

Examples for the fm1 scheme:

- sensor = ea1.2021-01.io.flexmeasures:fm1.42
- sensor = ea1.2021-01.io.flexmeasures:fm1.<sensor_id>
- connection = ea1.2021-01.io.flexmeasures:fm1.<sensor_id>
- market = ea1.2021-01.io.flexmeasures:fm1.<sensor_id>
- weather_station = ea1.2021-01.io.flexmeasures:fm1.<sensor_id>

.. todo:: UDI events are not yet modelled in the fm1 scheme, but will probably be ea1.2021-01.io.flexmeasures:fm1.<actuator_id>

{
"connection": "<asset-id>"
}

Groups
^^^^^^
Expand All @@ -253,8 +280,8 @@ When the attributes "start", "duration" and "unit" are stated outside of "groups
"groups": [
{
"connections": [
"CS 1",
"CS 2"
"ea1.2021-02.io.flexmeasures.company:fm0.30:71",
"ea1.2021-02.io.flexmeasures.company:fm0.30:72"
],
"values": [
306.66,
Expand All @@ -266,7 +293,7 @@ When the attributes "start", "duration" and "unit" are stated outside of "groups
]
},
{
"connection": "CS 3",
"connection": "ea1.2021-02.io.flexmeasures.company:fm0.30:73"
"values": [
306.66,
0,
Expand All @@ -288,8 +315,8 @@ In case of a single group of connections, the message may be flattened to:
{
"connections": [
"CS 1",
"CS 2"
"ea1.2021-02.io.flexmeasures.company:fm0.30:71",
"ea1.2021-02.io.flexmeasures.company:fm0.30:72"
],
"values": [
306.66,
Expand Down
1 change: 1 addition & 0 deletions documentation/changelog.rst
Expand Up @@ -22,6 +22,7 @@ Infrastructure / Support
----------------------
* Add tutorials on how to add and read data from FlexMeasures via its API [see `PR #130 <http://www.github.com/SeitaBV/flexmeasures/pull/130>`_]
* For weather forecasts, switch from Dark Sky (closed from Aug 1, 2021) to OpenWeatherMap API [see `PR #113 <http://www.github.com/SeitaBV/flexmeasures/pull/113>`_]
* Entity address improvements: add new id-based `fm1` scheme, better documentation and more validation support of entity addresses [see `PR #81 <http://www.github.com/SeitaBV/flexmeasures/pull/81>`_]
* Re-use the database between automated tests, if possible. This shaves 2/3rd off of the time it takes for the FlexMeasures test suite to run [see `PR #115 <http://www.github.com/SeitaBV/flexmeasures/pull/115>`_]
* Make assets use MW as their default unit and enforce that in CLI, as well (API already did) [see `PR #108 <http://www.github.com/SeitaBV/flexmeasures/pull/108>`_]
* Let CLI package and plugins use Marshmallow Field definitions [see `PR #125 <http://www.github.com/SeitaBV/flexmeasures/pull/125>`_]
Expand Down
135 changes: 135 additions & 0 deletions flexmeasures/api/common/schemas/sensors.py
@@ -0,0 +1,135 @@
from typing import Union

from marshmallow import fields

from flexmeasures.api import FMValidationError
from flexmeasures.api.common.utils.api_utils import get_weather_sensor_by
from flexmeasures.utils.entity_address_utils import (
parse_entity_address,
EntityAddressException,
)
from flexmeasures.data.models.assets import Asset
from flexmeasures.data.models.markets import Market
from flexmeasures.data.models.weather import WeatherSensor
from flexmeasures.data.models.time_series import Sensor


class EntityAddressValidationError(FMValidationError):
status = "INVALID_DOMAIN" # USEF error status


class SensorField(fields.Str):
"""Field that de-serializes to a Sensor, Asset, Market or WeatherSensor
and serializes back to an entity address (string)."""

# todo: when Actuators also get an entity address, refactor this class to EntityField,
# where an Entity represents anything with an entity address: we currently foresee Sensors and Actuators

def __init__(
self,
entity_type: str,
fm_scheme: str,
*args,
**kwargs,
):
"""
:param entity_type: "sensor", "connection", "market" or "weather_sensor"
:param fm_scheme: "fm0" or "fm1"
"""
self.entity_type = entity_type
self.fm_scheme = fm_scheme
super().__init__(*args, **kwargs)

def _deserialize( # noqa: C901 todo: the noqa can probably be removed after refactoring Asset/Market/WeatherSensor to Sensor
self, value, attr, obj, **kwargs
) -> Union[Sensor, Asset, Market, WeatherSensor]:
"""De-serialize to a Sensor, Asset, Market or WeatherSensor."""
# TODO: After refactoring, unify 3 generic_asset cases -> 1 sensor case
try:
ea = parse_entity_address(value, self.entity_type, self.fm_scheme)
if self.fm_scheme == "fm0":
if self.entity_type == "connection":
asset = Asset.query.filter(Asset.id == ea["asset_id"]).one_or_none()
if asset is not None:
return asset
else:
raise EntityAddressValidationError(
f"Asset with entity address {value} doesn't exist."
)
elif self.entity_type == "market":
market = Market.query.filter(
Market.name == ea["market_name"]
).one_or_none()
if market is not None:
return market
else:
raise EntityAddressValidationError(
f"Market with entity address {value} doesn't exist."
)
elif self.entity_type == "weather_sensor":
weather_sensor = get_weather_sensor_by(
ea["weather_sensor_type_name"], ea["latitude"], ea["longitude"]
)
if weather_sensor is not None and isinstance(
weather_sensor, WeatherSensor
):
return weather_sensor
else:
raise EntityAddressValidationError(
f"Weather sensor with entity address {value} doesn't exist."
)
else:
if self.entity_type == "sensor":
sensor = Sensor.query.filter(
Sensor.id == ea["sensor_id"]
).one_or_none()
if sensor is not None:
return sensor
else:
raise EntityAddressValidationError(
f"Sensor with entity address {value} doesn't exist."
)
elif self.entity_type == "connection":
asset = Asset.query.filter(
Asset.id == ea["sensor_id"]
).one_or_none()
if asset is not None:
return asset
else:
raise EntityAddressValidationError(
f"Asset with entity address {value} doesn't exist."
)
elif self.entity_type == "market":
market = Market.query.filter(
Market.id == ea["sensor_id"]
).one_or_none()
if market is not None:
return market
else:
raise EntityAddressValidationError(
f"Market with entity address {value} doesn't exist."
)
elif self.entity_type == "weather_sensor":
weather_sensor = WeatherSensor.query.filter(
WeatherSensor.id == ea["sensor_id"]
).one_or_none()
if weather_sensor is not None and isinstance(
weather_sensor, WeatherSensor
):
return weather_sensor
else:
raise EntityAddressValidationError(
f"Weather sensor with entity address {value} doesn't exist."
)
except EntityAddressException as eae:
raise EntityAddressValidationError(str(eae))
return NotImplemented

def _serialize(
self, value: Union[Sensor, Asset, Market, WeatherSensor], attr, data, **kwargs
):
"""Serialize to an entity address."""
if self.fm_scheme == "fm0":
return value.entity_address_fm0
else:
return value.entity_address

0 comments on commit 927aba9

Please sign in to comment.