/
implementations.py
228 lines (202 loc) · 8.22 KB
/
implementations.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
from datetime import datetime, timedelta
import isodate
from flask_json import as_json
from flask import request, current_app
from flexmeasures.utils.entity_address_utils import (
parse_entity_address,
EntityAddressException,
)
from flexmeasures.api.common.responses import (
invalid_domain,
invalid_datetime,
invalid_timezone,
request_processed,
unrecognized_event,
invalid_market,
unknown_prices,
unrecognized_connection_group,
outdated_event_id,
ptus_incomplete,
)
from flexmeasures.api.common.utils.api_utils import (
groups_to_dict,
get_form_from_request,
)
from flexmeasures.api.common.utils.validators import (
type_accepted,
assets_required,
optional_duration_accepted,
units_accepted,
parse_isodate_str,
)
from flexmeasures.data import db
from flexmeasures.data.models.planning.storage import StorageScheduler
from flexmeasures.data.models.planning.exceptions import (
UnknownMarketException,
UnknownPricesException,
)
from flexmeasures.data.models.time_series import Sensor
from flexmeasures.data.services.resources import has_assets, can_access_asset
from flexmeasures.data.models.planning.utils import ensure_storage_specs
from flexmeasures.utils.time_utils import duration_isoformat
@type_accepted("GetDeviceMessageRequest")
@assets_required("event")
@optional_duration_accepted(timedelta(hours=6))
@as_json
def get_device_message_response(generic_asset_name_groups, duration):
unit = "MW"
min_planning_horizon = timedelta(
hours=24
) # user can request a shorter planning, but the scheduler takes into account at least this horizon
planning_horizon = min(
max(min_planning_horizon, duration),
current_app.config.get("FLEXMEASURES_PLANNING_HORIZON"),
)
if not has_assets():
current_app.logger.info("User doesn't seem to have any assets.")
value_groups = []
new_event_groups = []
for event_group in generic_asset_name_groups:
for event in event_group:
# Parse the entity address
try:
ea = parse_entity_address(event, entity_type="event", fm_scheme="fm0")
except EntityAddressException as eae:
return invalid_domain(str(eae))
sensor_id = ea["asset_id"]
event_id = ea["event_id"]
event_type = ea["event_type"]
# Look for the Sensor object
sensor = Sensor.query.filter(Sensor.id == sensor_id).one_or_none()
if sensor is None or not can_access_asset(sensor):
current_app.logger.warning(
"Cannot identify sensor given the event %s." % event
)
return unrecognized_connection_group()
if sensor.generic_asset.generic_asset_type.name != "battery":
return invalid_domain(
"API version 1.2 only supports device messages for batteries. "
"Sensor ID:%s does not belong to a battery." % sensor_id
)
if event_type != "soc" or event_id != sensor.generic_asset.get_attribute(
"soc_udi_event_id"
):
return unrecognized_event(event_id, event_type)
start = datetime.fromisoformat(
sensor.generic_asset.get_attribute("soc_datetime")
)
end = start + planning_horizon
resolution = sensor.event_resolution
# Schedule the asset
storage_specs = dict(
soc_at_start=sensor.generic_asset.get_attribute("soc_in_mwh"),
prefer_charging_sooner=False,
)
storage_specs = ensure_storage_specs(
storage_specs, sensor, start, end, resolution
)
try:
schedule = StorageScheduler().schedule(
sensor, start, end, resolution, storage_specs=storage_specs
)
except UnknownPricesException:
return unknown_prices()
except UnknownMarketException:
return invalid_market()
else:
# Update the planning window
start = schedule.index[0]
duration = min(duration, schedule.index[-1] + resolution - start)
schedule = schedule[start : start + duration - resolution]
value_groups.append(schedule.tolist())
new_event_groups.append(event)
response = groups_to_dict(
new_event_groups, value_groups, generic_asset_type_name="event"
)
response["start"] = isodate.datetime_isoformat(start)
response["duration"] = duration_isoformat(duration)
response["unit"] = unit
d, s = request_processed()
return dict(**response, **d), s
@type_accepted("PostUdiEventRequest")
@units_accepted("State of charge", "kWh", "MWh")
@as_json
def post_udi_event_response(unit): # noqa: C901
if not has_assets():
current_app.logger.info("User doesn't seem to have any assets.")
form = get_form_from_request(request)
# check datetime, or use server_now
if "datetime" not in form:
return invalid_datetime("Missing datetime parameter.")
else:
datetime = parse_isodate_str(form.get("datetime"))
if datetime is None:
return invalid_datetime(
"Cannot parse datetime string %s as iso date" % form.get("datetime")
)
if datetime.tzinfo is None:
current_app.logger.warning(
"Cannot parse timezone of 'datetime' value %s" % form.get("datetime")
)
return invalid_timezone("Datetime should explicitly state a timezone.")
# parse event/address info
if "event" not in form:
return invalid_domain("No event identifier sent.")
try:
ea = parse_entity_address(
form.get("event"), entity_type="event", fm_scheme="fm0"
)
except EntityAddressException as eae:
return invalid_domain(str(eae))
sensor_id = ea["asset_id"]
event_id = ea["event_id"]
event_type = ea["event_type"]
if event_type != "soc":
return unrecognized_event(event_id, event_type)
# Look for the Sensor object
sensor = Sensor.query.filter(Sensor.id == sensor_id).one_or_none()
if sensor is None or not can_access_asset(sensor):
current_app.logger.warning("Cannot identify sensor via %s." % ea)
return unrecognized_connection_group()
if sensor.generic_asset.generic_asset_type.name != "battery":
return invalid_domain(
"API version 1.2 only supports UDI events for batteries. "
"Sensor ID:%s does not belong to a battery." % sensor_id
)
# unless on play, keep events ordered by entry date and ID
if current_app.config.get("FLEXMEASURES_MODE") != "play":
# do not allow new date to be after last date
if (
isinstance(sensor.generic_asset.get_attribute("soc_datetime"), str)
and datetime.fromisoformat(
sensor.generic_asset.get_attribute("soc_datetime")
)
>= datetime
):
msg = (
"The date of the requested UDI event (%s) is earlier than the latest known date (%s)."
% (
datetime,
datetime.fromisoformat(
sensor.generic_asset.get_attribute("soc_datetime")
),
)
)
current_app.logger.warning(msg)
return invalid_datetime(msg)
# check if udi event id is higher than existing
soc_udi_event_id = sensor.generic_asset.get_attribute("soc_udi_event_id")
if soc_udi_event_id is not None and soc_udi_event_id >= event_id:
return outdated_event_id(event_id, soc_udi_event_id)
# get value
if "value" not in form:
return ptus_incomplete()
value = form.get("value")
if unit == "kWh":
value = value / 1000.0
# Store new soc info as GenericAsset attributes
sensor.generic_asset.set_attribute("soc_datetime", datetime.isoformat())
sensor.generic_asset.set_attribute("soc_udi_event_id", event_id)
sensor.generic_asset.set_attribute("soc_in_mwh", value)
db.session.commit()
return request_processed("Request has been processed.")