sensors.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import json
  2. import warnings
  3. from flask_classful import FlaskView, route
  4. from flask_security import current_user
  5. from marshmallow import fields
  6. from webargs.flaskparser import use_kwargs
  7. from werkzeug.exceptions import abort
  8. from flexmeasures.data import db
  9. from flexmeasures.auth.policy import ADMIN_ROLE, ADMIN_READER_ROLE
  10. from flexmeasures.auth.decorators import permission_required_for_context
  11. from flexmeasures.data.schemas import (
  12. AssetIdField,
  13. AwareDateTimeField,
  14. DurationField,
  15. SensorIdField,
  16. )
  17. from flexmeasures.data.models.generic_assets import GenericAsset
  18. from flexmeasures.data.models.time_series import Sensor
  19. from flexmeasures.data.services.annotations import prepare_annotations_for_chart
  20. from flexmeasures.ui.utils.view_utils import set_session_variables
  21. class SensorAPI(FlaskView):
  22. """
  23. This view exposes sensor attributes through API endpoints under development.
  24. These endpoints are not yet part of our official API, but support the FlexMeasures UI.
  25. """
  26. route_base = "/sensor"
  27. trailing_slash = False
  28. # Note: when promoting these endpoints to the main API, we aim to be strict with trailing slashes, see #1014
  29. @route("/<id>/chart", strict_slashes=False)
  30. @use_kwargs(
  31. {"sensor": SensorIdField(data_key="id")},
  32. location="path",
  33. )
  34. @use_kwargs(
  35. {
  36. "event_starts_after": AwareDateTimeField(format="iso", required=False),
  37. "event_ends_before": AwareDateTimeField(format="iso", required=False),
  38. "beliefs_after": AwareDateTimeField(format="iso", required=False),
  39. "beliefs_before": AwareDateTimeField(format="iso", required=False),
  40. "include_data": fields.Boolean(required=False),
  41. "include_sensor_annotations": fields.Boolean(required=False),
  42. "include_asset_annotations": fields.Boolean(required=False),
  43. "include_account_annotations": fields.Boolean(required=False),
  44. "dataset_name": fields.Str(required=False),
  45. "chart_type": fields.Str(required=False),
  46. "height": fields.Str(required=False),
  47. "width": fields.Str(required=False),
  48. },
  49. location="query",
  50. )
  51. @permission_required_for_context("read", ctx_arg_name="sensor")
  52. def get_chart(self, id: int, sensor: Sensor, **kwargs):
  53. """GET from /sensor/<id>/chart
  54. .. :quickref: Chart; Download a chart with time series
  55. **Optional fields**
  56. - "event_starts_after" (see the `timely-beliefs documentation <https://github.com/SeitaBV/timely-beliefs/blob/main/timely_beliefs/docs/timing.md/#events-and-sensors>`_)
  57. - "event_ends_before" (see the `timely-beliefs documentation <https://github.com/SeitaBV/timely-beliefs/blob/main/timely_beliefs/docs/timing.md/#events-and-sensors>`_)
  58. - "beliefs_after" (see the `timely-beliefs documentation <https://github.com/SeitaBV/timely-beliefs/blob/main/timely_beliefs/docs/timing.md/#events-and-sensors>`_)
  59. - "beliefs_before" (see the `timely-beliefs documentation <https://github.com/SeitaBV/timely-beliefs/blob/main/timely_beliefs/docs/timing.md/#events-and-sensors>`_)
  60. - "include_data" (if true, chart specs include the data; if false, use the `GET /api/dev/sensor/(id)/chart_data <../api/dev.html#get--api-dev-sensor-(id)-chart_data->`_ endpoint to fetch data)
  61. - "chart_type" (currently 'bar_chart' and 'daily_heatmap' are supported types)
  62. - "width" (an integer number of pixels; without it, the chart will be scaled to the full width of the container (hint: use ``<div style="width: 100%;">`` to set a div width to 100%)
  63. - "height" (an integer number of pixels; without it, FlexMeasures sets a default, currently 300)
  64. """
  65. # Store selected time range and chart type as session variables, for a consistent UX across UI page loads
  66. set_session_variables("event_starts_after", "event_ends_before", "chart_type")
  67. return json.dumps(sensor.chart(**kwargs))
  68. @route("/<id>/chart_data", strict_slashes=False)
  69. @use_kwargs(
  70. {"sensor": SensorIdField(data_key="id")},
  71. location="path",
  72. )
  73. @use_kwargs(
  74. {
  75. "event_starts_after": AwareDateTimeField(format="iso", required=False),
  76. "event_ends_before": AwareDateTimeField(format="iso", required=False),
  77. "beliefs_after": AwareDateTimeField(format="iso", required=False),
  78. "beliefs_before": AwareDateTimeField(format="iso", required=False),
  79. "resolution": DurationField(required=False),
  80. "most_recent_beliefs_only": fields.Boolean(
  81. required=False, load_default=True
  82. ),
  83. },
  84. location="query",
  85. )
  86. @permission_required_for_context("read", ctx_arg_name="sensor")
  87. def get_chart_data(self, id: int, sensor: Sensor, **kwargs):
  88. """GET from /sensor/<id>/chart_data
  89. .. :quickref: Chart; Download time series for use in charts
  90. Data for use in charts (in case you have the chart specs already).
  91. **Optional fields**
  92. - "event_starts_after" (see the `timely-beliefs documentation <https://github.com/SeitaBV/timely-beliefs/blob/main/timely_beliefs/docs/timing.md/#events-and-sensors>`_)
  93. - "event_ends_before" (see the `timely-beliefs documentation <https://github.com/SeitaBV/timely-beliefs/blob/main/timely_beliefs/docs/timing.md/#events-and-sensors>`_)
  94. - "beliefs_after" (see the `timely-beliefs documentation <https://github.com/SeitaBV/timely-beliefs/blob/main/timely_beliefs/docs/timing.md/#events-and-sensors>`_)
  95. - "beliefs_before" (see the `timely-beliefs documentation <https://github.com/SeitaBV/timely-beliefs/blob/main/timely_beliefs/docs/timing.md/#events-and-sensors>`_)
  96. - "resolution" (see :ref:`resolutions`)
  97. - "most_recent_beliefs_only" (if true, returns the most recent belief for each event; if false, returns each belief for each event; defaults to true)
  98. """
  99. return sensor.search_beliefs(as_json=True, **kwargs)
  100. @route("/<id>/chart_annotations", strict_slashes=False)
  101. @use_kwargs(
  102. {"sensor": SensorIdField(data_key="id")},
  103. location="path",
  104. )
  105. @use_kwargs(
  106. {
  107. "event_starts_after": AwareDateTimeField(format="iso", required=False),
  108. "event_ends_before": AwareDateTimeField(format="iso", required=False),
  109. "beliefs_after": AwareDateTimeField(format="iso", required=False),
  110. "beliefs_before": AwareDateTimeField(format="iso", required=False),
  111. },
  112. location="query",
  113. )
  114. @permission_required_for_context("read", ctx_arg_name="sensor")
  115. def get_chart_annotations(self, id: int, sensor: Sensor, **kwargs):
  116. """GET from /sensor/<id>/chart_annotations
  117. .. :quickref: Chart; Download annotations for use in charts
  118. Annotations for use in charts (in case you have the chart specs already).
  119. """
  120. event_starts_after = kwargs.get("event_starts_after", None)
  121. event_ends_before = kwargs.get("event_ends_before", None)
  122. df = sensor.generic_asset.search_annotations(
  123. annotations_after=event_starts_after,
  124. annotations_before=event_ends_before,
  125. as_frame=True,
  126. )
  127. # Wrap and stack annotations
  128. df = prepare_annotations_for_chart(df)
  129. # Return JSON records
  130. df = df.reset_index()
  131. df["source"] = df["source"].astype(str)
  132. return df.to_json(orient="records")
  133. @route("/<id>", strict_slashes=False)
  134. @use_kwargs(
  135. {"sensor": SensorIdField(data_key="id")},
  136. location="path",
  137. )
  138. @permission_required_for_context("read", ctx_arg_name="sensor")
  139. def get(self, id: int, sensor: Sensor):
  140. """GET from /sensor/<id>
  141. .. :quickref: Chart; Download sensor attributes for use in charts
  142. """
  143. attributes = ["name", "timezone", "timerange"]
  144. return {attr: getattr(sensor, attr) for attr in attributes}
  145. class AssetAPI(FlaskView):
  146. """
  147. This view exposes asset attributes through API endpoints under development.
  148. These endpoints are not yet part of our official API, but support the FlexMeasures UI.
  149. """
  150. route_base = "/asset"
  151. trailing_slash = False
  152. @route("/<id>", strict_slashes=False)
  153. @use_kwargs(
  154. {"asset": AssetIdField(data_key="id")},
  155. location="path",
  156. )
  157. @permission_required_for_context("read", ctx_arg_name="asset")
  158. def get(self, id: int, asset: GenericAsset):
  159. """GET from /asset/<id>
  160. .. :quickref: Chart; Download asset attributes for use in charts
  161. """
  162. attributes = ["name", "timezone", "timerange_of_sensors_to_show"]
  163. return {attr: getattr(asset, attr) for attr in attributes}
  164. def get_sensor_or_abort(id: int) -> Sensor:
  165. """
  166. Util function to help the GET requests. Will be obsolete..
  167. """
  168. warnings.warn(
  169. "Util function will be deprecated. Switch to using SensorIdField to suppress this warning.",
  170. FutureWarning,
  171. )
  172. sensor = db.session.get(Sensor, id)
  173. if sensor is None:
  174. raise abort(404, f"Sensor {id} not found")
  175. if not (
  176. current_user.has_role(ADMIN_ROLE)
  177. or current_user.has_role(ADMIN_READER_ROLE)
  178. or sensor.generic_asset.owner is None # public
  179. or sensor.generic_asset.owner == current_user.account # private but authorized
  180. ):
  181. raise abort(403)
  182. return sensor