|
1 | 1 | from __future__ import annotations
|
2 | 2 |
|
3 |
| -import pint |
4 | 3 | from marshmallow import (
|
5 | 4 | Schema,
|
6 | 5 | fields,
|
|
19 | 18 | )
|
20 | 19 | from flexmeasures.data.schemas.utils import FMValidationError
|
21 | 20 | from flexmeasures.data.schemas.times import AwareDateTimeField, PlanningDurationField
|
22 |
| -from flexmeasures.utils.unit_utils import ur, units_are_convertible |
| 21 | +from flexmeasures.utils.flexmeasures_inflection import p |
| 22 | +from flexmeasures.utils.unit_utils import ( |
| 23 | + ur, |
| 24 | + units_are_convertible, |
| 25 | + is_capacity_price_unit, |
| 26 | + is_energy_price_unit, |
| 27 | + is_power_unit, |
| 28 | + is_energy_unit, |
| 29 | +) |
23 | 30 |
|
24 | 31 |
|
25 | 32 | class FlexContextSchema(Schema):
|
@@ -153,6 +160,8 @@ def check_prices(self, data: dict, **kwargs):
|
153 | 160 | f"""Please switch to using `production-price: {{"sensor": {data[field_map["production-price-sensor"]].id}}}`."""
|
154 | 161 | )
|
155 | 162 |
|
| 163 | + # make sure that the prices fields are valid price units |
| 164 | + |
156 | 165 | # All prices must share the same unit
|
157 | 166 | data = self._try_to_convert_price_units(data)
|
158 | 167 |
|
@@ -224,40 +233,122 @@ def _get_variable_quantity_unit(
|
224 | 233 |
|
225 | 234 |
|
226 | 235 | class DBFlexContextSchema(FlexContextSchema):
|
| 236 | + mapped_schema_keys = { |
| 237 | + field: FlexContextSchema().declared_fields[field].data_key |
| 238 | + for field in FlexContextSchema().declared_fields |
| 239 | + } |
227 | 240 |
|
228 | 241 | @validates_schema
|
229 | 242 | def forbid_time_series_specs(self, data: dict, **kwargs):
|
230 | 243 | """Do not allow time series specs for the flex-context fields saved in the db."""
|
231 | 244 |
|
232 |
| - keys_to_check = [] |
233 | 245 | # List of keys to check for time series specs
|
| 246 | + keys_to_check = [] |
234 | 247 | # All the keys in this list are all fields of type VariableQuantity
|
235 | 248 | for field_var, field in self.declared_fields.items():
|
236 | 249 | if isinstance(field, VariableQuantityField):
|
237 |
| - keys_to_check.append(field_var) |
| 250 | + keys_to_check.append((field_var, field)) |
238 | 251 |
|
239 | 252 | # Check each key and raise a ValidationError if it's a list
|
240 |
| - for key in keys_to_check: |
241 |
| - if key in data and isinstance(data[key], list): |
| 253 | + for field_var, field in keys_to_check: |
| 254 | + if field_var in data and isinstance(data[field_var], list): |
242 | 255 | raise ValidationError(
|
243 |
| - f"Time series specs are not allowed in flex-context fields in the DB for '{key}'." |
| 256 | + "A time series specification (listing segments) is not supported when storing flex-context fields. Use a fixed quantity or a sensor reference instead.", |
| 257 | + field_name=field.data_key, |
244 | 258 | )
|
245 | 259 |
|
246 | 260 | @validates_schema
|
247 |
| - def forbid_fixed_prices(self, data: dict, **kwargs): |
248 |
| - """Do not allow fixed consumption price or fixed production price in the flex-context fields saved in the db.""" |
| 261 | + def validate_fields_unit(self, data: dict, **kwargs): |
| 262 | + """Check that each field value has a valid unit.""" |
| 263 | + |
| 264 | + self._validate_price_fields(data) |
| 265 | + self._validate_power_fields(data) |
| 266 | + self._validate_inflexible_device_sensors(data) |
| 267 | + |
| 268 | + def _validate_price_fields(self, data: dict): |
| 269 | + """Validate price fields.""" |
| 270 | + energy_price_fields = [ |
| 271 | + "consumption_price", |
| 272 | + "production_price", |
| 273 | + ] |
| 274 | + capacity_price_fields = [ |
| 275 | + "ems_consumption_breach_price", |
| 276 | + "ems_production_breach_price", |
| 277 | + "ems_peak_consumption_price", |
| 278 | + "ems_peak_production_price", |
| 279 | + ] |
| 280 | + |
| 281 | + # Check that consumption and production prices are Sensors |
| 282 | + self._forbid_fixed_prices(data) |
| 283 | + |
| 284 | + for field in energy_price_fields: |
| 285 | + if field in data: |
| 286 | + self._validate_field(data, "energy price", field, is_energy_price_unit) |
| 287 | + for field in capacity_price_fields: |
| 288 | + if field in data: |
| 289 | + self._validate_field( |
| 290 | + data, "capacity price", field, is_capacity_price_unit |
| 291 | + ) |
| 292 | + |
| 293 | + def _validate_power_fields(self, data: dict): |
| 294 | + """Validate power fields.""" |
| 295 | + power_fields = [ |
| 296 | + "ems_power_capacity_in_mw", |
| 297 | + "ems_production_capacity_in_mw", |
| 298 | + "ems_consumption_capacity_in_mw", |
| 299 | + "ems_peak_consumption_in_mw", |
| 300 | + "ems_peak_production_in_mw", |
| 301 | + ] |
| 302 | + |
| 303 | + for field in power_fields: |
| 304 | + if field in data: |
| 305 | + self._validate_field(data, "power", field, is_power_unit) |
| 306 | + |
| 307 | + def _validate_field(self, data: dict, field_type: str, field: str, unit_validator): |
| 308 | + """Validate fields based on type and unit validator.""" |
| 309 | + |
| 310 | + if isinstance(data[field], ur.Quantity): |
| 311 | + if not unit_validator(str(data[field].units)): |
| 312 | + raise ValidationError( |
| 313 | + f"{field_type.capitalize()} field '{self.mapped_schema_keys[field]}' must have {p.a(field_type)} unit.", |
| 314 | + field_name=self.mapped_schema_keys[field], |
| 315 | + ) |
| 316 | + elif isinstance(data[field], Sensor): |
| 317 | + if not unit_validator(data[field].unit): |
| 318 | + raise ValidationError( |
| 319 | + f"{field_type.capitalize()} field '{self.mapped_schema_keys[field]}' must have {p.a(field_type)} unit.", |
| 320 | + field_name=self.mapped_schema_keys[field], |
| 321 | + ) |
| 322 | + |
| 323 | + def _validate_inflexible_device_sensors(self, data: dict): |
| 324 | + """Validate inflexible device sensors.""" |
| 325 | + if "inflexible_device_sensors" in data: |
| 326 | + for sensor in data["inflexible_device_sensors"]: |
| 327 | + if not is_power_unit(sensor.unit) and not is_energy_unit(sensor.unit): |
| 328 | + raise ValidationError( |
| 329 | + f"Inflexible device sensor '{sensor.id}' must have a power or energy unit.", |
| 330 | + field_name="inflexible-device-sensors", |
| 331 | + ) |
| 332 | + |
| 333 | + def _forbid_fixed_prices(self, data: dict, **kwargs): |
| 334 | + """Do not allow fixed consumption price or fixed production price in the flex-context fields saved in the db. |
| 335 | +
|
| 336 | + This is a temporary restriction as future iterations will allow fixed prices on these fields as well. |
| 337 | + """ |
249 | 338 | if "consumption_price" in data and isinstance(
|
250 |
| - data["consumption_price"], pint.Quantity |
| 339 | + data["consumption_price"], ur.Quantity |
251 | 340 | ):
|
252 | 341 | raise ValidationError(
|
253 |
| - "Fixed prices are not currently supported for consumption_price in flex-context fields in the DB." |
| 342 | + "Fixed prices are not currently supported for consumption-price in flex-context fields in the DB.", |
| 343 | + field_name="consumption-price", |
254 | 344 | )
|
255 | 345 |
|
256 | 346 | if "production_price" in data and isinstance(
|
257 |
| - data["production_price"], pint.Quantity |
| 347 | + data["production_price"], ur.Quantity |
258 | 348 | ):
|
259 | 349 | raise ValidationError(
|
260 |
| - "Fixed prices are not currently supported for production_price in flex-context fields in the DB." |
| 350 | + "Fixed prices are not currently supported for production-price in flex-context fields in the DB.", |
| 351 | + field_name="production-price", |
261 | 352 | )
|
262 | 353 |
|
263 | 354 |
|
|
0 commit comments