Add source: Entsorgung + Recycling Stadt Bern (ERB), Switzerland - #7290
Add source: Entsorgung + Recycling Stadt Bern (ERB), Switzerland#7290sbaerlocher wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
There are user-facing correctness issues in the new source (waste-type icon mapping and error attribution when configured via key) that should be fixed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds a new waste-collection source for Entsorgung + Recycling Stadt Bern (ERB), integrating Bern’s public iCalendar feed into the hacs_waste_collection_schedule source catalogue.
Changes:
- Added
bern_chPython source that resolves an address (or accepts a key) and parses ERB’s ICS schedule intoCollectionentries. - Added end-user documentation for configuring the new
bern_chsource in Home Assistant.
File summaries
| File | Description |
|---|---|
custom_components/waste_collection_schedule/waste_collection_schedule/source/bern_ch.py |
New source implementation using ERB address lookup / key fallback + ICS parsing. |
doc/source/bern_ch.md |
New documentation describing configuration and how to obtain arguments/key. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if not entries: | ||
| raise SourceArgumentNotFound( | ||
| "strasse", | ||
| f"{self._strasse or ''} {self._hnr or ''}".strip() or self._key, | ||
| "No collections returned for this address.", | ||
| ) |
There was a problem hiding this comment.
Good catch — fixed in c7f5d49. The "no collections" branch now reports key when the source is configured that way, and keeps strasse for the address path:
bad key : argument 'key' ... The service does not know this key.
bad address: argument 'strasse' with the value 'Gibtsnichtstrasse 1' ...
| ICON_MAP = { | ||
| "Hauskehricht": Icons.GENERAL_WASTE, | ||
| "Altpapiersammlung": Icons.PAPER, | ||
| # Bern collects kitchen and garden organics together in one "Grünabfuhr". | ||
| "Gruenabfuhr": Icons.ORGANIC, | ||
| } |
There was a problem hiding this comment.
Checked this against the live feed before changing anything — the SUMMARY is ASCII, so the current key is the matching one:
$ grep -c 'Gruenabfuhr' feed.ics # 1
$ grep -c 'Grünabfuhr' feed.ics # 0
All three types resolve to an icon (Hauskehricht mdi:trash-can, Gruenabfuhr mdi:leaf, Altpapiersammlung mdi:package-variant); no entry comes back with icon=None.
The website does display "Grünabfuhr", which is what my comment referred to — that was misleading, so I clarified it in c7f5d49 to state that ICON_MAP keys must match the feed's SUMMARY verbatim. Leaving the key as-is.
Add a source for the city of Bern (ERB), Switzerland, covering Hauskehricht, Grünabfuhr and Altpapiersammlung. The provider exposes a public iCalendar feed at bernentsorgung.glue.ch/erb/web/ical?key=<KEY> without auth or session. The key is the uppercase MD5 of street name concatenated with the house number, UTF-8 encoded and without a separator. Rather than hashing blindly, the source resolves the address through the site's own typeahead endpoint (searchAddress), which returns the canonical spelling and the key itself. This makes house-number suffixes case-insensitive for the user and yields suggestions when a number does not exist. The MD5 derivation remains as an offline fallback, and an optional key argument lets users paste the raw link if the scheme ever changes. Two feed quirks are handled: empty EXDATE lines are stripped because icalendar reports them as a broken property, and entries past the RRULE UNTIL date are dropped since UNTIL is given in UTC while the events are all-day, which otherwise spills one collection into the next year. Signed-off-by: Simon Bärlocher <s.baerlocher@sbaerlocher.ch>
When the source is configured with `key` instead of `strasse`/`hnr`, the "no collections" error reported the argument as `strasse` and passed the key as its value, so the Home Assistant UI highlighted the wrong field. Report `key` in that case and keep `strasse` for the address path. Also clarify the ICON_MAP comment: the keys must match the feed's SUMMARY values verbatim, and the feed spells "Gruenabfuhr" without the umlaut even though the website shows "Grünabfuhr". Signed-off-by: Simon Bärlocher <s.baerlocher@sbaerlocher.ch>
1525581 to
c7f5d49
Compare
|
This looks pretty complicated for a simple source, where the service provider provides a ics file. Wouldn't it be easier to add a ICS source |
|
Fair question — and you're right that this could technically be an ICS YAML. I tested that before answering: I dropped a throwaway YAML with the feed URL into Two things I want to correct in my own PR description first, because they are not arguments for a Python source:
What generic ICS does not handle:
The bigger issue is the URL itself. The feed key is MD5(street + housenumber) uppercased — So an ICS YAML could not offer the 12 zones as This source asks for "Bundesplatz" and "1" instead, derives the key via the site's own address lookup, and turns a wrong house number into a suggestion list rather than a silent HTTP 500. That said — if you'd still rather keep the source catalogue lean, say so and I'll ship it as an ICS YAML instead and drop the Python source. The cost is the |
There was a problem hiding this comment.
🟡 Changes recommended
The new source raises SourceArgumentNotFound for missing required arguments in __init__, which yields misleading UI/error semantics and should use SourceArgumentRequired for the missing field.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
| if not self._key and not (self._strasse and self._hnr): | ||
| raise SourceArgumentNotFound( | ||
| "strasse", | ||
| self._strasse, | ||
| "Either 'key' or both 'strasse' and 'hnr' must be provided.", | ||
| ) |
There was a problem hiding this comment.
Agreed — contributing_source.md says as much explicitly ("It should NOT be thrown when a required argument is not provided"). Fixed in 8f5b233: the constructor now raises SourceArgumentRequired and names the field that is actually missing instead of always pointing at strasse.
nothing given : Argument 'strasse' must be provided, Provide 'strasse' and 'hnr', or 'key' instead.
strasse only : Argument 'hnr' must be provided, ...
hnr only : Argument 'strasse' must be provided, ...
The constructor raised SourceArgumentNotFound when neither `key` nor the `strasse`/`hnr` pair was supplied. That exception is meant for values the API could not resolve, so the user saw "We could not find values for the argument ..." for an argument they had simply not filled in. Raise SourceArgumentRequired instead, and name the field that is actually missing rather than always pointing at `strasse`. Signed-off-by: Simon Bärlocher <s.baerlocher@sbaerlocher.ch>
Summary
Adds
bern_ch.pyplusdoc/source/bern_ch.md, covering the city of Bern's three collection types: Hauskehricht, Grünabfuhr and Altpapiersammlung. Data comes from the city's public iCalendar feed.Closes #3727
Note on #3727: that issue carries the
data: PDFlabel, but the city also publishes an iCalendar feed (the reporter linked it themselves). This source uses the feed, not the PDF — the label can be dropped. The issue asks for all three zones; zones need no configuration here, since the address lookup resolves them internally. The reporter's example address (Morgenstrasse, zone B) resolves correctly.Type of change
Checklist
python -m pytest tests/test_source_components.py -qpassesruff check --fixandruff formatrun on changed source filesdoc/source/<name>.mdcreated for new sourcesData source
ERB publishes a public iCalendar feed:
Plain GET, no auth, no cookie, no session. Three recurring VEVENTs with an RRULE bounded by
UNTIL=<end of current year>, plus EXDATE lines for public holidays. Parsed with the existingservice/ICS.py— no changes to shared code.Key derivation
The key is the uppercase MD5 of the street name concatenated directly with the house number, UTF-8 encoded, no separator, no ZIP, no salt:
Rather than hashing user input blindly, the source resolves the address through the site's own typeahead endpoint:
which returns the canonical street spelling, the house number and the key itself. The returned keys match the MD5 derivation exactly for every address checked, including suffixed numbers and streets with spaces.
The lookup runs first because raw hashing only succeeds on byte-perfect input:
bundesplatz1,Bundesplatz 1andHelvetiaplatz3Aall return HTTP 500 and would fall through to the lookup anyway. Hashing first would therefore be slower for mistyped input and identical otherwise, and a wrong-cased number would produce a valid-looking key whose 500 is indistinguishable from an unknown address — losing the suggestion list. MD5 remains the fallback when the lookup returns nothing.Edge cases verified against the live service
Helvetiaplatz3a. The hash is case-sensitive —MD5(...3a)returns HTTP 200 and matches the server's key,MD5(...3A)returns HTTP 500. The lookup compares case-insensitively, so users may enter either form.MD5("Alter Aargauerstalden2b")matches the server's key. Bern has no "Alte Bernstrasse"; multi-word names are usually hyphenated (Von-Werdt-Passage).SourceArgumentNotFound.SourceArgumentNotFoundWithSuggestionslisting the known numbers.Feed quirks handled in the source
Both handled locally so
service/ICS.pystays untouched — a fix there would affect ~600 other sources and belongs in its own PR:Empty
EXDATE:lines on two of the three events, which icalendar reports as a broken property. Stripped before parsing. The multi-valueEXDATE;VALUE=DATE:...,...,...line parses correctly as-is, so holiday exclusions do come through — verified that 2026-11-23 and 2026-12-25 are absent from the results.RRULE
UNTILis given in UTC (<Dec 31>T230000Z) while the events are all-day, so that instant is 00:00 local on Jan 1 and the recurrence yields a phantom collection on New Year's Day — confirmed against the published calendar as not a collection day. Entries past the feed's ownUNTILdate are dropped. The bound is read from the fetched feed at runtime (no year assumed); this removes a parsing artifact and is not a user-facing time-range filter.Arguments
strasse— street name, e.g.Bundesplatzhnr— house number, e.g.1or3akey— optional; accepts the key from the iKalender link directly and skips address resolution. An escape hatch: if ERB changes the hashing scheme or retires the lookup, users can paste their own link and keep working without waiting for a release.HOW_TO_GET_ARGUMENTS_DESCRIPTION,PARAM_TRANSLATIONSandPARAM_DESCRIPTIONSare provided in de and en.Test cases
Public buildings only, no private residences:
The fourth case exercises the key fallback and returns the same result as the street/number path for the same address. Helvetiaplatz 5 legitimately returns fewer entries — its Hauskehricht runs
BYDAY=MO,THrather thanMO,TU,TH,FR, and its Altpapiersammlung isINTERVAL=2(fortnightly). All three collection types are present.Dates cross-checked against bernentsorgung.glue.ch for the same address, including the holiday exclusions.
test_sources.pypasses with-d(fetch()does not mutate the Source object). pytest: 39 passed.pre-commit run --all-files: all hooks pass.