diff --git a/README.md b/README.md index 725c2f3..acb938e 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ We support following data sources: | Stadt Reutlingen: Fahrrad-Abstellanlagen | bike | push (csv) | `reutlingen_bike` | no | | Stadt Stuttgart | car | push (json) | `stuttgart` | yes | | Stadt Ulm | car | pull | `ulm` | yes | +| Verkehrsverbund Rhein-Neckar GmbH: P+R Parkplätze | car | pull | `vrn_p_r` | yes | | Verband Region Stuttgart: Bondorf | car | pull | `vrs_bondorf` | yes | | Verband Region Stuttgart: Kirchheim | car | pull | `vrs_kirchheim` | yes | | Verband Region Stuttgart: Neustadt | car | pull | `vrs_neustadt` | yes | diff --git a/src/parkapi_sources/converters/__init__.py b/src/parkapi_sources/converters/__init__.py index 42d70aa..4dbc438 100644 --- a/src/parkapi_sources/converters/__init__.py +++ b/src/parkapi_sources/converters/__init__.py @@ -39,5 +39,6 @@ from .reutlingen_bike import ReutlingenBikePushConverter from .stuttgart import StuttgartPushConverter from .ulm import UlmPullConverter +from .vrn_p_r import VrnParkAndRidePullConverter from .vrs import VrsBondorfPullConverter, VrsKirchheimPullConverter, VrsNeustadtPullConverter, VrsVaihingenPullConverter from .vrs_p_r import VrsParkAndRidePushConverter diff --git a/src/parkapi_sources/converters/vrn_p_r/__init__.py b/src/parkapi_sources/converters/vrn_p_r/__init__.py new file mode 100644 index 0000000..fc5a00f --- /dev/null +++ b/src/parkapi_sources/converters/vrn_p_r/__init__.py @@ -0,0 +1,6 @@ +""" +Copyright 2024 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from .converter import VrnParkAndRidePullConverter diff --git a/src/parkapi_sources/converters/vrn_p_r/converter.py b/src/parkapi_sources/converters/vrn_p_r/converter.py new file mode 100644 index 0000000..85bbb35 --- /dev/null +++ b/src/parkapi_sources/converters/vrn_p_r/converter.py @@ -0,0 +1,195 @@ +""" +Copyright 2024 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from abc import ABC + +import requests +from validataclass.exceptions import ValidationError +from validataclass.validators import AnythingValidator, DataclassValidator, ListValidator + +from parkapi_sources.converters.base_converter.pull import GeojsonInput, PullConverter +from parkapi_sources.exceptions import ImportParkingSiteException, ImportSourceException +from parkapi_sources.models import RealtimeParkingSiteInput, SourceInfo, StaticParkingSiteInput + +from .models import VrnParkAndRideFeaturesInput, VrnParkAndRideMultiguideInput, VrnParkAndRideSonahInput + + +class VrnParkAndRidePullConverter(PullConverter, ABC): + list_validator = ListValidator(AnythingValidator(allowed_types=[dict])) + geojson_validator = DataclassValidator(GeojsonInput) + vrn_p_r_feature_validator = DataclassValidator(VrnParkAndRideFeaturesInput) + vrn_multiguide_validator = DataclassValidator(VrnParkAndRideMultiguideInput) + vrn_sonah_validator = DataclassValidator(VrnParkAndRideSonahInput) + + source_info = SourceInfo( + uid='vrn_p_r', + name='Verkehrsverbund Rhein-Neckar GmbH - P+R Parkplätze', + public_url='https://www.vrn.de/opendata/datasets/pr-parkplaetze-mit-vrn-parksensorik', + timezone='Europe/Berlin', + has_realtime_data=True, + ) + + def _get_feature_inputs(self) -> tuple[list[VrnParkAndRideFeaturesInput], list[ImportParkingSiteException]]: + feature_inputs: list[VrnParkAndRideFeaturesInput] = [] + import_parking_site_exceptions: list[ImportParkingSiteException] = [] + + response = requests.get( + url='https://spatial.vrn.de/data/rest/services/Hosted/p_r_parkapi_static/FeatureServer/5/query?where=objectid%3E0&outFields=*&returnGeometry=true&f=geojson', + timeout=30, + ) + response_data = response.json() + + try: + geojson_input = self.geojson_validator.validate(response_data) + except ValidationError as e: + raise ImportSourceException( + source_uid=self.source_info.uid, + message=f'Invalid Input at source {self.source_info.uid}: {e.to_dict()}, data: {response_data}', + ) from e + + for feature_dict in geojson_input.features: + if self._should_ignore_dataset(feature_dict): + continue + + try: + feature_input = self.vrn_p_r_feature_validator.validate(feature_dict) + except ValidationError as e: + import_parking_site_exceptions.append( + ImportParkingSiteException( + source_uid=self.source_info.uid, + parking_site_uid=feature_dict.get('properties', {}).get('id'), + message=f'Invalid data at uid {feature_dict.get("properties", {}).get("id")}: ' + f'{e.to_dict()}, data: {feature_dict}', + ), + ) + continue + + feature_inputs.append(feature_input) + + return feature_inputs, import_parking_site_exceptions + + def _should_ignore_dataset(self, feature_dict: dict) -> bool: + if self.config_helper.get('PARK_API_VRN_P_R_IGNORE_MISSING_CAPACITIES'): + return feature_dict.get('properties', {}).get('capacity') is None + + return False + + def get_static_parking_sites(self) -> tuple[list[StaticParkingSiteInput], list[ImportParkingSiteException]]: + feature_inputs, import_parking_site_exceptions = self._get_feature_inputs() + static_parking_site_inputs: list[StaticParkingSiteInput] = [] + + realtime_vrn_p_r_inputs, import_realtime_parking_site_exceptions = self._get_raw_realtime_parking_sites() + import_parking_site_exceptions += import_realtime_parking_site_exceptions + + static_parking_site_inputs_by_uid: dict[str, StaticParkingSiteInput] = {} + for feature_input in feature_inputs: + static_parking_site_inputs_by_uid[str(feature_input.properties.vrn_sensor_id)] = feature_input.to_static_parking_site_input() + + for realtime_vrn_p_r_input in realtime_vrn_p_r_inputs: + # If the realtime group_uid is not known in our static data: ignore the static data + parking_site_uid = str(realtime_vrn_p_r_input.uid) + if parking_site_uid in static_parking_site_inputs_by_uid: + # Update static data with only parking places having realtime data + static_parking_site_inputs.append(static_parking_site_inputs_by_uid[parking_site_uid]) + + return static_parking_site_inputs, import_parking_site_exceptions + + def _get_raw_realtime_parking_sites(self) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]: + realtime_parking_site_inputs: list[RealtimeParkingSiteInput] = [] + import_parking_site_exceptions: list[ImportParkingSiteException] = [] + + multiguide_response_data = self._request_vrn_multiguide() + sonah_response_data = self._request_vrn_sonah() + + try: + multiguide_parking_site_dicts = self.list_validator.validate(multiguide_response_data) + except ValidationError as e: + raise ImportSourceException( + source_uid=self.source_info.uid, + message=f'Invalid Input at source {self.source_info.uid}: {e.to_dict()}, data: {multiguide_response_data}', + ) from e + + for multiguide_parking_site_dict in multiguide_parking_site_dicts: + try: + realtime_parking_site_inputs.append( + self.vrn_multiguide_validator.validate(multiguide_parking_site_dict).to_realtime_parking_site_input() + ) + except ValidationError as e: + import_parking_site_exceptions.append( + ImportParkingSiteException( + source_uid=self.source_info.uid, + parking_site_uid=multiguide_parking_site_dict.get('Id'), + message=f'validation error for {multiguide_parking_site_dict}: {e.to_dict()}', + ), + ) + + try: + sonah_parking_site_dicts = self.list_validator.validate(sonah_response_data) + except ValidationError as e: + raise ImportSourceException( + source_uid=self.source_info.uid, + message=f'Invalid Input at source {self.source_info.uid}: {e.to_dict()}, data: {sonah_response_data}', + ) from e + + for sonah_parking_site_dict in sonah_parking_site_dicts: + try: + realtime_parking_site_inputs.append( + self.vrn_sonah_validator.validate(sonah_parking_site_dict).to_realtime_parking_site_input() + ) + except ValidationError as e: + import_parking_site_exceptions.append( + ImportParkingSiteException( + source_uid=self.source_info.uid, + parking_site_uid=sonah_parking_site_dict.get('LocationID'), + message=f'validation error for {sonah_parking_site_dict}: {e.to_dict()}', + ), + ) + + return realtime_parking_site_inputs, import_parking_site_exceptions + + def get_realtime_parking_sites(self) -> tuple[list[RealtimeParkingSiteInput], list[ImportParkingSiteException]]: + static_parking_site_inputs, import_parking_site_exceptions = self.get_static_parking_sites() + realtime_parking_site_inputs: list[RealtimeParkingSiteInput] = [] + + realtime_vrn_p_r_inputs, import_parking_site_exceptions = self._get_raw_realtime_parking_sites() + static_parking_site_inputs_by_uid: dict[str, StaticParkingSiteInput] = {} + + for static_parking_site_input in static_parking_site_inputs: + static_parking_site_inputs_by_uid[static_parking_site_input.group_uid] = static_parking_site_input + + for realtime_vrn_p_r_input in realtime_vrn_p_r_inputs: + # If the realtime group_uid is not known in our static data: ignore the realtime data + parking_site_uid = str(realtime_vrn_p_r_input.uid) + if parking_site_uid in static_parking_site_inputs_by_uid: + # Update realtime data with only parking places having static data + realtime_vrn_p_r_input.uid = static_parking_site_inputs_by_uid[parking_site_uid].uid + realtime_parking_site_inputs.append(realtime_vrn_p_r_input) + + return realtime_parking_site_inputs, import_parking_site_exceptions + + def _request_vrn_multiguide(self) -> list[dict]: + response = requests.get( + url='https://vrn.multiguide.info/api/area', + auth=( + self.config_helper.get('PARK_API_VRN_P_R_MULTIGUIDE_API_USERNAME'), + self.config_helper.get('PARK_API_VRN_P_R_MULTIGUIDE_API_PASSWORD'), + ), + timeout=30, + ) + + return response.json() + + def _request_vrn_sonah(self) -> list[dict]: + headers: dict[str, str] = { + 'Accept': 'application/json', + 'Authorization': self.config_helper.get('PARK_API_VRN_P_R_SONAH_API_BEARER_TOKEN'), + } + response = requests.get( + url='https://vrnm.dyndns.sonah.xyz/api/v3/rest/json/locations', + headers=headers, + timeout=30, + ) + + return response.json() diff --git a/src/parkapi_sources/converters/vrn_p_r/models.py b/src/parkapi_sources/converters/vrn_p_r/models.py new file mode 100644 index 0000000..a22848f --- /dev/null +++ b/src/parkapi_sources/converters/vrn_p_r/models.py @@ -0,0 +1,161 @@ +""" +Copyright 2024 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from datetime import date, datetime, timezone +from decimal import Decimal +from enum import Enum + +from validataclass.dataclasses import DefaultUnset, ValidataclassMixin, validataclass +from validataclass.exceptions import ValidationError +from validataclass.helpers import OptionalUnset, UnsetValue +from validataclass.validators import ( + DataclassValidator, + EnumValidator, + IntegerValidator, + NoneToUnsetValue, + NumericValidator, + StringValidator, + UrlValidator, +) + +from parkapi_sources.converters.base_converter.pull import GeojsonFeatureGeometryInput +from parkapi_sources.models import RealtimeParkingSiteInput, StaticParkingSiteInput +from parkapi_sources.models.enums import ParkingSiteType, PurposeType +from parkapi_sources.validators import MappedBooleanValidator, ParsedDateValidator, TimestampDateTimeValidator + + +class VrnParkAndRideType(Enum): + CAR_PARK = 'Parkhaus' + OFF_STREET_PARKING_GROUND = 'Parkplatz' + + def to_parking_site_type(self) -> ParkingSiteType: + return { + self.CAR_PARK: ParkingSiteType.CAR_PARK, + self.OFF_STREET_PARKING_GROUND: ParkingSiteType.OFF_STREET_PARKING_GROUND, + }.get(self) + + +@validataclass +class VrnParkAndRidePropertiesOpeningHoursInput: + string: str = StringValidator(min_length=1, max_length=256) + langIso639: str = StringValidator(min_length=1, max_length=256) + + +@validataclass +class VrnParkAndRidePropertiesInput(ValidataclassMixin): + original_uid: str = StringValidator(min_length=1, max_length=256) + name: str = StringValidator(min_length=0, max_length=256) + type: OptionalUnset[VrnParkAndRideType] = NoneToUnsetValue(EnumValidator(VrnParkAndRideType)), DefaultUnset + public_url: OptionalUnset[str] = NoneToUnsetValue(UrlValidator(max_length=4096)), DefaultUnset + photo_url: OptionalUnset[str] = NoneToUnsetValue(UrlValidator(max_length=4096)), DefaultUnset + lat: OptionalUnset[Decimal] = NumericValidator() + lon: OptionalUnset[Decimal] = NumericValidator() + address: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + description: OptionalUnset[str] = NoneToUnsetValue(StringValidator(max_length=512)), DefaultUnset + operator_name: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + capacity: int = IntegerValidator(min_value=0) + capacity_charging: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_family: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_woman: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_bus: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_truck: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_carsharing: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + capacity_disabled: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + max_height: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + has_realtime_data: OptionalUnset[bool] = NoneToUnsetValue(MappedBooleanValidator(mapping={'ja': True, 'nein': False})), DefaultUnset + vrn_sensor_id: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + realtime_opening_status: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + created_at: OptionalUnset[date] = NoneToUnsetValue(ParsedDateValidator(date_format='%Y-%m-%d')), DefaultUnset + static_data_updated_at: OptionalUnset[datetime] = ( + NoneToUnsetValue(TimestampDateTimeValidator(allow_strings=True, divisor=1000)), + DefaultUnset, + ) + has_lighting: OptionalUnset[bool] = NoneToUnsetValue(MappedBooleanValidator(mapping={'ja': True, 'nein': False})), DefaultUnset + has_fee: OptionalUnset[bool] = NoneToUnsetValue(MappedBooleanValidator(mapping={'ja': True, 'nein': False})), DefaultUnset + is_covered: OptionalUnset[bool] = NoneToUnsetValue(MappedBooleanValidator(mapping={'ja': True, 'nein': False})), DefaultUnset + related_location: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + opening_hours: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + park_and_ride_type: OptionalUnset[str] = NoneToUnsetValue(StringValidator(min_length=0, max_length=256)), DefaultUnset + max_stay: OptionalUnset[int] = NoneToUnsetValue(IntegerValidator(min_value=0)), DefaultUnset + fee_description: OptionalUnset[str] = NoneToUnsetValue(StringValidator(max_length=512)), DefaultUnset + + +@validataclass +class VrnParkAndRideFeaturesInput: + geometry: GeojsonFeatureGeometryInput = DataclassValidator(GeojsonFeatureGeometryInput) + properties: VrnParkAndRidePropertiesInput = DataclassValidator(VrnParkAndRidePropertiesInput) + + def to_static_parking_site_input(self) -> StaticParkingSiteInput: + return StaticParkingSiteInput( + uid=str(self.properties.original_uid), + group_uid=str(self.properties.vrn_sensor_id), + name=self.properties.name if self.properties.name != '' else 'Fahrrad-Abstellanlagen', + type=self.properties.type.to_parking_site_type(), + description=self.properties.description, + capacity=self.properties.capacity, + has_realtime_data=self.properties.has_realtime_data, + has_lighting=self.properties.has_lighting, + is_covered=self.properties.is_covered, + related_location=self.properties.related_location, + operator_name=self.properties.operator_name, + max_height=self.properties.max_height, + has_fee=self.properties.has_fee, + fee_description=self.properties.fee_description, + capacity_charging=self.properties.capacity_charging, + lat=self.geometry.coordinates[1], + lon=self.geometry.coordinates[0], + static_data_updated_at=datetime.now(timezone.utc) + if self.properties.static_data_updated_at is UnsetValue + else self.properties.static_data_updated_at, + purpose=PurposeType.CAR, + opening_hours='24/7' + if 'Mo-So: 24 Stunden' in self.properties.opening_hours or 'Mo-So: Kostenlos' in self.properties.opening_hours + else UnsetValue, + ) + + +@validataclass +class VrnParkAndRideMultiguideInput: + Name: str = StringValidator() + Id: int = IntegerValidator(allow_strings=True) + Constructed: int = IntegerValidator(allow_strings=True) + Available: int = IntegerValidator(allow_strings=True) + Free: int = IntegerValidator(allow_strings=True) + Occupied: int = IntegerValidator(allow_strings=True) + Reserved: int = IntegerValidator(allow_strings=True) + Defect: int = IntegerValidator(allow_strings=True) + + def __post_init__(self): + if self.Constructed < self.Occupied: + raise ValidationError(reason='More occupied sites than capacity') + + def to_realtime_parking_site_input(self) -> RealtimeParkingSiteInput: + return RealtimeParkingSiteInput( + uid=str(self.Id), + realtime_capacity=self.Constructed, + realtime_free_capacity=self.Free, + realtime_data_updated_at=datetime.now(timezone.utc), + ) + + +@validataclass +class VrnParkAndRideSonahInput: + Name: str = StringValidator() + LocationID: int = IntegerValidator() + TotalParking: int = NumericValidator() + FreeParking: int = NumericValidator() + OccupiedParking: int = NumericValidator() + + def __post_init__(self): + if self.TotalParking < self.OccupiedParking: + raise ValidationError(reason='More occupied sites than capacity') + + def to_realtime_parking_site_input(self) -> RealtimeParkingSiteInput: + return RealtimeParkingSiteInput( + uid=str(self.LocationID), + realtime_capacity=int(self.TotalParking), + realtime_free_capacity=int(self.FreeParking), + realtime_data_updated_at=datetime.now(timezone.utc), + ) diff --git a/src/parkapi_sources/parkapi_sources.py b/src/parkapi_sources/parkapi_sources.py index 86088ca..e073c36 100644 --- a/src/parkapi_sources/parkapi_sources.py +++ b/src/parkapi_sources/parkapi_sources.py @@ -43,6 +43,7 @@ ReutlingenPushConverter, StuttgartPushConverter, UlmPullConverter, + VrnParkAndRidePullConverter, VrsBondorfPullConverter, VrsKirchheimPullConverter, VrsNeustadtPullConverter, @@ -93,6 +94,7 @@ class ParkAPISources: ReutlingenBikePushConverter, StuttgartPushConverter, UlmPullConverter, + VrnParkAndRidePullConverter, VrsBondorfPullConverter, VrsKirchheimPullConverter, VrsNeustadtPullConverter, diff --git a/tests/converters/data/vrn_p_r.json b/tests/converters/data/vrn_p_r.json new file mode 100644 index 0000000..1c87f2f --- /dev/null +++ b/tests/converters/data/vrn_p_r.json @@ -0,0 +1,708 @@ +{ + "features": [ + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.668479999795732, + 49.34205000040286 + ], + "crs": null + }, + "id": 2871, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Customers Only: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 3, + "created_at": null, + "lon": 8.66848, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 20, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/153768/st-ilgen-sandhausen-leimen-bw-germany", + "related_location": null, + "original_uid": "2-2-2-153768", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.34205, + "address": "Leimbachstraße,69181,Leimen", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 678, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Sonah", + "name": "P+R St. Ilgen-Sandhausen, Leimbachstr.", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2871, + "objectid": 2871, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.613580000505587, + 49.70168999996302 + ], + "crs": null + }, + "id": 2872, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 2, + "created_at": null, + "lon": 8.61358, + "capacity_charging": 2, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 37, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/178307/bensheim-auerbach-bensheim-he-germany", + "related_location": null, + "original_uid": "2-2-2-178307", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.70169, + "address": "Wilhelmstraße 1,64625,Bensheim", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": null, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "MVVEnergie SmartCities", + "name": "P+R Bensheim-Auerbach", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2872, + "objectid": 2872, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.305570000463035, + 49.37974999964875 + ], + "crs": null + }, + "id": 2873, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 2, + "created_at": null, + "lon": 8.30557, + "capacity_charging": 2, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 140, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/178316/am-bahnhof-bhl-iggelheim-rp-germany", + "related_location": null, + "original_uid": "2-2-2-178316", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.37975, + "address": "Am Bahnhofspl. 1,67459,Böhl-Iggelheim", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 498, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Multiguide", + "name": "P+R Böhl-Iggelheim, Bf Süd", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2873, + "objectid": 2873, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.305450000503294, + 49.380349999967336 + ], + "crs": null + }, + "id": 2874, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 0, + "created_at": null, + "lon": 8.30545, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 61, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/178317/bahnhofsvorplatz-nord-bhl-iggelheim-rp-germany", + "related_location": null, + "original_uid": "2-2-2-178317", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.38035, + "address": "Berliner Str. 2A,67459,Böhl-Iggelheim", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 499, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Multiguide", + "name": "P+R Böhl-Iggelheim, Bf Nord", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": null, + "objectid": 2874, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.253149999448969, + 49.37224999976917 + ], + "crs": null + }, + "id": 2875, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 2, + "created_at": null, + "lon": 8.25315, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 190, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/178381/p-r-haloch-haloch-rp-germany", + "related_location": null, + "original_uid": "2-2-2-178381", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.37225, + "address": "Lehmgrubenweg 1,67454,Haßloch", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 718, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Multiguide", + "name": "P+R Haßloch, Bf Nord", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2875, + "objectid": 2875, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.25578000008815, + 49.372149999782685 + ], + "crs": null + }, + "id": 2876, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 2, + "created_at": null, + "lon": 8.25578, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 114, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/178382/bahnhofsvorplatz-1", + "related_location": null, + "original_uid": "2-2-2-178382", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.37215, + "address": "Bahnhofsvorplatz 1,67454,Haßloch", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 191, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Multiguide", + "name": "P+R Haßloch, Bf Süd", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": null, + "objectid": 2876, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.789859999874407, + 49.39428000018094 + ], + "crs": null + }, + "id": 2877, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 1, + "created_at": null, + "lon": 8.78986, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 45, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/178562/neckargemuend-neckargemnd-bw-germany", + "related_location": null, + "original_uid": "2-2-2-178562", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.39428, + "address": "Bahnhofstraße 35,69151,Neckargemünd", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 676, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Sonah", + "name": "P+R Neckargemünd", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2877, + "objectid": 2877, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.669239999763356, + 49.34173999989347 + ], + "crs": null + }, + "id": 2878, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Customers Only: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 6, + "created_at": null, + "lon": 8.66924, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 62, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/178654/st-ilgen-sandhausen-leimen-bw-germany", + "related_location": null, + "original_uid": "2-2-2-178654", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.34174, + "address": "Bahnhofstraße 62,69181,Leimen", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 679, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Sonah", + "name": "P+R St. Ilgen-Sandhausen, Bf Ost", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2878, + "objectid": 2878, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.780650000569803, + 49.64836999997582 + ], + "crs": null + }, + "id": 2879, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 2, + "created_at": null, + "lon": 8.78065, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 32, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/179058/frth-odw-bf-p1-frth-he-germany", + "related_location": null, + "original_uid": "2-2-2-179058", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/lot_img/179058/532b13142edc49bfb0a3ebab49035ce9.jpg", + "modified_at": null, + "lat": 49.64837, + "address": "Bahnhofstraße 5,64658,Fürth", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 155, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Multiguide", + "name": "P+R Fürth (Odw.) Bf P1", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2879, + "objectid": 2879, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.753530000273994, + 49.60971999995243 + ], + "crs": null + }, + "id": 2880, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 2, + "created_at": null, + "lon": 8.75353, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 28, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/179073/rimbach-zotzenbach-bf-p1-mrlenbach-he-germany", + "related_location": null, + "original_uid": "2-2-2-179073", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/logo_ivm.png", + "modified_at": null, + "lat": 49.60972, + "address": "Grüne Au,69509,Mörlenbach", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": null, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "MVVEnergie SmartCities", + "name": "P+R Rimbach-Zotzenbach Bf P1", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2880, + "objectid": 2880, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.537719999545239, + 49.31639999989083 + ], + "crs": null + }, + "id": 2881, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 4, + "created_at": null, + "lon": 8.53772, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 288, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/182421/hockenheim-nord-hockenheim-bw-germany", + "related_location": null, + "original_uid": "2-2-2-182421", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.3164, + "address": "Eisenbahnstraße 2,68766,Hockenheim", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 721, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Multiguide", + "name": "P+R Hockenheim Nord", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2881, + "objectid": 2881, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.537150000001558, + 49.315610000181195 + ], + "crs": null + }, + "id": 2882, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 4, + "created_at": null, + "lon": 8.53715, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 87, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/182424/hockenheim-sd-hockenheim-bw-germany", + "related_location": null, + "original_uid": "2-2-2-182424", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.31561, + "address": "Lußheimer Str. 21,68766,Hockenheim", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 720, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Multiguide", + "name": "P+R Hockenheim Süd", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2882, + "objectid": 2882, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.664919999451357, + 49.29114000026921 + ], + "crs": null + }, + "id": 2883, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Mo-So: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": "ja", + "capacity_woman": 0, + "capacity_disabled": 10, + "created_at": null, + "lon": 8.66492, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkhaus", + "capacity": 263, + "capacity_bus": null, + "is_covered": "ja", + "public_url": "https://www.parkme.com/lot/1222006/p-r-parkhaus-am-bahnhof-wiesloch-walldorf-walldorf-bw-germany", + "related_location": null, + "original_uid": "2-2-2-1222006", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.29114, + "address": "Staatsbahnhofstraße 14, 69168 Wiesloch", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 951, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "ja", + "operator_name": "Multiguide", + "name": "P+R Parkhaus am Bahnhof Wiesloch-Walldorf", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2883, + "objectid": 2883, + "capacity_carsharing": null + } + }, + { + "geometry": { + "type": "Point", + "coordinates": [ + 8.668783470525582, + 49.343135926233316 + ], + "crs": null + }, + "id": 2884, + "type": "Feature", + "properties": { + "fee_description": "[{'string': 'Customers Only: Kostenlos', 'langIso639': 'de'}]", + "max_stay": null, + "capacity_family": null, + "has_lighting": null, + "capacity_woman": 0, + "capacity_disabled": 3, + "created_at": null, + "lon": 8.66878347, + "capacity_charging": 0, + "max_height": null, + "capacity_truck": null, + "type": "Parkplatz", + "capacity": 52, + "capacity_bus": null, + "is_covered": "nein", + "public_url": "https://www.parkme.com/lot/153768/st-ilgen-sandhausen-leimen-bw-germany", + "related_location": null, + "original_uid": "2-2-2-153768", + "supervision_type": null, + "photo_url": "https://d13esfgglb25od.cloudfront.net/vrn.jpg", + "modified_at": null, + "lat": 49.34313593, + "address": "Am Güterbahnhof,69181,Leimen", + "static_data_updated_at": 1721389723000, + "vrn_sensor_id": 664, + "park_and_ride_type": "ja", + "realtime_opening_status": "unbekannt", + "has_fee": "nein", + "operator_name": "Sonah", + "name": "P+R St. Ilgen-Sandhausen, Am Güterbahnhof", + "opening_hours": "[{'string': 'Mo-So: 24 Stunden', 'langIso639': 'de'}]", + "realtime_data_updated_at": null, + "has_realtime_data": "ja", + "source_id": 2884, + "objectid": 2884, + "capacity_carsharing": null + } + } + ], + "type": "FeatureCollection", + "properties": { + "exceededTransferLimit": false + } +} \ No newline at end of file diff --git a/tests/converters/data/vrn_p_r_multiguide.json b/tests/converters/data/vrn_p_r_multiguide.json new file mode 100644 index 0000000..d6c98d2 --- /dev/null +++ b/tests/converters/data/vrn_p_r_multiguide.json @@ -0,0 +1,611 @@ +[ + { + "Id": 108, + "IdParent": -1, + "Name": "VRN", + "Typ": "CarParkGroup", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 782, + "Available": 324, + "DisplayValue": 324, + "Free": 324, + "Occupied": 456, + "Reserved": 0, + "Defect": 2, + "OccupiedPercent": 58.567774936061376, + "Precount": 0, + "Exceeded": 0, + "RealFree": 324, + "RealOccupied": 456, + "RealOccupiedPercent": 41.687979539641937, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 152, + "IdParent": 108, + "Name": "P+R Fürth", + "Typ": "CarPark", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 32, + "Available": 15, + "DisplayValue": 15, + "Free": 15, + "Occupied": 17, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 53.125, + "Precount": 0, + "Exceeded": 0, + "RealFree": 15, + "RealOccupied": 17, + "RealOccupiedPercent": 46.875, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 189, + "IdParent": 108, + "Name": "P+R Haßloch", + "Typ": "CarPark", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 304, + "Available": 122, + "DisplayValue": 122, + "Free": 122, + "Occupied": 181, + "Reserved": 0, + "Defect": 1, + "OccupiedPercent": 59.868421052631575, + "Precount": 0, + "Exceeded": 0, + "RealFree": 122, + "RealOccupied": 181, + "RealOccupiedPercent": 40.460526315789465, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 496, + "IdParent": 108, + "Name": "P+R Böhl-Iggelheim", + "Typ": "CarPark", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 217, + "Available": 67, + "DisplayValue": 67, + "Free": 67, + "Occupied": 150, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 69.124423963133637, + "Precount": 0, + "Exceeded": 0, + "RealFree": 67, + "RealOccupied": 150, + "RealOccupiedPercent": 30.875576036866363, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 500, + "IdParent": 108, + "Name": "P+R Hockenheim", + "Typ": "CarPark", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 229, + "Available": 120, + "DisplayValue": 120, + "Free": 120, + "Occupied": 108, + "Reserved": 0, + "Defect": 1, + "OccupiedPercent": 47.598253275109172, + "Precount": 0, + "Exceeded": 0, + "RealFree": 120, + "RealOccupied": 108, + "RealOccupiedPercent": 52.838427947598255, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 951, + "IdParent": 108, + "Name": "P+R Wiesloch", + "Typ": "CarPark", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 0, + "Available": 0, + "DisplayValue": 0, + "Free": 0, + "Occupied": 0, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 0.0, + "Precount": 0, + "Exceeded": 0, + "RealFree": 0, + "RealOccupied": 0, + "RealOccupiedPercent": 0.0, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 153, + "IdParent": 152, + "Name": "P_R Bahnhofstr. ", + "Typ": "AreaGroup", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 32, + "Available": 15, + "DisplayValue": 15, + "Free": 15, + "Occupied": 17, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 53.125, + "Precount": 0, + "Exceeded": 0, + "RealFree": 15, + "RealOccupied": 17, + "RealOccupiedPercent": 46.875, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 190, + "IdParent": 189, + "Name": "P_R Haßloch", + "Typ": "AreaGroup", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 304, + "Available": 122, + "DisplayValue": 122, + "Free": 122, + "Occupied": 181, + "Reserved": 0, + "Defect": 1, + "OccupiedPercent": 59.868421052631575, + "Precount": 0, + "Exceeded": 0, + "RealFree": 122, + "RealOccupied": 181, + "RealOccupiedPercent": 40.460526315789465, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 497, + "IdParent": 496, + "Name": "P_R Böhl-Iggelheim", + "Typ": "AreaGroup", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 217, + "Available": 67, + "DisplayValue": 67, + "Free": 67, + "Occupied": 150, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 69.124423963133637, + "Precount": 0, + "Exceeded": 0, + "RealFree": 67, + "RealOccupied": 150, + "RealOccupiedPercent": 30.875576036866363, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 719, + "IdParent": 500, + "Name": "P_R Hockenheim", + "Typ": "AreaGroup", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 229, + "Available": 120, + "DisplayValue": 120, + "Free": 120, + "Occupied": 108, + "Reserved": 0, + "Defect": 1, + "OccupiedPercent": 47.598253275109172, + "Precount": 0, + "Exceeded": 0, + "RealFree": 120, + "RealOccupied": 108, + "RealOccupiedPercent": 52.838427947598255, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 952, + "IdParent": 951, + "Name": "PH Ebene 0", + "Typ": "AreaGroup", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 134, + "Available": 0, + "DisplayValue": 0, + "Free": 0, + "Occupied": 0, + "Reserved": 0, + "Defect": 134, + "OccupiedPercent": 100.0, + "Precount": 0, + "Exceeded": 0, + "RealFree": 0, + "RealOccupied": 0, + "RealOccupiedPercent": 100.0, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 953, + "IdParent": 951, + "Name": "PH Ebene 1", + "Typ": "AreaGroup", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 130, + "Available": 0, + "DisplayValue": 0, + "Free": 0, + "Occupied": 0, + "Reserved": 0, + "Defect": 130, + "OccupiedPercent": 100.0, + "Precount": 0, + "Exceeded": 0, + "RealFree": 0, + "RealOccupied": 0, + "RealOccupiedPercent": 100.0, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 954, + "IdParent": 952, + "Name": "A001", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 134, + "Available": 0, + "DisplayValue": 0, + "Free": 0, + "Occupied": 0, + "Reserved": 0, + "Defect": 134, + "OccupiedPercent": 100.0, + "Precount": 0, + "Exceeded": 0, + "RealFree": 0, + "RealOccupied": 0, + "RealOccupiedPercent": 100.0, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 955, + "IdParent": 953, + "Name": "A101", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 130, + "Available": 0, + "DisplayValue": 0, + "Free": 0, + "Occupied": 0, + "Reserved": 0, + "Defect": 130, + "OccupiedPercent": 100.0, + "Precount": 0, + "Exceeded": 0, + "RealFree": 0, + "RealOccupied": 0, + "RealOccupiedPercent": 100.0, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 191, + "IdParent": 190, + "Name": "P_R_h Süd", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 114, + "Available": 23, + "DisplayValue": 23, + "Free": 23, + "Occupied": 90, + "Reserved": 0, + "Defect": 1, + "OccupiedPercent": 79.824561403508767, + "Precount": 0, + "Exceeded": 0, + "RealFree": 23, + "RealOccupied": 90, + "RealOccupiedPercent": 21.05263157894737, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 720, + "IdParent": 719, + "Name": " P_R hck Süd", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 52, + "Available": 40, + "DisplayValue": 40, + "Free": 40, + "Occupied": 12, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 23.076923076923066, + "Precount": 0, + "Exceeded": 0, + "RealFree": 40, + "RealOccupied": 12, + "RealOccupiedPercent": 76.92307692307692, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 721, + "IdParent": 719, + "Name": " P_R hck Nord", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 177, + "Available": 80, + "DisplayValue": 80, + "Free": 80, + "Occupied": 96, + "Reserved": 0, + "Defect": 1, + "OccupiedPercent": 54.802259887005647, + "Precount": 0, + "Exceeded": 0, + "RealFree": 80, + "RealOccupied": 96, + "RealOccupiedPercent": 45.762711864406782, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 498, + "IdParent": 497, + "Name": "P_R_bh Süd", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 156, + "Available": 26, + "DisplayValue": 26, + "Free": 26, + "Occupied": 130, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 83.333333333333343, + "Precount": 0, + "Exceeded": 0, + "RealFree": 26, + "RealOccupied": 130, + "RealOccupiedPercent": 16.666666666666657, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 718, + "IdParent": 190, + "Name": "P_R_h Nord", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 190, + "Available": 99, + "DisplayValue": 99, + "Free": 99, + "Occupied": 91, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 47.894736842105267, + "Precount": 0, + "Exceeded": 0, + "RealFree": 99, + "RealOccupied": 91, + "RealOccupiedPercent": 52.10526315789474, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 499, + "IdParent": 497, + "Name": "P_R_bh Nord", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 61, + "Available": 41, + "DisplayValue": 41, + "Free": 41, + "Occupied": 20, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 32.786885245901644, + "Precount": 0, + "Exceeded": 0, + "RealFree": 41, + "RealOccupied": 20, + "RealOccupiedPercent": 67.21311475409837, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + }, + { + "Id": 155, + "IdParent": 153, + "Name": "P_R_b", + "Typ": "Area", + "Mode": "Automatic", + "Buffer": 0, + "Hysteresis": 0, + "Constructed": 32, + "Available": 15, + "DisplayValue": 15, + "Free": 15, + "Occupied": 17, + "Reserved": 0, + "Defect": 0, + "OccupiedPercent": 53.125, + "Precount": 0, + "Exceeded": 0, + "RealFree": 15, + "RealOccupied": 17, + "RealOccupiedPercent": 46.875, + "Comment": null, + "Location_Name": null, + "Location_Street": null, + "Location_City": null, + "Location_GPS_Latitude": 0.0, + "Location_GPS_Longitude": 0.0, + "ValuesForCustomerGroup": null + } +] \ No newline at end of file diff --git a/tests/converters/data/vrn_p_r_sonah.json b/tests/converters/data/vrn_p_r_sonah.json new file mode 100644 index 0000000..34a6910 --- /dev/null +++ b/tests/converters/data/vrn_p_r_sonah.json @@ -0,0 +1,305 @@ +[ + { + "LocationID": 664, + "Name": "Am Güterbahnhof", + "Type": "STATIC", + "FreeParking": 39.0, + "OccupiedParking": 1.0, + "TotalParking": 40.0, + "ParentLocations": [ + "api/v3/rest/json/locations?filter=677&exclude=SubLocations&select=[0]" + ], + "Areas": [ + "api/v3/rest/json/areas?filter=3897&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3898&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3899&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3900&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3901&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3902&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3903&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3904&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3905&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3906&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3907&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3908&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3909&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3910&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3911&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3912&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3913&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3914&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3915&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3916&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3917&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3918&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3919&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3920&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3921&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3922&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3923&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3924&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3925&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3926&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3927&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3928&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3929&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3930&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3931&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3932&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3933&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3934&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3935&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3936&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3937&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3938&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3939&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3940&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3941&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3942&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3943&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3944&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3945&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3946&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3947&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3948&exclude=Location&select=[0]" + ], + "SubLocations": [], + "Positions": { + "Center": { + "Lat": 49.34319583651544, + "Long": 8.668797095979574 + }, + "Navigation": { + "Lat": null, + "Long": null + } + }, + "Geometries": [] + }, + { + "LocationID": 676, + "Name": "Neckargemünd", + "Type": "STATIC", + "FreeParking": 15.0, + "OccupiedParking": 28.0, + "TotalParking": 43.0, + "ParentLocations": [], + "Areas": [ + "api/v3/rest/json/areas?filter=3949&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3950&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3951&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3952&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3953&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3954&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3955&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3956&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3957&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3958&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3959&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3960&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3961&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3962&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3963&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3964&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3965&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3966&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3967&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3968&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3969&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3970&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3971&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3972&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3973&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3974&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3975&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3976&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3977&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3978&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3979&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3980&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3981&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3982&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3983&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3984&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3985&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3986&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3987&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3988&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3989&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3990&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4465&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4466&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4467&exclude=Location&select=[0]" + ], + "SubLocations": [], + "Positions": { + "Center": { + "Lat": 49.39428030857759, + "Long": 8.789651524948898 + }, + "Navigation": { + "Lat": null, + "Long": null + } + }, + "Geometries": [] + }, + { + "LocationID": 677, + "Name": "St. Ilgen - Sandhausen", + "Type": "GROUP", + "FreeParking": 108.0, + "OccupiedParking": 14.0, + "TotalParking": 122.0, + "ParentLocations": [], + "Areas": [], + "SubLocations": [ + "api/v3/rest/json/locations?filter=664&exclude=ParentLocations&select=[0]", + "api/v3/rest/json/locations?filter=678&exclude=ParentLocations&select=[0]", + "api/v3/rest/json/locations?filter=679&exclude=ParentLocations&select=[0]" + ], + "Positions": { + "Center": { + "Lat": null, + "Long": null + }, + "Navigation": { + "Lat": null, + "Long": null + } + }, + "Geometries": [] + }, + { + "LocationID": 678, + "Name": "Leimbachstraße", + "Type": "STATIC", + "FreeParking": 13.0, + "OccupiedParking": 7.0, + "TotalParking": 20.0, + "ParentLocations": [ + "api/v3/rest/json/locations?filter=677&exclude=SubLocations&select=[0]" + ], + "Areas": [ + "api/v3/rest/json/areas?filter=3991&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3992&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3993&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3994&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3995&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3996&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3997&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3998&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=3999&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4000&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4001&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4002&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4003&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4004&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4005&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4006&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4007&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4008&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4009&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4010&exclude=Location&select=[0]" + ], + "SubLocations": [], + "Positions": { + "Center": { + "Lat": 49.34209431064309, + "Long": 8.668326465412974 + }, + "Navigation": { + "Lat": null, + "Long": null + } + }, + "Geometries": [] + }, + { + "LocationID": 679, + "Name": "Bahnhofstraße", + "Type": "STATIC", + "FreeParking": 56.0, + "OccupiedParking": 6.0, + "TotalParking": 62.0, + "ParentLocations": [ + "api/v3/rest/json/locations?filter=677&exclude=SubLocations&select=[0]" + ], + "Areas": [ + "api/v3/rest/json/areas?filter=4011&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4012&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4013&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4014&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4015&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4016&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4017&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4018&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4019&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4020&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4021&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4022&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4023&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4024&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4025&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4026&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4027&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4028&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4029&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4030&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4031&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4032&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4033&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4034&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4035&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4036&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4037&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4038&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4039&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4040&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4041&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4042&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4043&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4044&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4045&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4046&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4047&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4048&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4049&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4050&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4051&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4052&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4053&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4054&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4055&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4056&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4057&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4058&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4059&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4060&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4061&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4062&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4063&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4064&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4065&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4066&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4067&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4068&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4069&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4070&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4071&exclude=Location&select=[0]", + "api/v3/rest/json/areas?filter=4072&exclude=Location&select=[0]" + ], + "SubLocations": [], + "Positions": { + "Center": { + "Lat": 49.341713800212275, + "Long": 8.669230901378217 + }, + "Navigation": { + "Lat": null, + "Long": null + } + }, + "Geometries": [] + } +] \ No newline at end of file diff --git a/tests/converters/vrn_p_r_test.py b/tests/converters/vrn_p_r_test.py new file mode 100644 index 0000000..6272828 --- /dev/null +++ b/tests/converters/vrn_p_r_test.py @@ -0,0 +1,103 @@ +""" +Copyright 2024 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from pathlib import Path +from unittest.mock import Mock + +import pytest +from parkapi_sources.converters import VrnParkAndRidePullConverter +from requests_mock import Mocker + +from tests.converters.helper import validate_realtime_parking_site_inputs, validate_static_parking_site_inputs + + +@pytest.fixture +def vrn_p_r_config_helper(mocked_config_helper: Mock): + config = { + 'PARK_API_VRN_P_R_MULTIGUIDE_API_USERNAME': '0152d634-9e16-46c0-bfef-20c0b623eaa3', + 'PARK_API_VRN_P_R_MULTIGUIDE_API_PASSWORD': 'eaf7a00c-d0e1-4464-a9dc-f8ef4d01f2cc', + 'PARK_API_VRN_P_R_SONAH_API_BEARER_TOKEN': '0152d634-9e16-46c0-bfef-20c0b623eaa3', + } + mocked_config_helper.get.side_effect = lambda key, default=None: config.get(key, default) + return mocked_config_helper + + +@pytest.fixture +def vrn_p_r_pull_converter(vrn_p_r_config_helper: Mock) -> VrnParkAndRidePullConverter: + return VrnParkAndRidePullConverter(config_helper=vrn_p_r_config_helper) + + +class VrnParkAndRideBaseConverterTest: + @staticmethod + def test_get_static_parking_sites(vrn_p_r_pull_converter: VrnParkAndRidePullConverter, requests_mock: Mocker): + json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r.json') + with json_path.open() as json_file: + json_data = json_file.read() + + requests_mock.get( + 'https://spatial.vrn.de/data/rest/services/Hosted/p_r_parkapi_static/FeatureServer/5/query?where=objectid%3E0&outFields=*&returnGeometry=true&f=geojson', + text=json_data, + ) + + multiguide_json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r_multiguide.json') + with multiguide_json_path.open() as json_file: + multiguide_json_data = json_file.read() + + requests_mock.get( + 'https://vrn.multiguide.info/api/area', + text=multiguide_json_data, + ) + + sonah_json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r_sonah.json') + with sonah_json_path.open() as json_file: + sonah_json_data = json_file.read() + + requests_mock.get( + 'https://vrnm.dyndns.sonah.xyz/api/v3/rest/json/locations', + text=sonah_json_data, + ) + + static_parking_site_inputs, import_parking_site_exceptions = vrn_p_r_pull_converter.get_static_parking_sites() + + assert len(static_parking_site_inputs) == 12 + assert len(import_parking_site_exceptions) == 0 + + validate_static_parking_site_inputs(static_parking_site_inputs) + + @staticmethod + def test_get_realtime_parking_sites(vrn_p_r_pull_converter: VrnParkAndRidePullConverter, requests_mock: Mocker): + json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r.json') + with json_path.open() as json_file: + json_data = json_file.read() + + requests_mock.get( + 'https://spatial.vrn.de/data/rest/services/Hosted/p_r_parkapi_static/FeatureServer/5/query?where=objectid%3E0&outFields=*&returnGeometry=true&f=geojson', + text=json_data, + ) + + multiguide_json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r_multiguide.json') + with multiguide_json_path.open() as json_file: + multiguide_json_data = json_file.read() + + requests_mock.get( + 'https://vrn.multiguide.info/api/area', + text=multiguide_json_data, + ) + + sonah_json_path = Path(Path(__file__).parent, 'data', 'vrn_p_r_sonah.json') + with sonah_json_path.open() as json_file: + sonah_json_data = json_file.read() + + requests_mock.get( + 'https://vrnm.dyndns.sonah.xyz/api/v3/rest/json/locations', + text=sonah_json_data, + ) + + realtime_parking_site_inputs, import_parking_site_exceptions = vrn_p_r_pull_converter.get_realtime_parking_sites() + + assert len(realtime_parking_site_inputs) == 12 + assert len(import_parking_site_exceptions) == 0 + + validate_realtime_parking_site_inputs(realtime_parking_site_inputs)