def wrapper(*args, **kwargs): form = get_form_from_request(request) if form is None: current_app.logger.warning( "Unsupported request method for unpacking 'values' from request." ) return invalid_method(request.method) if "value" in form: value_groups = [parse_as_list(form["value"], of_type=float)] elif "values" in form: value_groups = [parse_as_list(form["values"], of_type=float)] elif "groups" in form: value_groups = [] for group in form["groups"]: if "value" in group: value_groups.append(parse_as_list(group["value"], of_type=float)) elif "values" in group: value_groups.append(parse_as_list(group["values"], of_type=float)) else: current_app.logger.warning("Group %s missing value(s)" % group) return ptus_incomplete() else: current_app.logger.warning("Request missing value(s) or group.") return ptus_incomplete() if not contains_empty_items(value_groups): kwargs["value_groups"] = value_groups return fn(*args, **kwargs) else: extra_info = "Request includes empty or ill-formatted value(s)." current_app.logger.warning(extra_info) return ptus_incomplete(extra_info)
def wrapper(*args, **kwargs): form = get_form_from_request(request) if form is None: current_app.logger.warning( "Unsupported request method for unpacking 'start' and 'duration' from request." ) return invalid_method(request.method) if "start" in form: start = parse_isodate_str(form["start"]) if not start: current_app.logger.warning("Cannot parse 'start' value") return invalid_period() if start.tzinfo is None: current_app.logger.warning("Cannot parse timezone of 'start' value") return invalid_timezone( "Start time should explicitly state a timezone." ) else: current_app.logger.warning("Request missing 'start'.") return invalid_period() kwargs["start"] = start if "duration" in form: duration = parse_duration(form["duration"], start) if not duration: current_app.logger.warning("Cannot parse 'duration' value") return invalid_period() else: current_app.logger.warning("Request missing 'duration'.") return invalid_period() kwargs["duration"] = duration return fn(*args, **kwargs)
def decorated_service(*args, **kwargs): form = get_form_from_request(request) if form is None: current_app.logger.warning( "Unsupported request method for unpacking 'prior' from request." ) return invalid_method(request.method) if "prior" in form: prior = parse_isodate_str(form["prior"]) if ex_post is True: start = parse_isodate_str(form["start"]) duration = parse_duration(form["duration"], start) # todo: validate start and duration (refactor already duplicate code from period_required and optional_horizon_accepted) knowledge_time = ( start + duration ) # todo: take into account knowledge horizon function if prior < knowledge_time: extra_info = "Meter data can only be observed after the fact." return invalid_horizon(extra_info) else: prior = None kwargs["prior"] = prior return fn(*args, **kwargs)
def decorated_service(*args, **kwargs): kwargs[ "resolution" ] = None # using this decorator means you can expect this attribute, None means default form = get_form_from_request(request) if form is None: current_app.logger.warning( "Unsupported request method for unpacking 'resolution' from request." ) return invalid_method(request.method) if "resolution" in form and form["resolution"]: ds_resolution = parse_duration(form["resolution"]) if ds_resolution is None: return invalid_resolution_str(form["resolution"]) # Check if the resolution can be applied to all assets (if it is a multiple # of the event_resolution(s) and thus downsampling is possible) for asset_group in kwargs["generic_asset_name_groups"]: for asset_descriptor in asset_group: generic_asset = get_generic_asset(asset_descriptor, entity_type) if generic_asset is None: return unrecognized_asset() asset_resolution = generic_asset.event_resolution if ds_resolution % asset_resolution != timedelta(minutes=0): return unapplicable_resolution( f"{isodate.duration_isoformat(asset_resolution)} or a multiple hereof." ) kwargs["resolution"] = to_offset( isodate.parse_duration(form["resolution"]) ).freqstr # Convert ISO period string to pandas frequency string return fn(*args, **kwargs)
def decorated_service(*args, **kwargs): try: current_app.logger.info(get_form_from_request(request)) except OSError as e: # don't crash if request can't be logged (e.g. [Errno 90] Message too long) current_app.logger.info(e) response = fn(*args, **kwargs) # expects flask response object if not ( hasattr(response, "json") and hasattr(response, "headers") and hasattr(response, "status_code") ): current_app.logger.warning( "Response is not a Flask response object. I did not assign a response type." ) return response data = response.json headers = dict( zip(Headers.keys(response.headers), Headers.values(response.headers)) ) status_code = response.status_code if "type" in data: current_app.logger.warning( "Response already contains 'type' key. I did not assign a new response type." ) else: data["type"] = response_type headers.pop("content-length", None) headers.pop("Content-Length", None) return data, status_code, headers
def decorated_service(*args, **kwargs): form = get_form_from_request(request) if form is None: current_app.logger.warning( "Unsupported request method for unpacking 'prior' from request." ) return invalid_method(request.method) if "prior" in form: prior = parse_isodate_str(form["prior"]) if ex_post is True: start = parse_isodate_str(form["start"]) duration = parse_duration(form["duration"], start) # todo: validate start and duration (refactor already duplicate code from period_required and optional_horizon_accepted) knowledge_time = ( start + duration ) # todo: take into account knowledge horizon function if prior < knowledge_time: extra_info = "Meter data can only be observed after the fact." return invalid_horizon(extra_info) elif infer_missing is True or ( infer_missing_play is True and current_app.config.get( "FLEXMEASURES_MODE", "") == "play"): # A missing prior is inferred by the server prior = server_now() else: # Otherwise, a missing prior is fine (a horizon may still be inferred by the server) prior = None kwargs["prior"] = prior return fn(*args, **kwargs)
def decorated_service(*args, **kwargs): form = get_form_from_request(request) if form is None: current_app.logger.warning( "Unsupported request method for unpacking 'horizon' from request." ) return invalid_method(request.method) rolling = True if "horizon" in form: horizon, rolling = parse_horizon(form["horizon"]) if horizon is None: current_app.logger.warning("Cannot parse 'horizon' value") return invalid_horizon() elif ex_post is True: if horizon > timedelta(hours=0): extra_info = "Meter data must have a zero or negative horizon to indicate observations after the fact." return invalid_horizon(extra_info) elif infer_missing is True: # A missing horizon is only accepted if the server can infer it if "start" in form and "duration" in form: start = parse_isodate_str(form["start"]) duration = parse_duration(form["duration"], start) if not start: extra_info = "Cannot parse 'start' value." current_app.logger.warning(extra_info) return invalid_period(extra_info) if start.tzinfo is None: current_app.logger.warning( "Cannot parse timezone of 'start' value" ) return invalid_timezone( "Start time should explicitly state a timezone." ) if not duration: extra_info = "Cannot parse 'duration' value." current_app.logger.warning(extra_info) return invalid_period(extra_info) if current_app.config.get("FLEXMEASURES_MODE", "") == "play": horizon = timedelta(hours=0) else: horizon = start + duration - server_now() rolling = False else: current_app.logger.warning( "Request missing both 'horizon', 'start' and 'duration'." ) extra_info = "Specify a 'horizon' value, or 'start' and 'duration' values so that the horizon can be inferred." return invalid_horizon(extra_info) else: # Otherwise, a missing horizon is fine horizon = None kwargs["horizon"] = horizon if infer_missing is True: kwargs["rolling"] = rolling return fn(*args, **kwargs)
def decorated_service(*args, **kwargs): form = get_form_from_request(request) if form is None: current_app.logger.warning( "Unsupported request method for unpacking 'type' from request." ) return invalid_method(request.method) elif "type" not in form: current_app.logger.warning("Request is missing message type.") return no_message_type() elif form["type"] != message_type: current_app.logger.warning("Type is not accepted for this endpoint.") return invalid_message_type(message_type) else: return fn(*args, **kwargs)
def wrapper(*args, **kwargs): form = get_form_from_request(request) if form is None: current_app.logger.warning( "Unsupported request method for unpacking 'unit' from request." ) return invalid_method(request.method) if "unit" in form: unit = form["unit"] else: current_app.logger.warning("Request missing 'unit'.") return invalid_unit(quantity=None, units=None) kwargs["unit"] = unit return fn(*args, **kwargs)
def decorated_service(*args, **kwargs): form = get_form_from_request(request) if form is None: current_app.logger.warning( "Unsupported request method for unpacking 'unit' from request." ) return invalid_method(request.method) elif "unit" not in form: current_app.logger.warning("Request is missing unit.") return invalid_unit(quantity, units) elif form["unit"] not in units: current_app.logger.warning( "Unit %s is not accepted as one of %s." % (form["unit"], units) ) return invalid_unit(quantity, units) else: kwargs["unit"] = form["unit"] return fn(*args, **kwargs)
def decorated_service(*args, **kwargs): form = get_form_from_request(request) if form is None: current_app.logger.warning( "Unsupported request method for unpacking '%s' from request." % plural_name ) return invalid_method(request.method) if generic_asset_type_name in form: generic_asset_name_groups = [ parse_as_list(form[generic_asset_type_name]) ] elif plural_name in form: generic_asset_name_groups = [parse_as_list(form[plural_name])] elif groups_name in form: generic_asset_name_groups = [] for group in form["groups"]: if generic_asset_type_name in group: generic_asset_name_groups.append( parse_as_list(group[generic_asset_type_name]) ) elif plural_name in group: generic_asset_name_groups.append( parse_as_list(group[plural_name]) ) else: current_app.logger.warning( "Group %s missing %s" % (group, plural_name) ) return unrecognized_connection_group() else: current_app.logger.warning("Request missing %s or group." % plural_name) return unrecognized_connection_group() if not contains_empty_items(generic_asset_name_groups): kwargs["generic_asset_name_groups"] = generic_asset_name_groups return fn(*args, **kwargs) else: current_app.logger.warning("Request includes empty %s." % plural_name) return unrecognized_connection_group()
def decorated_service(*args, **kwargs): form = get_form_from_request(request) if form is None: current_app.logger.warning( "Unsupported request method for unpacking 'source' from request." ) return invalid_method(request.method) if "source" in form: validated_user_source_ids = validate_user_sources( form["source"]) if None in validated_user_source_ids: return invalid_source(form["source"]) kwargs["user_source_ids"] = include_current_user_source_id( validated_user_source_ids) elif default_source is not None: kwargs["user_source_ids"] = include_current_user_source_id( validate_user_sources(default_source)) else: kwargs["user_source_ids"] = None return fn(*args, **kwargs)
def decorated_service(*args, **kwargs): form = get_form_from_request(request) if form is None: current_app.logger.warning( "Unsupported request method for unpacking 'horizon' from request." ) return invalid_method(request.method) rolling = True if "horizon" in form: horizon, rolling = parse_horizon(form["horizon"]) if horizon is None: current_app.logger.warning("Cannot parse 'horizon' value") return invalid_horizon() elif ex_post is True: if horizon > timedelta(hours=0): extra_info = "Meter data must have a zero or negative horizon to indicate observations after the fact." return invalid_horizon(extra_info) elif rolling is True and accept_repeating_interval is False: extra_info = ( "API versions 2.0 and higher use regular ISO 8601 durations instead of repeating time intervals. " "For example: R/P1D should be replaced by P1D.") return invalid_horizon(extra_info) elif infer_missing is True or ( infer_missing_play is True and current_app.config.get( "FLEXMEASURES_MODE", "") == "play"): # A missing horizon is set to zero horizon = timedelta(hours=0) else: # Otherwise, a missing horizon is fine (a prior may still be inferred by the server) horizon = None kwargs["horizon"] = horizon if infer_missing is True and accept_repeating_interval is True: kwargs["rolling"] = rolling return fn(*args, **kwargs)
def post_udi_event_response(unit: str, prior: datetime): if not has_assets(): current_app.logger.info("User doesn't seem to have any assets.") form = get_form_from_request(request) 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 not in ("soc", "soc-with-targets"): return unrecognized_event_type(event_type) # Look for the Sensor object sensor = Sensor.query.filter_by(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 not in ( "battery", "one-way_evse", "two-way_evse", ): return invalid_domain( f"API version 1.3 only supports UDI events for batteries and Electric Vehicle Supply Equipment (EVSE). " f"Sensor ID:{sensor_id} does not belong to a battery or EVSE, but {p.a(sensor.generic_asset.generic_asset_type.description)}." ) # 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 precede previous date if isinstance(sensor.generic_asset.get_attribute("soc_datetime"), str) and datetime < datetime.fromisoformat( sensor.generic_asset.get_attribute("soc_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 if sensor.generic_asset.get_attribute("soc_udi_event_id") is not None: if sensor.generic_asset.get_attribute( "soc_udi_event_id") >= event_id: return outdated_event_id( event_id, sensor.generic_asset.get_attribute("soc_udi_event_id")) # get value if "value" not in form: return ptus_incomplete() try: value = float(form.get("value")) except ValueError: extra_info = "Request includes empty or ill-formatted value(s)." current_app.logger.warning(extra_info) return ptus_incomplete(extra_info) if unit == "kWh": value = value / 1000.0 # get optional efficiency roundtrip_efficiency = form.get("roundtrip_efficiency", None) # get optional min and max SOC soc_min = form.get("soc_min", None) soc_max = form.get("soc_max", None) if soc_min is not None and unit == "kWh": soc_min = soc_min / 1000.0 if soc_max is not None and unit == "kWh": soc_max = soc_max / 1000.0 # set soc targets start_of_schedule = datetime end_of_schedule = datetime + current_app.config.get( "FLEXMEASURES_PLANNING_HORIZON") resolution = sensor.event_resolution soc_targets = pd.Series( np.nan, index=pd.date_range( start_of_schedule, end_of_schedule, freq=resolution, closed="right" ), # note that target values are indexed by their due date (i.e. closed="right") ) if event_type == "soc-with-targets": if "targets" not in form: return incomplete_event( event_id, event_type, "Cannot process event %s with missing targets." % form.get("event"), ) for target in form.get("targets"): # get target value if "value" not in target: return ptus_incomplete("Target missing value parameter.") try: target_value = float(target["value"]) except ValueError: extra_info = "Request includes empty or ill-formatted target value(s)." current_app.logger.warning(extra_info) return ptus_incomplete(extra_info) if unit == "kWh": target_value = target_value / 1000.0 # get target datetime if "datetime" not in target: return invalid_datetime("Target missing datetime parameter.") else: target_datetime = parse_isodate_str(target["datetime"]) if target_datetime is None: return invalid_datetime( "Cannot parse target datetime string %s as iso date" % target["datetime"]) if target_datetime.tzinfo is None: current_app.logger.warning( "Cannot parse timezone of target 'datetime' value %s" % target["datetime"]) return invalid_timezone( "Target datetime should explicitly state a timezone.") if target_datetime > end_of_schedule: return invalid_datetime( f'Target datetime exceeds {end_of_schedule}. Maximum scheduling horizon is {current_app.config.get("FLEXMEASURES_PLANNING_HORIZON")}.' ) target_datetime = target_datetime.astimezone( soc_targets.index.tzinfo ) # otherwise DST would be problematic # set target soc_targets.loc[target_datetime] = target_value create_scheduling_job( sensor_id, start_of_schedule, end_of_schedule, resolution=resolution, belief_time=prior, # server time if no prior time was sent soc_at_start=value, soc_targets=soc_targets, soc_min=soc_min, soc_max=soc_max, roundtrip_efficiency=roundtrip_efficiency, job_id=form.get("event"), enqueue=True, ) # 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()
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.")
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") except EntityAddressException as eae: return invalid_domain(str(eae)) asset_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) # get asset asset: Asset = Asset.query.filter_by(id=asset_id).one_or_none() if asset is None or not can_access_asset(asset): current_app.logger.warning("Cannot identify asset via %s." % ea) return unrecognized_connection_group() if asset.asset_type_name != "battery": return invalid_domain( "API version 1.2 only supports UDI events for batteries. Asset ID:%s is not a battery." % asset_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 asset.soc_datetime is not None: if asset.soc_datetime >= datetime: msg = ( "The date of the requested UDI event (%s) is earlier than the latest known date (%s)." % (datetime, asset.soc_datetime) ) current_app.logger.warning(msg) return invalid_datetime(msg) # check if udi event id is higher than existing if asset.soc_udi_event_id is not None: if asset.soc_udi_event_id >= event_id: return outdated_event_id(event_id, asset.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 in asset asset.soc_datetime = datetime asset.soc_udi_event_id = event_id asset.soc_in_mwh = value db.session.commit() return request_processed("Request has been processed.")
def decorated_service(*args, **kwargs): form = get_form_from_request(request) if form is None: current_app.logger.warning( "Unsupported request method for inferring resolution from request." ) return invalid_method(request.method) if not all( key in kwargs for key in [ "value_groups", "start", "duration", ] ): current_app.logger.warning("Could not infer resolution.") fields = ("values", "start", "duration") return required_info_missing(fields, "Resolution cannot be inferred.") if "generic_asset_name_groups" not in kwargs: return required_info_missing( (entity_type), "Required resolution cannot be found without asset info.", ) # Calculating (inferring) the resolution in the POSTed data inferred_resolution = ( (kwargs["start"] + kwargs["duration"]) - kwargs["start"] ) / len(kwargs["value_groups"][0]) # Finding the required resolution for assets affected in this request required_resolution = None last_asset = None for asset_group in kwargs["generic_asset_name_groups"]: for asset_descriptor in asset_group: # Getting the asset generic_asset = get_generic_asset(asset_descriptor, entity_type) if generic_asset is None: return unrecognized_asset( f"Failed to look up asset by {asset_descriptor}" ) # Complain if assets don't all require the same resolution if ( required_resolution is not None and generic_asset.event_resolution != required_resolution ): return conflicting_resolutions( f"Cannot send data for both {generic_asset} and {last_asset}." ) # Setting the resolution & remembering last looked-at asset required_resolution = generic_asset.event_resolution last_asset = generic_asset # if inferred resolution is a multiple from required_solution, we can upsample_values if inferred_resolution % required_resolution == timedelta(hours=0): for i in range(len(kwargs["value_groups"])): kwargs["value_groups"][i] = upsample_values( kwargs["value_groups"][i], from_resolution=inferred_resolution, to_resolution=required_resolution, ) inferred_resolution = required_resolution if inferred_resolution != required_resolution: current_app.logger.warning( f"Resolution {inferred_resolution} is not accepted. We require {required_resolution}." ) return unapplicable_resolution( isodate.duration_isoformat(required_resolution) ) else: kwargs["resolution"] = inferred_resolution return fn(*args, **kwargs)