def test_get_device_message_wrong_event_id(client, message): asset = Asset.query.filter(Asset.name == "Test battery").one_or_none() message["event"] = message["event"] % (asset.owner_id, asset.id) auth_token = get_auth_token(client, "*****@*****.**", "testtest") get_device_message_response = client.get( url_for("flexmeasures_api_v1_3.get_device_message"), query_string=message, headers={"content-type": "application/json", "Authorization": auth_token}, ) print("Server responded with:\n%s" % get_device_message_response.json) assert get_device_message_response.status_code == 400 assert get_device_message_response.json["type"] == "GetDeviceMessageResponse" assert ( get_device_message_response.json["status"] == unrecognized_event(9999, "soc")[0]["status"] )
def get_schedule(self, sensor: Sensor, job_id: str, duration: timedelta, **kwargs): """Get a schedule from FlexMeasures. .. :quickref: Schedule; Download schedule from the platform **Optional fields** - "duration" (6 hours by default; can be increased to plan further into the future) **Example response** This message contains a schedule indicating to consume at various power rates from 10am UTC onwards for a duration of 45 minutes. .. sourcecode:: json { "values": [ 2.15, 3, 2 ], "start": "2015-06-02T10:00:00+00:00", "duration": "PT45M", "unit": "MW" } :reqheader Authorization: The authentication token :reqheader Content-Type: application/json :resheader Content-Type: application/json :status 200: PROCESSED :status 400: INVALID_TIMEZONE, INVALID_DOMAIN, INVALID_UNIT, UNKNOWN_SCHEDULE, UNRECOGNIZED_CONNECTION_GROUP :status 401: UNAUTHORIZED :status 403: INVALID_SENDER :status 405: INVALID_METHOD :status 422: UNPROCESSABLE_ENTITY """ planning_horizon = min( # type: ignore duration, current_app.config.get("FLEXMEASURES_PLANNING_HORIZON")) # Look up the scheduling job connection = current_app.queues["scheduling"].connection try: # First try the scheduling queue job = Job.fetch(job_id, connection=connection) except NoSuchJobError: return unrecognized_event(job_id, "job") if job.is_finished: error_message = "A scheduling job has been processed with your job ID, but " elif job.is_failed: # Try to inform the user on why the job failed e = job.meta.get( "exception", Exception( "The job does not state why it failed. " "The worker may be missing an exception handler, " "or its exception handler is not storing the exception as job meta data." ), ) return unknown_schedule( f"Scheduling job failed with {type(e).__name__}: {e}") elif job.is_started: return unknown_schedule("Scheduling job in progress.") elif job.is_queued: return unknown_schedule("Scheduling job waiting to be processed.") elif job.is_deferred: try: preferred_job = job.dependency except NoSuchJobError: return unknown_schedule( "Scheduling job waiting for unknown job to be processed.") return unknown_schedule( f'Scheduling job waiting for {preferred_job.status} job "{preferred_job.id}" to be processed.' ) else: return unknown_schedule("Scheduling job has an unknown status.") schedule_start = job.kwargs["start"] schedule_data_source_name = "Seita" scheduler_source = DataSource.query.filter_by( name="Seita", type="scheduling script").one_or_none() if scheduler_source is None: return unknown_schedule( error_message + f'no data is known from "{schedule_data_source_name}".') power_values = sensor.search_beliefs( event_starts_after=schedule_start, event_ends_before=schedule_start + planning_horizon, source=scheduler_source, most_recent_beliefs_only=True, one_deterministic_belief_per_event=True, ) # For consumption schedules, positive values denote consumption. For the db, consumption is negative consumption_schedule = -simplify_index(power_values)["event_value"] if consumption_schedule.empty: return unknown_schedule( error_message + "the schedule was not found in the database.") # Update the planning window resolution = sensor.event_resolution start = consumption_schedule.index[0] duration = min(duration, consumption_schedule.index[-1] + resolution - start) consumption_schedule = consumption_schedule[start:start + duration - resolution] response = dict( values=consumption_schedule.tolist(), start=isodate.datetime_isoformat(start), duration=isodate.duration_isoformat(duration), unit=sensor.unit, ) d, s = request_processed() return dict(**response, **d), s
def get_device_message_response(generic_asset_name_groups, duration): unit = "MW" planning_horizon = min( 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 not in ( "battery", "one-way_evse", "two-way_evse", ): return invalid_domain( f"API version 1.3 only supports device messages 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)}." ) # Use the event_id to look up the schedule start if event_type not in ("soc", "soc-with-targets"): return unrecognized_event_type(event_type) connection = current_app.queues["scheduling"].connection try: # First try the scheduling queue job = Job.fetch(event, connection=connection) except NoSuchJobError: # Then try the most recent event_id (stored as a generic asset attribute) if event_id == sensor.generic_asset.get_attribute( "soc_udi_event_id"): schedule_start = datetime.fromisoformat( sensor.generic_asset.get_attribute("soc_datetime")) message = ( "Your UDI event is the most recent event for this device, but " ) else: return unrecognized_event(event_id, event_type) else: if job.is_finished: message = "A scheduling job has been processed based on your UDI event, but " elif job.is_failed: # Try to inform the user on why the job failed e = job.meta.get( "exception", Exception( "The job does not state why it failed. " "The worker may be missing an exception handler, " "or its exception handler is not storing the exception as job meta data." ), ) return unknown_schedule( f"Scheduling job failed with {type(e).__name__}: {e}") elif job.is_started: return unknown_schedule("Scheduling job in progress.") elif job.is_queued: return unknown_schedule( "Scheduling job waiting to be processed.") elif job.is_deferred: try: preferred_job = job.dependency except NoSuchJobError: return unknown_schedule( "Scheduling job waiting for unknown job to be processed." ) return unknown_schedule( f'Scheduling job waiting for {preferred_job.status} job "{preferred_job.id}" to be processed.' ) else: return unknown_schedule( "Scheduling job has an unknown status.") schedule_start = job.kwargs["start"] schedule_data_source_name = "Seita" scheduler_source = DataSource.query.filter_by( name="Seita", type="scheduling script").one_or_none() if scheduler_source is None: return unknown_schedule( message + f'no data is known from "{schedule_data_source_name}".') power_values = sensor.search_beliefs( event_starts_after=schedule_start, event_ends_before=schedule_start + planning_horizon, source=scheduler_source, most_recent_beliefs_only=True, one_deterministic_belief_per_event=True, ) # For consumption schedules, positive values denote consumption. For the db, consumption is negative consumption_schedule = -simplify_index(power_values)["event_value"] if consumption_schedule.empty: return unknown_schedule( message + "the schedule was not found in the database.") # Update the planning window resolution = sensor.event_resolution start = consumption_schedule.index[0] duration = min(duration, consumption_schedule.index[-1] + resolution - start) consumption_schedule = consumption_schedule[start:start + duration - resolution] value_groups.append(consumption_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"] = isodate.duration_isoformat(duration) response["unit"] = unit d, s = request_processed() return dict(**response, **d), s
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")) resolution = sensor.event_resolution # Schedule the asset try: schedule = schedule_battery( sensor, start, start + planning_horizon, resolution, soc_at_start=sensor.generic_asset.get_attribute( "soc_in_mwh"), prefer_charging_sooner=False, ) 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"] = isodate.duration_isoformat(duration) response["unit"] = unit d, s = request_processed() return dict(**response, **d), s
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.")