Skip to content

Commit 1f3a831

Browse files
committed
Add Jena
1 parent f044e8b commit 1f3a831

File tree

4 files changed

+364
-0
lines changed

4 files changed

+364
-0
lines changed

park_api/cities/Jena.geojson

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"type": "FeatureCollection",
3+
"features": [
4+
{
5+
"type": "Feature",
6+
"geometry": {
7+
"type": "Point",
8+
"coordinates": [
9+
50.9282,
10+
11.5880
11+
]
12+
},
13+
"properties": {
14+
"name": "Jena",
15+
"type": "city",
16+
"url": "https://mobilitaet.jena.de/",
17+
"source": "https://opendata.jena.de/data/parkplatzbelegung.xml",
18+
"active_support": false,
19+
"attribution": {
20+
"contributor":"Kommunal Service Jena",
21+
"url":"https://opendata.jena.de/dataset/parken",
22+
"license":"dl-de/by-2-0"
23+
}
24+
}
25+
}
26+
]
27+
}

park_api/cities/Jena.py

+207
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import json
2+
import requests
3+
import pytz
4+
from datetime import datetime, time
5+
from bs4 import BeautifulSoup
6+
from park_api import env
7+
from park_api.geodata import GeoData
8+
from park_api.util import convert_date
9+
10+
# there is no need for any geodata for this file, as the api returns all of the information,
11+
# but if this is removed, the code crashes
12+
geodata = GeoData(__file__)
13+
14+
def parse_html(lot_vacancy_xml):
15+
16+
# there is a second source with all the general data for the parking lots
17+
HEADERS = {
18+
"User-Agent": "ParkAPI v%s - Info: %s" %
19+
(env.SERVER_VERSION, env.SOURCE_REPOSITORY),
20+
}
21+
22+
lot_data_json = requests.get("https://opendata.jena.de/dataset/1a542cd2-c424-4fb6-b30b-d7be84b701c8/resource/76b93cff-4f6c-47fa-ab83-b07d64c8f38a/download/parking.json", headers={**HEADERS})
23+
24+
lot_vacancy = BeautifulSoup(lot_vacancy_xml, "xml")
25+
lot_data = json.loads(lot_data_json.text)
26+
27+
data = {
28+
# the time contains the timezone and milliseconds which need to be stripped
29+
"last_updated": lot_vacancy.find("publicationTime").text.split(".")[0],
30+
"lots": []
31+
}
32+
33+
for lot in lot_data["parkingPlaces"]:
34+
# the lots from both sources need to be matched
35+
lot_data_list = [
36+
_lot for _lot in lot_vacancy.find_all("parkingFacilityStatus")
37+
if hasattr(_lot.parkingFacilityReference, "attr")
38+
and _lot.parkingFacilityReference.attrs["id"] == lot["general"]["name"]
39+
]
40+
41+
lot_id = lot["general"]["name"].lower().replace(" ", "-").replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
42+
43+
lot_info = {
44+
"id": lot_id,
45+
"name": lot["general"]["name"],
46+
"url": "https://mobilitaet.jena.de/de/" + lot_id,
47+
"address": lot["details"]["parkingPlaceAddress"]["parkingPlaceAddress"],
48+
"coords": lot["general"]["coordinates"],
49+
"state": get_status(lot),
50+
"lot_type": lot["general"]["objectType"],
51+
"opening_hours": parse_opening_hours(lot),
52+
"fee_hours": parse_charged_hours(lot),
53+
"forecast": False,
54+
}
55+
56+
# some lots do not have live vacancy data
57+
if len(lot_data_list) > 0:
58+
lot_info["free"] = int(lot_data_list[0].totalNumberOfVacantParkingSpaces.text)
59+
lot_info["total"] = int(lot_data_list[0].totalParkingCapacityShortTermOverride.text)
60+
else:
61+
continue
62+
# lot_info["free"] = None
63+
# lot_info["total"] = int(lot["details"]["parkingCapacity"]["totalParkingCapacityShortTermOverride"])
64+
# note: both api's have different values for the total parking capacity,
65+
# but the vacant slot are based on the total parking capacity from the same api,
66+
# so that is used if available
67+
68+
# also in the vacancy api the total capacity for the "Goethe Gallerie" are 0 if it is closed
69+
70+
71+
data["lots"].append(lot_info)
72+
73+
return data
74+
75+
# the rest of the code is there to deal with the api's opening/charging hours objects
76+
# example:
77+
# "openingTimes": [
78+
# {
79+
# "alwaysCharged": True,
80+
# "dateFrom": 2,
81+
# "dateTo": 5,
82+
# "times": [
83+
# {
84+
# "from": "07:00",
85+
# "to": "23:00"
86+
# }
87+
# ]
88+
# },
89+
# {
90+
# "alwaysCharged": False,
91+
# "dateFrom": 7,
92+
# "dateTo": 1,
93+
# "times": [
94+
# {
95+
# "from": "10:00",
96+
# "to": "03:00"
97+
# }
98+
# ]
99+
# }
100+
# ]
101+
102+
def parse_opening_hours(lot_data):
103+
if lot_data["parkingTime"]["openTwentyFourSeven"]: return "24/7"
104+
105+
return parse_times(lot_data["parkingTime"]["openingTimes"])
106+
107+
def parse_charged_hours(lot_data):
108+
charged_hour_objs = []
109+
110+
ph_info = "An Feiertagen sowie außerhalb der oben genannten Zeiten ist das Parken gebührenfrei."
111+
112+
if not lot_data["parkingTime"]["chargedOpeningTimes"] and lot_data["parkingTime"]["openTwentyFourSeven"]:
113+
if lot_data["priceList"]:
114+
if ph_info in str(lot_data["priceList"]["priceInfo"]):
115+
return "24/7; PH off"
116+
else: return "24/7"
117+
else: return "off"
118+
119+
# charging hours can also be indicated by the "alwaysCharged" variable in "openingTimes"
120+
elif not lot_data["parkingTime"]["chargedOpeningTimes"] and not lot_data["parkingTime"]["openTwentyFourSeven"]:
121+
for oh in lot_data["parkingTime"]["openingTimes"]:
122+
if "alwaysCharged" in oh and oh["alwaysCharged"]: charged_hour_objs.append(oh)
123+
if len(charged_hour_objs) == 0: return "off"
124+
125+
elif lot_data["parkingTime"]["chargedOpeningTimes"]:
126+
charged_hour_objs = lot_data["parkingTime"]["chargedOpeningTimes"]
127+
128+
charged_hours = parse_times(charged_hour_objs)
129+
130+
if ph_info in str(lot_data["priceList"]["priceInfo"]):
131+
charged_hours += "; PH off"
132+
133+
return charged_hours
134+
135+
# creatin osm opening_hours strings from opening/charging hours objects
136+
def parse_times(times_objs):
137+
DAYS = ["", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
138+
139+
ohs = ""
140+
141+
for index, oh in enumerate(times_objs):
142+
part = ""
143+
144+
if oh["dateFrom"] == oh["dateTo"]:
145+
part += DAYS[oh["dateFrom"]]
146+
else:
147+
part += DAYS[oh["dateFrom"]] + "-" + DAYS[oh["dateTo"]]
148+
149+
part += " "
150+
151+
for index2, time in enumerate(oh["times"]):
152+
part += time["from"] + "-" + time["to"]
153+
if index2 != len(oh["times"]) - 1: part += ","
154+
155+
if index != len(times_objs) - 1: part += "; "
156+
157+
ohs += part
158+
159+
return ohs
160+
161+
def get_status(lot_data):
162+
if lot_data["parkingTime"]["openTwentyFourSeven"]: return "open"
163+
164+
# check for public holiday?
165+
166+
for oh in lot_data["parkingTime"]["openingTimes"]:
167+
now = datetime.now(pytz.timezone("Europe/Berlin"))
168+
169+
weekday = now.weekday() + 1
170+
171+
# oh rules can also go beyond week ends (e.g. from Sunday to Monday)
172+
# this need to be treated differently
173+
if oh["dateFrom"] <= oh["dateTo"]:
174+
if not (weekday >= oh["dateFrom"]) or not (weekday <= oh["dateTo"] + 1): continue
175+
else:
176+
if weekday > oh["dateTo"] + 1 and weekday < oh["dateFrom"]: continue
177+
178+
for times in oh["times"]:
179+
time_from = get_timestamp_without_date(time.fromisoformat(times["from"]).replace(tzinfo=pytz.timezone("Europe/Berlin")))
180+
time_to = get_timestamp_without_date(time.fromisoformat(times["to"]).replace(tzinfo=pytz.timezone("Europe/Berlin")))
181+
182+
time_now = get_timestamp_without_date(now)
183+
184+
# time ranges can go over to the next day (e.g 10:00-03:00)
185+
if time_to >= time_from:
186+
if time_now >= time_from and time_now <= time_to:
187+
return "open"
188+
else: continue
189+
190+
else:
191+
if oh["dateFrom"] <= oh["dateTo"]:
192+
if (time_now >= time_from and weekday >= oh["dateFrom"] and weekday <= oh["dateTo"]
193+
or time_now <= time_to and weekday >= oh["dateFrom"] + 1 and weekday <= oh["dateTo"] + 1):
194+
return "open"
195+
else: continue
196+
197+
else:
198+
if (time_now >= time_from and (weekday >= oh["dateFrom"] or weekday <= oh["dateTo"])
199+
or time_now <= time_to and (weekday >= oh["dateFrom"] + 1 or weekday <= oh["dateTo"] + 1)):
200+
return "open"
201+
else: continue
202+
203+
# if no matching rule was found, the lot is closed
204+
return "closed"
205+
206+
def get_timestamp_without_date (date_obj):
207+
return date_obj.hour * 3600 + date_obj.minute * 60 + date_obj.second

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ yoyo-migrations
88
requests-mock
99
utm
1010
ddt
11+
lxml

tests/fixtures/jena.xml

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+
<d2LogicalModel xmlns="http://datex2.eu/schema/2/2_0">
3+
<payloadPublication xsi:type="GenericPublication" lang="de" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
4+
<publicationTime>2023-12-31T16:10:47.378+01:00</publicationTime>
5+
<publicationCreator>
6+
<country>de</country>
7+
<nationalIdentifier>Kommunal Service Jena</nationalIdentifier>
8+
</publicationCreator>
9+
<genericPublicationExtension>
10+
<parkingFacilityTableStatusPublication>
11+
<headerInformation>
12+
<confidentiality>noRestriction</confidentiality>
13+
<informationStatus>real</informationStatus>
14+
</headerInformation>
15+
<parkingFacilityStatus>
16+
<parkingFacilityExitRate>0</parkingFacilityExitRate>
17+
<parkingFacilityFillRate>0</parkingFacilityFillRate>
18+
<parkingFacilityOccupancy>0.0</parkingFacilityOccupancy>
19+
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
20+
<parkingFacilityReference id="City Carree" version="SYSTEM"/>
21+
<parkingFacilityStatusTime>2023-12-30T23:10:39.826+01:00</parkingFacilityStatusTime>
22+
<totalNumberOfOccupiedParkingSpaces>0</totalNumberOfOccupiedParkingSpaces>
23+
<totalNumberOfVacantParkingSpaces>0</totalNumberOfVacantParkingSpaces>
24+
<totalParkingCapacityShortTermOverride>0</totalParkingCapacityShortTermOverride>
25+
</parkingFacilityStatus>
26+
<parkingFacilityStatus>
27+
<parkingFacilityExitRate>0</parkingFacilityExitRate>
28+
<parkingFacilityFillRate>0</parkingFacilityFillRate>
29+
<parkingFacilityOccupancy>12.0</parkingFacilityOccupancy>
30+
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
31+
<parkingFacilityReference id="Seidelparkplatz" version="SYSTEM"/>
32+
<parkingFacilityStatusTime>2023-12-31T16:05:45.504+01:00</parkingFacilityStatusTime>
33+
<totalNumberOfOccupiedParkingSpaces>20</totalNumberOfOccupiedParkingSpaces>
34+
<totalNumberOfVacantParkingSpaces>141</totalNumberOfVacantParkingSpaces>
35+
<totalParkingCapacityShortTermOverride>161</totalParkingCapacityShortTermOverride>
36+
</parkingFacilityStatus>
37+
<parkingFacilityStatus>
38+
<parkingFacilityExitRate>0</parkingFacilityExitRate>
39+
<parkingFacilityFillRate>0</parkingFacilityFillRate>
40+
<parkingFacilityOccupancy>8.0</parkingFacilityOccupancy>
41+
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
42+
<parkingFacilityReference id="Krautgasse" version="SYSTEM"/>
43+
<parkingFacilityStatusTime>2023-12-31T14:11:34.771+01:00</parkingFacilityStatusTime>
44+
<totalNumberOfOccupiedParkingSpaces>15</totalNumberOfOccupiedParkingSpaces>
45+
<totalNumberOfVacantParkingSpaces>178</totalNumberOfVacantParkingSpaces>
46+
<totalParkingCapacityShortTermOverride>193</totalParkingCapacityShortTermOverride>
47+
</parkingFacilityStatus>
48+
<parkingFacilityStatus>
49+
<parkingFacilityExitRate>0</parkingFacilityExitRate>
50+
<parkingFacilityFillRate>0</parkingFacilityFillRate>
51+
<parkingFacilityOccupancy>0.0</parkingFacilityOccupancy>
52+
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
53+
<parkingFacilityReference id="Goethe Galerie" version="SYSTEM"/>
54+
<parkingFacilityStatusTime>2023-12-30T23:00:29.090+01:00</parkingFacilityStatusTime>
55+
<totalNumberOfOccupiedParkingSpaces>0</totalNumberOfOccupiedParkingSpaces>
56+
<totalNumberOfVacantParkingSpaces>0</totalNumberOfVacantParkingSpaces>
57+
<totalParkingCapacityShortTermOverride>0</totalParkingCapacityShortTermOverride>
58+
</parkingFacilityStatus>
59+
<parkingFacilityStatus>
60+
<parkingFacilityExitRate>0</parkingFacilityExitRate>
61+
<parkingFacilityFillRate>0</parkingFacilityFillRate>
62+
<parkingFacilityOccupancy>5.0</parkingFacilityOccupancy>
63+
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
64+
<parkingFacilityReference id="Neue Mitte" version="SYSTEM"/>
65+
<parkingFacilityStatusTime>2023-12-31T16:07:44.769+01:00</parkingFacilityStatusTime>
66+
<totalNumberOfOccupiedParkingSpaces>10</totalNumberOfOccupiedParkingSpaces>
67+
<totalNumberOfVacantParkingSpaces>180</totalNumberOfVacantParkingSpaces>
68+
<totalParkingCapacityShortTermOverride>190</totalParkingCapacityShortTermOverride>
69+
</parkingFacilityStatus>
70+
<parkingFacilityStatus>
71+
<parkingFacilityExitRate>0</parkingFacilityExitRate>
72+
<parkingFacilityFillRate>0</parkingFacilityFillRate>
73+
<parkingFacilityOccupancy>19.0</parkingFacilityOccupancy>
74+
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
75+
<parkingFacilityReference id="Windberg" version="SYSTEM"/>
76+
<parkingFacilityStatusTime>2023-12-31T15:32:41.436+01:00</parkingFacilityStatusTime>
77+
<totalNumberOfOccupiedParkingSpaces>14</totalNumberOfOccupiedParkingSpaces>
78+
<totalNumberOfVacantParkingSpaces>61</totalNumberOfVacantParkingSpaces>
79+
<totalParkingCapacityShortTermOverride>75</totalParkingCapacityShortTermOverride>
80+
</parkingFacilityStatus>
81+
<parkingFacilityStatus>
82+
<parkingFacilityExitRate>0</parkingFacilityExitRate>
83+
<parkingFacilityFillRate>0</parkingFacilityFillRate>
84+
<parkingFacilityOccupancy>0.0</parkingFacilityOccupancy>
85+
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
86+
<parkingFacilityReference id="Eichplatz" version="SYSTEM"/>
87+
<parkingFacilityStatusTime>2023-11-16T07:26:33.218+01:00</parkingFacilityStatusTime>
88+
<totalNumberOfOccupiedParkingSpaces>0</totalNumberOfOccupiedParkingSpaces>
89+
<totalNumberOfVacantParkingSpaces>0</totalNumberOfVacantParkingSpaces>
90+
<totalParkingCapacityShortTermOverride>0</totalParkingCapacityShortTermOverride>
91+
</parkingFacilityStatus>
92+
<parkingFacilityStatus>
93+
<parkingFacilityExitRate>0</parkingFacilityExitRate>
94+
<parkingFacilityFillRate>0</parkingFacilityFillRate>
95+
<parkingFacilityOccupancy>100.0</parkingFacilityOccupancy>
96+
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
97+
<parkingFacilityReference id="Haeckelplatz" version="SYSTEM"/>
98+
<parkingFacilityStatus>full</parkingFacilityStatus>
99+
<parkingFacilityStatusTime>2023-12-28T13:11:30.623+01:00</parkingFacilityStatusTime>
100+
<totalNumberOfOccupiedParkingSpaces>32</totalNumberOfOccupiedParkingSpaces>
101+
<totalNumberOfVacantParkingSpaces>0</totalNumberOfVacantParkingSpaces>
102+
<totalParkingCapacityShortTermOverride>32</totalParkingCapacityShortTermOverride>
103+
</parkingFacilityStatus>
104+
<parkingFacilityStatus>
105+
<parkingFacilityExitRate>0</parkingFacilityExitRate>
106+
<parkingFacilityFillRate>0</parkingFacilityFillRate>
107+
<parkingFacilityOccupancy>29.0</parkingFacilityOccupancy>
108+
<parkingFacilityOccupancyTrend>stable</parkingFacilityOccupancyTrend>
109+
<parkingFacilityReference id="Holzmarkt" version="SYSTEM"/>
110+
<parkingFacilityStatusTime>2023-12-31T16:08:44.571+01:00</parkingFacilityStatusTime>
111+
<totalNumberOfOccupiedParkingSpaces>44</totalNumberOfOccupiedParkingSpaces>
112+
<totalNumberOfVacantParkingSpaces>106</totalNumberOfVacantParkingSpaces>
113+
<totalParkingCapacityShortTermOverride>150</totalParkingCapacityShortTermOverride>
114+
</parkingFacilityStatus>
115+
<parkingFacilityStatus>
116+
<parkingFacilityExitRate>0</parkingFacilityExitRate>
117+
<parkingFacilityFillRate>0</parkingFacilityFillRate>
118+
<parkingFacilityOccupancy>43.0</parkingFacilityOccupancy>
119+
<parkingFacilityOccupancyTrend>decreasing</parkingFacilityOccupancyTrend>
120+
<parkingFacilityReference id="Steinkreuz" version="SYSTEM"/>
121+
<parkingFacilityStatusTime>2023-12-31T16:08:44.586+01:00</parkingFacilityStatusTime>
122+
<totalNumberOfOccupiedParkingSpaces>26</totalNumberOfOccupiedParkingSpaces>
123+
<totalNumberOfVacantParkingSpaces>34</totalNumberOfVacantParkingSpaces>
124+
<totalParkingCapacityShortTermOverride>60</totalParkingCapacityShortTermOverride>
125+
</parkingFacilityStatus>
126+
</parkingFacilityTableStatusPublication>
127+
</genericPublicationExtension>
128+
</payloadPublication>
129+
</d2LogicalModel>

0 commit comments

Comments
 (0)