Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions uk_bin_collection/tests/input.json
Original file line number Diff line number Diff line change
Expand Up @@ -2038,12 +2038,13 @@
"LAD24CD": "E07000064"
},
"RotherhamCouncil": {
"uprn": "100050866000",
"url": "https://www.rotherham.gov.uk/bin-collections?address=100050866000&submit=Submit",
"wiki_command_url_override": "https://www.rotherham.gov.uk/bin-collections?address=XXXXXXXXX&submit=Submit",
"LAD24CD": "E08000018",
"paon": "77",
"postcode": "S60 1JD",
"skip_get_url": true,
"url": "https://www.rotherham.gov.uk/",
"wiki_name": "Rotherham",
"wiki_note": "Replace `XXXXXXXXX` with your UPRN in the URL. You can find your UPRN using [FindMyAddress](https://www.findmyaddress.co.uk/search).",
"LAD24CD": "E08000018"
"wiki_note": "Provide your postcode and house number (paon). Backend is the shared Imactivate API at bins.azurewebsites.net (same data the Rotherham Bins Android app uses). Rotherham's own bin-day page only links to PDF calendars."
},
"RoyalBoroughofGreenwich": {
"house_number": "57",
Expand Down Expand Up @@ -2874,4 +2875,4 @@
"wiki_note": "Provide your UPRN.",
"LAD24CD": "E06000014"
}
}
}
158 changes: 111 additions & 47 deletions uk_bin_collection/uk_bin_collection/councils/RotherhamCouncil.py
Original file line number Diff line number Diff line change
@@ -1,89 +1,153 @@
from uk_bin_collection.uk_bin_collection.common import *
from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass
import requests
from datetime import datetime

import requests

from uk_bin_collection.uk_bin_collection.common import *
from uk_bin_collection.uk_bin_collection.get_bin_data import (
AbstractGetBinDataClass,
)


class CouncilClass(AbstractGetBinDataClass):
"""
Rotherham collections via the public JSON API.
Returns the same shape as before:
{"bins": [{"type": "Black Bin", "collectionDate": "Tuesday, 29 September 2025"}, ...]}
Accepts kwargs['premisesid'] (recommended) or a numeric kwargs['uprn'].
Rotherham collections via Imactivate's shared `bins.azurewebsites.net` API.
Rotherham's own bin-day page directs residents to a printed PDF calendar
only — there is no usable web lookup at rotherham.gov.uk. The same data
backs the Rotherham Bins Android app and is exposed on the Imactivate
shared instance keyed by PremiseID + LocalAuthority.

Resolution order:
1. explicit `premisesid` kwarg (Imactivate ID, NOT a UPRN)
2. `postcode` + `paon` resolved through getaddress
3. numeric `uprn` only if it is in fact an Imactivate PremiseID
(kept for backward compatibility with old configs — most UPRNs
will yield no collections from this endpoint)
"""

BASE = "https://bins.azurewebsites.net/api"
LA = "Rotherham"
UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/132.0.0.0 Safari/537.36"
)

def _resolve_premise(self, postcode: str, paon: str) -> str:
params = {"postcode": postcode, "localauthority": self.LA}
r = requests.get(
f"{self.BASE}/getaddress",
params=params,
headers={"User-Agent": self.UA},
timeout=15,
)
r.raise_for_status()
rows = r.json() or []

target = str(paon).strip().lower()
if not target:
if not rows:
raise ValueError(
f"No addresses found for postcode {postcode}"
)
return str(rows[0].get("PremiseID"))

# Match against Address2 (house number/name) first, then Street.
for row in rows:
for key in ("Address2", "Address1", "Street"):
value = row.get(key)
if value is None:
continue
if str(value).strip().lower() == target:
return str(row.get("PremiseID"))

# Looser substring fallback so addresses like "Flat 3, 22A" match
# against a paon of "22A".
for row in rows:
blob = " ".join(
str(row.get(k, "")).strip()
for k in ("Address1", "Address2", "Street")
).lower()
if target and target in blob:
return str(row.get("PremiseID"))

raise ValueError(
f"No address matching '{paon}' for postcode {postcode}"
)

def parse_data(self, page: str, **kwargs) -> dict:
# prefer explicit premisesid, fallback to uprn (if numeric)
premises = kwargs.get("premisesid")
uprn = kwargs.get("uprn")

if uprn:
# preserve original behaviour where check_uprn exists for validation,
# but don't fail if uprn is intended as a simple premises id number.
try:
check_uprn(uprn)
except Exception:
# silently continue — user may have passed a numeric premises id as uprn
pass
if not premises:
postcode = kwargs.get("postcode")
paon = kwargs.get("paon")
if postcode:
check_postcode(postcode)
premises = self._resolve_premise(postcode, paon or "")

if not premises and str(uprn).strip().isdigit():
if not premises:
uprn = kwargs.get("uprn")
if uprn and str(uprn).strip().isdigit():
premises = str(uprn).strip()

if not premises:
raise ValueError("No premises ID supplied. Pass 'premisesid' in kwargs or a numeric 'uprn'.")

api_url = "https://bins.azurewebsites.net/api/getcollections"
params = {
"premisesid": str(premises),
"localauthority": kwargs.get("localauthority", "Rotherham"),
}
headers = {
"User-Agent": "UKBinCollectionData/1.0 (+https://github.com/robbrad/UKBinCollectionData)"
}
raise ValueError(
"Rotherham requires either an Imactivate `premisesid` or a "
"`postcode` (plus optionally `paon`) to resolve one."
)

params = {"premisesid": str(premises), "localauthority": self.LA}
try:
resp = requests.get(api_url, params=params, headers=headers, timeout=10)
resp = requests.get(
f"{self.BASE}/getcollections",
params=params,
headers={"User-Agent": self.UA},
timeout=15,
)
except Exception as exc:
print(f"Error contacting Rotherham API: {exc}")
return {"bins": []}

if resp.status_code != 200:
print(f"Rotherham API request failed ({resp.status_code}). URL: {resp.url}")
print(
f"Rotherham API request failed ({resp.status_code}). "
f"URL: {resp.url}"
)
return {"bins": []}

try:
collections = resp.json()
collections = resp.json() or []
except ValueError:
print("Rotherham API returned non-JSON response.")
return {"bins": []}

data = {"bins": []}
seen = set() # dedupe identical (type, date) pairs
seen = set()
for item in collections:
bin_type = item.get("BinType") or item.get("bintype") or "Unknown"
date_str = item.get("CollectionDate") or item.get("collectionDate")
bin_type = (
item.get("BinType") or item.get("bintype") or "Unknown"
)
date_str = (
item.get("CollectionDate") or item.get("collectionDate")
)
if not date_str:
continue

# API gives ISO date like '2025-09-29' (or possibly '2025-09-29T00:00:00').
try:
iso_date = date_str.split("T")[0]
parsed = datetime.strptime(iso_date, "%Y-%m-%d")
formatted = parsed.strftime(date_format)
except Exception:
# skip malformed dates
continue

key = (bin_type.strip().lower(), formatted)
if key in seen:
continue
seen.add(key)

dict_data = {"type": bin_type.title(), "collectionDate": formatted}
data["bins"].append(dict_data)

if not data["bins"]:
# helpful debugging note
print(f"Rotherham API returned no collection entries for premisesid={premises}")

return data
data["bins"].append(
{"type": bin_type, "collectionDate": formatted}
)

data["bins"].sort(
key=lambda x: datetime.strptime(
x["collectionDate"], date_format
)
)
return data