mirror of
https://github.com/sascha-hemi/hacs_waste_collection_schedule.git
synced 2026-03-21 00:04:11 +01:00
add Sammelkalender.ch
This commit is contained in:
@@ -1355,9 +1355,11 @@ If your service provider is not listed, feel free to open a [source request issu
|
||||
- [Münchenstein](/doc/source/muenchenstein_ch.md) / muenchenstein.ch
|
||||
- [Münsingen BE, Switzerland](/doc/ics/muensingen_ch.md) / muensingen.ch
|
||||
- [Real Luzern](/doc/source/real_luzern_ch.md) / real-luzern.ch
|
||||
- [Real Luzern](/doc/source/sammelkalender_ch.md) / realluzern.ch
|
||||
- [Rehetobel](/doc/source/a_region_ch.md) / a-region.ch
|
||||
- [Rorschach](/doc/source/a_region_ch.md) / a-region.ch
|
||||
- [Rorschacherberg](/doc/source/a_region_ch.md) / a-region.ch
|
||||
- [Sammelkalender.ch](/doc/source/sammelkalender_ch.md) / info.sammelkalender.ch
|
||||
- [Schwellbrunn](/doc/source/a_region_ch.md) / a-region.ch
|
||||
- [Schönengrund](/doc/source/a_region_ch.md) / a-region.ch
|
||||
- [Speicher](/doc/source/a_region_ch.md) / a-region.ch
|
||||
@@ -1374,6 +1376,9 @@ If your service provider is not listed, feel free to open a [source request issu
|
||||
- [Waldstatt](/doc/source/a_region_ch.md) / a-region.ch
|
||||
- [Wittenbach](/doc/source/a_region_ch.md) / a-region.ch
|
||||
- [Wolfhalden](/doc/source/a_region_ch.md) / a-region.ch
|
||||
- [ZAKU Entsorgung](/doc/source/sammelkalender_ch.md) / zaku.ch
|
||||
- [Zeba](/doc/source/sammelkalender_ch.md) / zebazug.ch
|
||||
- [ZKRI](/doc/source/sammelkalender_ch.md) / zkri.ch
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
@@ -7341,6 +7341,13 @@
|
||||
"module": "real_luzern_ch",
|
||||
"default_params": {}
|
||||
},
|
||||
{
|
||||
"title": "Real Luzern",
|
||||
"module": "sammelkalender_ch",
|
||||
"default_params": {
|
||||
"service_provider": "real_luzern"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Rehetobel",
|
||||
"module": "a_region_ch",
|
||||
@@ -7362,6 +7369,11 @@
|
||||
"municipality": "Rorschacherberg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Sammelkalender.ch",
|
||||
"module": "sammelkalender_ch",
|
||||
"default_params": {}
|
||||
},
|
||||
{
|
||||
"title": "Schwellbrunn",
|
||||
"module": "a_region_ch",
|
||||
@@ -7473,6 +7485,27 @@
|
||||
"default_params": {
|
||||
"municipality": "Wolfhalden"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "ZAKU Entsorgung",
|
||||
"module": "sammelkalender_ch",
|
||||
"default_params": {
|
||||
"service_provider": "zaku"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Zeba",
|
||||
"module": "sammelkalender_ch",
|
||||
"default_params": {
|
||||
"service_provider": "zeba"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "ZKRI",
|
||||
"module": "sammelkalender_ch",
|
||||
"default_params": {
|
||||
"service_provider": "zkri"
|
||||
}
|
||||
}
|
||||
],
|
||||
"United Kingdom": [
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
import requests
|
||||
from waste_collection_schedule import Collection # type: ignore[attr-defined]
|
||||
|
||||
TITLE = "Sammelkalender.ch"
|
||||
DESCRIPTION = "Source for Sammelkalender.ch."
|
||||
URL = "https://info.sammelkalender.ch"
|
||||
TEST_CASES = {
|
||||
"zeba Baar Aberenrain 10": {
|
||||
"service_provider": "zeba",
|
||||
"municipality": "Baar",
|
||||
"street": "Aberenrain",
|
||||
"hnr": 10,
|
||||
},
|
||||
"zkri Rothenthurm": {
|
||||
"service_provider": "zkri",
|
||||
"municipality": "Rothenthurm",
|
||||
},
|
||||
"ZKRI, Schwyz Seewen": {
|
||||
"service_provider": "zkri",
|
||||
"municipality": "Schwyz",
|
||||
"street": "Seewen",
|
||||
},
|
||||
"Real Luzern, Meggen": {
|
||||
"service_provider": "real_luzern",
|
||||
"municipality": "Meggen",
|
||||
},
|
||||
"Real Luzern, Malters, Bodenmättli": {
|
||||
"service_provider": "real_luzern",
|
||||
"municipality": "Malters",
|
||||
"street": "Bodenmättli",
|
||||
},
|
||||
"Zaku Altdorf dag": {
|
||||
"service_provider": "zaku",
|
||||
"municipality": "Altdorf",
|
||||
"street": "dag",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
ICON_MAP = {
|
||||
"Kehricht": "mdi:trash-can",
|
||||
"Christbaum": "mdi:pine-tree",
|
||||
"Grüngut": "mdi:leaf",
|
||||
"Karton": "mdi:package-variant-closed",
|
||||
"Papier": "mdi:newspaper",
|
||||
"Alteisen/Metall": "mdi:screw-flat-top",
|
||||
}
|
||||
|
||||
SERVICES = {
|
||||
"zeba": {
|
||||
"title": "Zeba",
|
||||
"url": "https://www.zebazug.ch",
|
||||
"api_url_search": "https://www.zeba.sammelkalender.ch/kunden_WANN_auswahl_form.php",
|
||||
"api_url_bin": "https://www.zeba.sammelkalender.ch/kunden_sammlung_ausw_cont.php",
|
||||
},
|
||||
"zkri": {
|
||||
"title": "ZKRI",
|
||||
"url": "https://zkri.ch",
|
||||
"api_url_search": "https://daten.zkri.ch/web/Sammelkalender/kunden_WANN_auswahl_form.php",
|
||||
"api_url_bin": "https://daten.zkri.ch/web/Sammelkalender/kunden_sammlung_ausw_cont.php",
|
||||
},
|
||||
"real_luzern": {
|
||||
"title": "Real Luzern",
|
||||
"url": "https://www.realluzern.ch",
|
||||
"api_url_search": "https://www.real.sammelkalender.ch/app/appSelect.php",
|
||||
"api_url_bin": "https://www.real.sammelkalender.ch/app/appSammeldaten.php",
|
||||
},
|
||||
"zaku": {
|
||||
"title": "ZAKU Entsorgung",
|
||||
"url": "https://www.zaku.ch",
|
||||
"api_url_search": "https://www.zaku.sammelkalender.ch/appSelect.php",
|
||||
"api_url_bin": "https://www.zaku.sammelkalender.ch/appSammeldaten.php",
|
||||
},
|
||||
}
|
||||
|
||||
EXTRA_INFO = [
|
||||
{
|
||||
"title": s["title"],
|
||||
"url": s["url"],
|
||||
"default_params": {
|
||||
"service_provider": key,
|
||||
},
|
||||
}
|
||||
for key, s in SERVICES.items()
|
||||
]
|
||||
|
||||
|
||||
PROVIDER_LITERALS = Literal["zeba", "zkri", "real_luzern", "zaku"]
|
||||
|
||||
|
||||
API_URL = ""
|
||||
|
||||
|
||||
class Source:
|
||||
def __init__(
|
||||
self,
|
||||
service_provider: PROVIDER_LITERALS,
|
||||
municipality: str,
|
||||
street: str | None = None,
|
||||
hnr: str | int | None = None,
|
||||
) -> None:
|
||||
service_provider_str = service_provider.lower()
|
||||
if service_provider not in SERVICES:
|
||||
raise ValueError(f"Invalid service provider: {service_provider_str}")
|
||||
self._search_url = SERVICES[service_provider_str]["api_url_search"]
|
||||
self._bin_url = SERVICES[service_provider_str]["api_url_bin"]
|
||||
|
||||
self._municipality: str = municipality
|
||||
self._street: str | None = street
|
||||
self._hnr: str = str(hnr)
|
||||
|
||||
self._municipality_id: str | None = None
|
||||
self._address_id: str | None = None
|
||||
|
||||
@staticmethod
|
||||
def _compare(s1: str, s2: str | list[str]) -> bool:
|
||||
if isinstance(s2, str):
|
||||
s2 = [s2]
|
||||
|
||||
for s in s2:
|
||||
if s1.lower().replace(" ", "").replace("str.", "straße").replace(
|
||||
"strasse", "straße"
|
||||
) == s.lower().replace(" ", "").replace("str.", "straße").replace(
|
||||
"strasse", "straße"
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_streets(self, sage: bool = False) -> list[dict]:
|
||||
params: dict[str, str | int | None] = {
|
||||
"optGem": self._municipality_id,
|
||||
"Jahr": None,
|
||||
}
|
||||
if sage:
|
||||
params["fuerSage"] = 1
|
||||
else:
|
||||
params["fuerStr"] = 1
|
||||
|
||||
r = requests.get(self._search_url, params=params)
|
||||
r.raise_for_status()
|
||||
streets = r.json()
|
||||
|
||||
if len(streets) == 0:
|
||||
return []
|
||||
return streets
|
||||
|
||||
def _fetch_sage(self) -> None:
|
||||
sages = self._get_streets(sage=True)
|
||||
if not sages:
|
||||
return
|
||||
if len(sages) == 1:
|
||||
self._address_id = sages[0]["SAGEid"]
|
||||
return
|
||||
for sage in sages:
|
||||
if not self._street:
|
||||
street_names = list(
|
||||
{s.get("STRname") or s.get("SAGEname") for s in sages}
|
||||
)
|
||||
raise ValueError(f"Street required, use one of {street_names}")
|
||||
if self._compare(self._street, [sage["SAGEabk"], sage["SAGEname"]]):
|
||||
self._address_id = sage["SAGEid"]
|
||||
break
|
||||
if not self._address_id:
|
||||
raise ValueError(
|
||||
f"Invalid street: {self._street}, use one of {list({s['SAGEname'] for s in sages})}"
|
||||
)
|
||||
|
||||
def _fetch_street(self) -> None:
|
||||
streets = self._get_streets()
|
||||
if not streets:
|
||||
self._fetch_sage()
|
||||
return
|
||||
|
||||
self._address_id = None
|
||||
street_matches = []
|
||||
if len(streets) == 1:
|
||||
street_matches = streets
|
||||
else:
|
||||
if not self._street:
|
||||
street_names = list(
|
||||
{s.get("STRname") or s.get("SAGEname") for s in streets}
|
||||
)
|
||||
raise ValueError(f"Street required, use one of {street_names}")
|
||||
for street in streets:
|
||||
if self._compare(street["STRname"], self._street):
|
||||
street_matches.append(street)
|
||||
|
||||
if not street_matches:
|
||||
raise ValueError(
|
||||
f"Invalid street: {self._street}, use one of {list({s['STRname'] for s in streets})}"
|
||||
)
|
||||
|
||||
for street in street_matches:
|
||||
if street["STRhausnr"] and self._hnr is None:
|
||||
raise ValueError(
|
||||
f"House number required, use one of {list({s['STRhausnr'] for s in street_matches})}"
|
||||
)
|
||||
if not street["STRhausnr"] or self._compare(street["STRhausnr"], self._hnr):
|
||||
self._address_id = street["SAGEid"]
|
||||
break
|
||||
|
||||
if not self._address_id:
|
||||
raise ValueError(
|
||||
f"Invalid house number: {self._hnr}, use one of {list({s['STRhausnr'] for s in street_matches})}"
|
||||
)
|
||||
|
||||
def _fetch_ids(self) -> None:
|
||||
params = {
|
||||
"optGem": "GEM",
|
||||
"Jahr": None,
|
||||
}
|
||||
|
||||
# get json file
|
||||
r = requests.get(self._search_url, params=params)
|
||||
r.raise_for_status()
|
||||
self._municipality_id = None
|
||||
for mun in r.json():
|
||||
if self._compare(mun["GEMname"], self._municipality):
|
||||
self._municipality_id = mun["GEMid"]
|
||||
break
|
||||
if not self._municipality_id:
|
||||
raise ValueError(
|
||||
f"Invalid municipality: {self._municipality}, use one of {[m['GEMname'] for m in r.json()]}"
|
||||
)
|
||||
|
||||
self._fetch_street()
|
||||
|
||||
def fetch(self) -> list[Collection]:
|
||||
fresh_ids = False
|
||||
if self._municipality_id is None:
|
||||
fresh_ids = True
|
||||
self._fetch_ids()
|
||||
try:
|
||||
return self._get_collections()
|
||||
except Exception:
|
||||
if fresh_ids:
|
||||
raise
|
||||
self._fetch_ids()
|
||||
return self._get_collections()
|
||||
|
||||
def _get_collections(self) -> list[Collection]:
|
||||
today = datetime.now()
|
||||
year = today.year
|
||||
entries = self._get_collections_year(year)
|
||||
if today.month == 12:
|
||||
try:
|
||||
entries += self._get_collections_year(year + 1)
|
||||
except Exception:
|
||||
pass
|
||||
return entries
|
||||
|
||||
def _get_collections_year(self, year: int) -> list[Collection]:
|
||||
if self._municipality is None:
|
||||
raise ValueError("Municipality required")
|
||||
|
||||
params: dict[str, str | int | None] = {
|
||||
"nGem": self._municipality_id,
|
||||
"nAbar": "null",
|
||||
"nSage": self._address_id,
|
||||
"sDatum": "",
|
||||
"delsel": "no",
|
||||
"jahr1": year,
|
||||
}
|
||||
r = requests.get(self._bin_url, params=params)
|
||||
r.raise_for_status()
|
||||
|
||||
entries = []
|
||||
for d in r.json():
|
||||
date = datetime.strptime(d["DATUM"], "%Y-%m-%d").date()
|
||||
bin_types = d["AbarOne"]
|
||||
if bin_types is None:
|
||||
continue
|
||||
for bin_type in bin_types.split("-"):
|
||||
icon = ICON_MAP.get(bin_type) # Collection icon
|
||||
entries.append(Collection(date=date, t=bin_type, icon=icon))
|
||||
|
||||
return entries
|
||||
56
doc/source/sammelkalender_ch.md
Normal file
56
doc/source/sammelkalender_ch.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Sammelkalender.ch
|
||||
|
||||
Support for schedules provided by [Sammelkalender.ch](https://info.sammelkalender.ch), serving multiple regions in Switzerland.
|
||||
|
||||
## Configuration via configuration.yaml
|
||||
|
||||
```yaml
|
||||
waste_collection_schedule:
|
||||
sources:
|
||||
- name: sammelkalender_ch
|
||||
args:
|
||||
service_provider: SERVICE PROVIDER
|
||||
municipality: MUNICIPALITY (Gemeinde)
|
||||
street: STREET (Straße)
|
||||
hnr: HOUSE NUMBER (Hausnummer)
|
||||
```
|
||||
|
||||
### Configuration Variables
|
||||
|
||||
**service_provider**
|
||||
*(String) (required)*
|
||||
|
||||
**municipality**
|
||||
*(String) (required)*
|
||||
|
||||
**street**
|
||||
*(String) (optional)* only required if the form asks for it
|
||||
|
||||
**hnr**
|
||||
*(String) (optional)* only required if the form asks for it
|
||||
|
||||
Supported service providers are:
|
||||
|
||||
- zeba: https://www.zebazug.ch
|
||||
- zkri: https://zkri.ch
|
||||
- real_luzern: https://www.realluzern.ch
|
||||
- zaku: https://www.zaku.ch
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
waste_collection_schedule:
|
||||
sources:
|
||||
- name: sammelkalender_ch
|
||||
args:
|
||||
service_provider: zeba
|
||||
municipality: Baar
|
||||
street: Aberenrain
|
||||
```
|
||||
|
||||
## How to get the source argument
|
||||
|
||||
You can check if your parameters work by visiting the website of the service provider and entering your address. Or using the app:
|
||||
|
||||
- [IOS AppStore](https://apps.apple.com/ch/app/sammelkalender/id1502137213?l)
|
||||
- [Android PlayStore](https://play.google.com/store/apps/details?id=ch.sammelkalender.app2020)
|
||||
Reference in New Issue
Block a user