Add source api.golemio.cz (Prague)

This commit is contained in:
Jan Čermák
2024-02-23 12:58:44 +01:00
committed by 5ila5
parent 0bfd72604e
commit ab1888dff6
4 changed files with 268 additions and 1 deletions

View File

@@ -491,6 +491,7 @@ Waste collection schedules in the following formats and countries are supported.
<details>
<summary>Czech Republic</summary>
- [Praha](/doc/source/api_golemio_cz.md) / api.golemio.cz/docs/openapi
- [Rudna u Prahy](/doc/source/mestorudna_cz.md) / rudnamesto.cz
</details>

View File

@@ -0,0 +1,195 @@
import logging
from datetime import date, timedelta
import requests
from waste_collection_schedule import Collection # type: ignore[attr-defined]
TITLE = "Praha"
DESCRIPTION = "Prague municipal waste collection via Golemio API."
URL = "https://api.golemio.cz/docs/openapi/"
TEST_CASES = {
"Chvalská": {
"lat": 50.104802397741665,
"lon": 14.538238985303936,
"radius": 1,
"api_key": "!secret api_golemio_api_key",
"ignored_containers": [35895],
},
"Smržových - monitored": {
"lat": 50.10847352,
"lon": 14.5154944,
"radius": 1,
"only_monitored": True,
"api_key": "!secret api_golemio_api_key",
},
"Chvalská - suffix": {
"lat": 50.104802397741665,
"lon": 14.538238985303936,
"radius": 1,
"api_key": "!secret api_golemio_api_key",
"suffix": " - Chvalská",
"ignored_containers": [35895],
},
"Radius - auto_suffix": {
"lat": 50.104802397741665,
"lon": 14.538238985303936,
"radius": 300,
"api_key": "!secret api_golemio_api_key",
"auto_suffix": True,
"ignored_containers": [35895, 35901, 35918],
},
}
API_URL = "https://api.golemio.cz/"
ICON_MAP = {
1: "mdi:bottle-soda", # Barevné sklo | Tinted glass |
2: "mdi:lightning-bolt", # Elektrozařízení | Electric waste |
3: "mdi:cog", # Kovy | Metals |
4: "mdi:package-variant-closed", # Nápojové kartóny | Beverage cartons |
5: "mdi:package-variant", # Papír | Paper |
6: "mdi:recycle", # Plast | Plastics |
7: "mdi:bottle-soda-outline", # Čiré sklo | Clear glass |
8: "mdi:barrel", # Jedlé tuky a oleje | Edible fats and oils |
9: "mdi:delete-variant", # Multikomoditní sběr | Multicommodity |
}
_CZ_DAY_TO_WEEKDAY = {
"Po": 0,
"Út": 1,
"St": 2,
"Čt": 3,
"": 4,
"So": 5,
"Ne": 6,
}
_LOGGER = logging.getLogger(__name__)
def _generate_next_picks(next_pick: date, pick_days: str, frequency: int, count=10):
"""
Yield expected pick dates from given `next_pick`, `pick_day` and `frequency`.
Cleaning Frequency
| Value | 1st digit - period duration | 2nd digit - frequency |
Example
| 13 | 1 | 3 | 3 times per 1 week |
| 61 | 6 | 1 | Once per 6 weeks |
>>> [d for d in _generate_next_picks(date(2024, 2, 20), "Po, Út, Pá", 13, 5)]
[datetime.date(2024, 2, 19), datetime.date(2024, 2, 20), datetime.date(2024, 2, 23), datetime.date(2024, 2, 26), datetime.date(2024, 2, 27)]
>>> [d for d in _generate_next_picks(date(2024, 2, 20), "Út", 41, 4)]
[datetime.date(2024, 1, 23), datetime.date(2024, 2, 20), datetime.date(2024, 3, 19), datetime.date(2024, 4, 16)]
>>> [d for d in _generate_next_picks(date(2024, 2, 26), "Po, Čt", 12, 4)]
[datetime.date(2024, 2, 22), datetime.date(2024, 2, 26), datetime.date(2024, 2, 29), datetime.date(2024, 3, 4)]
>>> [d for d in _generate_next_picks(date(2024, 2, 26), "Po, Čt", 42, 5)]
[datetime.date(2024, 2, 1), datetime.date(2024, 2, 26), datetime.date(2024, 2, 29), datetime.date(2024, 3, 25), datetime.date(2024, 3, 28)]
"""
period_duration = frequency // 10
times_per_week = frequency % 10
weekdays = [
_CZ_DAY_TO_WEEKDAY[day] for day in map(str.strip, pick_days.split(", "))
]
if times_per_week != len(weekdays):
_LOGGER.warning(
"Times per week (%s) and pick days (%s) mismatch, generated dates may be wrong.",
times_per_week,
pick_days,
)
# check where we are in the days list
first_pick_index = weekdays.index(next_pick.weekday())
first_pick_weekday = weekdays[first_pick_index]
# get one date in the past (can be today) - API only provides future dates
for i in range(-1, count - 1):
next_pick_index = (first_pick_index + i) % len(weekdays)
weeks = period_duration * ((first_pick_index + i) // len(weekdays))
days = weekdays[next_pick_index] - first_pick_weekday
yield next_pick + timedelta(weeks=weeks, days=days)
class Source:
def __init__(
self,
lat: int,
lon: int,
radius: int,
api_key: str,
only_monitored: bool = False,
ignored_containers: list | None = None,
auto_suffix: bool = False,
suffix: str | None = None,
):
self._lat = lat
self._lon = lon
self._radius = radius
self._api_key = api_key
self._only_monitored = only_monitored
self._ignored_containers = ignored_containers or []
self._auto_suffix = auto_suffix
self._suffix = suffix
def fetch(self):
r = requests.get(
f"{API_URL}v2/sortedwastestations",
params={
"latlng": f"{self._lat},{self._lon}",
"range": self._radius,
"limit": 1000,
},
headers={"X-Access-Token": self._api_key, "Accept": "application/json"},
)
r.raise_for_status()
features = r.json()["features"]
entries = []
for feature in features:
for container in feature["properties"]["containers"]:
if container["container_id"] in self._ignored_containers:
continue
trash_type = container["trash_type"]
frequency = container["cleaning_frequency"]
next_pick = frequency.get("next_pick")
if not next_pick:
# some containers have no next pick date, show warning if they are not ignored
_LOGGER.warning(
"No next pick date for container ID %s (%s), add it to ignored_containers.",
container["container_id"],
trash_type["description"],
)
continue
next_pick = date.fromisoformat(next_pick)
# use optionally suffixed trash type as the type display name
t = trash_type["description"]
if self._auto_suffix:
t = f"{t} - {feature['properties'].get('name', feature['properties']['id'])}"
if self._suffix:
t = f"{t}{self._suffix}"
for d in _generate_next_picks(
next_pick, frequency["pick_days"], frequency["id"]
):
entries.append(
Collection(
date=d,
t=t,
icon=ICON_MAP.get(trash_type["id"]),
)
)
return entries

View File

@@ -0,0 +1,71 @@
# Praha - Czech Republic
Support for schedules provided by [Golemio API](https://api.golemio.cz/docs/openapi/), open data platform for Prague, CZ.
## Configuration via configuration.yaml
```yaml
waste_collection_schedule:
sources:
- name: api_golemio_cz
args:
lat: LAT
lon: LON
radius: RADIUS
api_key: API_KEY
ignored_containers:
- 123
- 456
auto_suffix: True
suffix: SUFFIX
```
### Configuration Variables
**lat** _integer (required)_: Latitude of the location.
**lon** _integer (required)_: Longitude of the location.
**radius** _integer (required)_: Radius from the defined location, use low value to select a single separation point.
**api_key** _string (required)_: API key for Golemio API, see below.
**only_monitored** _boolean (optional)_: Only return monitored containers (the state is not reported by the integration though).
**ignored_containers**: _list of integers (optional)_: List of IDs for containers to ignore.
**auto_suffix**: _boolean (optional)_: Automatically append address as the suffix to all containers found in this query.
**suffix**: _string (optional)_: Suffix to append to all containers found in this query.
## Example
```yaml
waste_collection_schedule:
sources:
- name: api_golemio_cz
args:
lat: 50.104802397741665
lon: 14.538238985303936
radius: 1
api_key: !secret golemio_api_key
suffix: " - Chvalská"
ignored_containers:
- 35895
```
## Getting the API key
To get the API key, you must register at the Golemio site with a verified e-mail address. Follow instructions at the [Golemio site](https://api.golemio.cz/api-keys) and save the generated token to your `secrets.yaml`.
## Using suffix and auto_suffix
The `suffix` and `auto_suffix` arguments are used to append text to the end of the container types. This is useful when you have multiple containers with the same type, but in different locations. The `suffix` is generally useful if you have multiple config entries, each targeting a single separation point, while the `auto_suffix` is essentially necessary to use queries that return multiple separation points, each container will be then appended with an address (e.g. `Papír - Oktábcových 1039/1`). If both arguments are supplied, `suffix` is appended after the auto-generated suffix.
## Ignoring containers
Sometimes you don't want to get all containers, for example if a pick date is not provided for the container and a warning is shown in the logs. You can use `ignored_containers` list to exclude these containers from the result.
## Note on calculation of collection dates
The API provides only a single future date in a single query. However, the schema contains information about weekdays and frequency of the collection which are used for calculation of other dates. Currently, the integration calculates one past date (or today's date, because the API does not return the collection date if it's today) and future dates to return 10 dates for every container returned in the query.

File diff suppressed because one or more lines are too long