From 08bb09c41869eb16ce8bf4132001c8edc4a4e876 Mon Sep 17 00:00:00 2001 From: Rolf Date: Fri, 13 Feb 2026 21:17:04 +0100 Subject: [PATCH] fix in ui --- custom_components/uster_waste/__init__.py | 30 +- .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 1434 bytes .../__pycache__/button.cpython-313.pyc | Bin 0 -> 2451 bytes .../__pycache__/config_flow.cpython-313.pyc | Bin 0 -> 2053 bytes .../__pycache__/const.cpython-313.pyc | Bin 0 -> 466 bytes .../__pycache__/sensor.cpython-313.pyc | Bin 10531 -> 8970 bytes custom_components/uster_waste/config_flow.py | 43 +-- custom_components/uster_waste/const.py | 2 + custom_components/uster_waste/sensor.py | 286 ++++++++---------- 9 files changed, 170 insertions(+), 191 deletions(-) create mode 100644 custom_components/uster_waste/__pycache__/__init__.cpython-313.pyc create mode 100644 custom_components/uster_waste/__pycache__/button.cpython-313.pyc create mode 100644 custom_components/uster_waste/__pycache__/config_flow.cpython-313.pyc create mode 100644 custom_components/uster_waste/__pycache__/const.cpython-313.pyc diff --git a/custom_components/uster_waste/__init__.py b/custom_components/uster_waste/__init__.py index 84a10ae..dd005ea 100644 --- a/custom_components/uster_waste/__init__.py +++ b/custom_components/uster_waste/__init__.py @@ -3,19 +3,37 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.const import Platform -from .const import DOMAIN - -PLATFORMS = [Platform.SENSOR, Platform.BUTTON] +from .const import DOMAIN, MANUAL_REFRESH_SERVICE +from .sensor import UsterWasteDataUpdateCoordinator +PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Uster Waste from a config entry.""" hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data + # Set up coordinator *once per entry* + coordinator = UsterWasteDataUpdateCoordinator(hass, entry) + hass.data[DOMAIN][entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + # Register manual refresh service + async def handle_refresh(call): + entry_id = call.data.get("entry_id", entry.entry_id) + coord = hass.data[DOMAIN].get(entry_id) + if coord: + await coord.async_request_refresh() + else: + hass.components.persistent_notification.create( + "Uster Waste: No active entry found. Reload the integration.", + title="Uster Waste Error" + ) + + hass.services.async_register(DOMAIN, MANUAL_REFRESH_SERVICE, handle_refresh) return True - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + """Unload config entry.""" + hass.services.async_remove(DOMAIN, MANUAL_REFRESH_SERVICE) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) \ No newline at end of file diff --git a/custom_components/uster_waste/__pycache__/__init__.cpython-313.pyc b/custom_components/uster_waste/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..efc90ebe6608c013ce7bd2d26d06c05c3b73faa1 GIT binary patch literal 1434 zcma)6L2MgE6rI_fUE6CrsnaHemQ)KhYAuMvA*e+yNQMRxyLKvXEDnvbTI_YQ$lkSP z)>QE+v1+3BKatd>S#$VkA>lmBxuhZtwJ;-(YO&=B1UvcGGa@KS=G?aqAv+ep3uev?Z`?Z zlk7}Zm~T`4a|?v-xW2tYE#L9n?7j$_!94zn=hmH-IoGFauuOjDHSHW@4)ZP72SxN| z!}9AMZSLd22KD+vF*jf0WYuDfE4J9kRciZf>Qovk zxdA;qWXG#|m`2b+5_C}NOjO*4XVqS6SVpF!ke^Qs%F_LM#?sfoAdD;xQ&7#+crTUirqaFC)o$wQ*4(2Hc2dP&ytqw@-FWdi6D+)wp2ol6 zcgO=}8kaJoJYt$PuWFk7ja7J?@aP6_Gkcse6sQWPIO#%dDyqUypES74r{aZj1>yh> z2&o!#C1at?)sHKSiwmXTs+@e`d5wS>!G$)_0c=BqTSu=oy;{3re?;E`o%lmA(YwBk zF@A#1@1ogV^zJUY^f!8aFFF35`K|fG>V5A=Z#$7)kM&grpW2x2BM@8hKs+YT#HxQv Y!X>=%VIK*(b#5RZpZOEc-zc|WF!~g&Q literal 0 HcmV?d00001 diff --git a/custom_components/uster_waste/__pycache__/button.cpython-313.pyc b/custom_components/uster_waste/__pycache__/button.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1be01dfe11cc20e59e38fdce5b44cdcb4a2f124 GIT binary patch literal 2451 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 XytQm?=k;ZyliPZrz5CRJj^2C+M>dgW literal 0 HcmV?d00001 diff --git a/custom_components/uster_waste/__pycache__/sensor.cpython-313.pyc b/custom_components/uster_waste/__pycache__/sensor.cpython-313.pyc index 97468486cc9daf7a47fd59570dc2e1e436a477bd..377b7d669d3d93a30988d609d4c04af8b3018b52 100644 GIT binary patch delta 4011 zcmahMZERcB_1^d8=db5Dv6DE79XoNFx*rL&>7ZR3$U534p<(jvD?u?BH-1Tt+~;tg z(>5?j1>IDpPSD*!E5x8;KL*prI`zjor14S4v;v!2x~b9IG@)sK`e!mt5uh<`=RW5p zT{Xc?eD1mDocnP;?z!jvIQzXF+F~&1L(tgWM@!>vC$v`l%@e)f4fsi5G%^yU(UE3) z;x{oRiE=`A3#<7f^SQ((%x54(J9APOB7PFwjA@c+Bt#nEFH9Oq6N!*0X(lbCmBdI} zUy!Cqd#;OglMW&bs3hfZ(ya(q?`lJ&JLlgd zI$I(t_e=|!G&_agI9m}k5%;mjBp`@Ni?n8CQjjuhs}9<<{$CvR@I=%dkfHIBwIDiDxJ%higY`yXsd@f zoe%GV;TQ-=#X_}Ui_=EcZg8eLGl^kZ6-sx2P)4P9Ku7Q7UGaLQ;9dHf+u{A zC%nYphNzDOa{d8<1RYvHM+2B>fU7i^(|{KOUWoHH)t5vRq5}!dGcwR+g?>b?vJ`&MqAV!U(<-}ytQJ`lvZdaX#4Y1Ms;$3o`=4pzb%#xlMZwB z8Xem_kT(m}(m^ACuuz^gsLnactaA>k!GMN1Xy72s0k<=40+12tUXF8fDa2a>O)Q&IN7>~VgSku1%^W5s5JFjr z%>*I}gd55#fVOOcz{`4F;gwz&h)AT(0!G7_$onBs58`LVF*$%tQ5RV)5eX1w%Q!+s zIA@)ZCLT^G_z{{2x+@XYJ%E9AF&koUh;34m&o~{3u#2L~ior0J*uh|`omJ3fCSou8p)NeGeP4QEiYF_*s56U~({ zJrnINUH7E7f^h659sA55q=r7DRDsc9+rXHsT$yaf#a4O}(}O&Fxr!vw?(cvCYIg04jEm_3J*a!N@vG}0 zy=B+2zlAcQn(b%4hWNbdZdp+I^t%2e&NaLTqEP?;{KQxAziNJ`J3j|o$|T9%wKrr5 z{cxqhao~6}q=)LqfZo{-dS@C*^@-mSc1IBM!+o>_Geo-jy!j@2%okA%zv6SKw@}L4dnr7$*Ti@zhZKWsOddT%%5>0GyITP-ihV&5o zXSjLD5;_r)&CpONz0^G078Sa*9?CBIst&~jV4#EFgYpHP9bTadiArNV@|Cz%+5r&%!6OxDV!>q z#eAV$ru#Ts2(&gsckL|2UIJ{wD z!`FVb=9g>EbSyLuo~yq3*spsYJ6Ea6LiiqB6H&0C7C^0?Cm%iW=;@7Tgl8VT(z5YV z%f>T@+2NKEX#hs-cP$qc`7P^m`zU*%_3s-ZS`7t*+O_*JY8|+yq1N@Eh5!Re`2VnF z;}_Sau&1N(v+IqhA-0G-UX8sUTQeWgK0$I&`&Uf{$`{s;;OtM}Q9nL9B#(H|+g?1J zP~P^591jkKhI_^H9e6mUoR3Q!Px*myejOf8OXt_`XoA576dMr&7h)2}<9wwH3C!^n zh6xwa4&JK_-{HBi9uMDgYv4j(fDg6=LFhe?Z&VfE3$Gg$#rL*hjt|PP#QUNQ`1`5| z_z%*}@B#2a>NaH>L#-pTZC$k=0UuefD7@aKgLSkYogexWnRTjNV+$f{k(fk;`bnG@t}3emxjpGb5G5k zmM(_U7u7VqkNvy*HEgh-rP~50x!_U`U>~OY@E;iN`Nmq+sjDafE}i6H4+mKec5$$s z1AgZ+5%!n1LH1bB?VFBr+PxeY0HD+>O+r=6$Bd8F8Cn4YR_APKU!_u}r#R;YwyQnU z1)1W9FmlduXWeEaoXf!|XB=VSRUPvoIrdRwv?fCjHCaPb&BHMjH8t0~9P^=2<8?n^ z*SbQouhz92%i8II>xlQCJRk`2IuKE`8el!A1{M)?i=s>TR2AiP4FPbHm$P?f6@3}D zw>^%}8C%um1-w+*U#-rt*H``4FT>{+Bn4hTv9rD11=|bRuC+S&s3{Suab~tp_T_*|IT6&jF45$hyF`u3jg6 zs;~pAOIyM?wcu;1iO_%KZ?4JEgIQwhY95ZMNbz0w0(O1%T>?%&^X*R%@16Dk3sR~^ AEdT%j delta 5361 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 FlowResult: - """Handle the initial step.""" + async def async_step_user(self, user_input=None): if user_input is None: return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) token = user_input["token"].strip() waste_id = user_input["id"].strip() - if not token or not waste_id: return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, - errors={"base": "missing_params"}, + step_id="user", data_schema=DATA_SCHEMA, + errors={"base": "missing_params"} ) - # Build the URL (store config, not raw URL) - config = { - CONF_NAME: user_input[CONF_NAME], - "token": token, - "id": waste_id, - } - - return self.async_create_entry(title=user_input[CONF_NAME], data=config) + return self.async_create_entry( + title=user_input[CONF_NAME], + data={ + CONF_NAME: user_input[CONF_NAME], + "token": token, + "id": waste_id, + } + ) \ No newline at end of file diff --git a/custom_components/uster_waste/const.py b/custom_components/uster_waste/const.py index 35a4c76..1bf2be2 100644 --- a/custom_components/uster_waste/const.py +++ b/custom_components/uster_waste/const.py @@ -11,3 +11,5 @@ ATTR_ERROR = "error" # UI Labels MANUAL_REFRESH = "manual_refresh" + +MANUAL_REFRESH_SERVICE = "refresh_waste_schedule" \ No newline at end of file diff --git a/custom_components/uster_waste/sensor.py b/custom_components/uster_waste/sensor.py index ecdc640..bbddecf 100644 --- a/custom_components/uster_waste/sensor.py +++ b/custom_components/uster_waste/sensor.py @@ -1,209 +1,179 @@ -"""Sensor platform for Uster Waste.""" -import asyncio +"""Uster Waste Sensor.""" import logging +import re +import asyncio from datetime import datetime, timedelta from typing import Optional import aiohttp from bs4 import BeautifulSoup + from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator, + UpdateFailed, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession - -from .const import ( - DOMAIN, - ATTR_NEXT_COLLECTION, - ATTR_DATE, - ATTR_TYPE, - ATTR_DAYS_UNTIL, - ATTR_ENTRIES, - ATTR_ERROR, - MANUAL_REFRESH, -) +from .const import DOMAIN, MANUAL_REFRESH_SERVICE _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(days=1) +# ✅ CORRECTED URL FORMAT (no %5B, no extra encoded brackets) +BASE_URL = "https://www.uster.ch/abfallstrassenabschnitt" -# Swiss date helpers -MONTH_MAP = { +# For Swiss date parsing +MONTHS = { "Jan": "01", "Feb": "02", "Mrz": "03", "Mär": "03", "Apr": "04", "Mai": "05", "Jun": "06", "Jul": "07", "Aug": "08", "Sep": "09", "Okt": "10", "Nov": "11", "Dez": "12" } -def _parse_date(date_str: str) -> Optional[datetime]: - """Convert Swiss date string (e.g., '24.10.2023' or '24. Okt. 2023') to datetime.""" +def parse_swiss_date(date_str: str) -> Optional[datetime]: + """Parse Swiss date like '24. Okt. 2023' or '24.10.2023'.""" date_str = date_str.strip() - # Normalize Swiss month abbreviations - for key, value in MONTH_MAP.items(): - date_str = date_str.replace(key, value) - # Try formats: dd.mm.yyyy, d.m.yy + if not date_str: + return None + # Replace Swiss months (e.g., "Okt" → "10") + for swiss, num in MONTHS.items(): + date_str = date_str.replace(swiss, num) + # Clean extra spaces/dots + date_str = re.sub(r'\.\s*\.', '.', date_str) + # Try multiple formats for fmt in ["%d.%m.%Y", "%d.%m.%y"]: try: return datetime.strptime(date_str, fmt) except ValueError: continue - _LOGGER.warning(f"Could not parse date: '{date_str}'") + _LOGGER.warning(f"Failed to parse date: {date_str}") return None +class UsterWasteDataUpdateCoordinator(DataUpdateCoordinator[dict]): + """Fetch and cache Uster waste data.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry): + self.hass = hass + self.entry = entry + self.token = entry.data["token"] + self.id = entry.data["id"] + self.name = entry.data.get("name", "Uster Waste") + # ✅ CORRECT URL: + self.url = f"{BASE_URL}?token={self.token}&strassenabschnittId={self.id}" + super().__init__(hass, _LOGGER, name="uster_waste", update_interval=timedelta(days=1)) + + async def _async_update_data(self) -> dict: + """Fetch the latest data from Uster.""" + session = async_get_clientsession(self.hass) + try: + async with session.get(self.url, timeout=10) as resp: + if resp.status != 200: + raise UpdateFailed( + f"HTTP {resp.status} — check token & ID (token may be expired)." + "Get a fresh URL from https://www.uster.ch/abfallstrassenabschnitt" + ) + html = await resp.text() + except aiohttp.ClientError as e: + raise UpdateFailed(f"Network error: {e}") from e + + # Parse HTML + soup = BeautifulSoup(html, "html.parser") + + # Find the table (robust: class="table table-striped" or any ) + table = soup.find("table", class_="table table-striped") or soup.find("table") + if not table: + raise UpdateFailed("No waste schedule table found on Uster.ch page.") + + rows = table.find_all("tr") + if len(rows) < 2: + raise UpdateFailed("Table has no data rows — check page layout changed.") + + now = datetime.now() + entries = [] + + for row in rows[1:4]: # First 3 data rows + cols = row.find_all("td") + if len(cols) < 2: + continue + + collection = cols[0].get_text(strip=True) + date_str = cols[1].get_text(strip=True) + + dt = parse_swiss_date(date_str) + if not dt: + _LOGGER.warning(f"Skipping row: invalid date '{date_str}'") + continue + + entries.append({ + "type": collection, + "date": date_str, + "days_until": (dt - now).days + }) + + if not entries: + raise UpdateFailed("No valid dates found in table.") + + # Sort ascending by date + entries.sort(key=lambda x: x["date"]) + + return { + "next_collection": entries[0]["type"], + "date": entries[0]["date"], + "days_until": entries[0]["days_until"], + "entries": entries, + } + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor.""" - config = entry.data - token = config["token"] - waste_id = config["id"] - name = config.get("name", "Uster Waste") - - entity = UsterWasteSensor( - entry_id=entry.entry_id, - token=token, - waste_id=waste_id, - name=name - ) - async_add_entities([entity]) + coordinator: UsterWasteDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([UsterWasteSensor(entry, coordinator)], update_before_add=True) class UsterWasteSensor(SensorEntity): - """Uster Waste Sensor Entity.""" + """Uster Waste Collection Sensor.""" _attr_has_entity_name = True + _attr_name = None # Use coordinator name _attr_icon = "mdi:recycle" - _attr_device_class = None + _attr_native_unit_of_measurement = "days" if "days_until" else None def __init__( self, - entry_id: str, - token: str, - waste_id: str, - name: str + entry: ConfigEntry, + coordinator: UsterWasteDataUpdateCoordinator, ): - self._entry_id = entry_id - self.token = token - self.waste_id = waste_id - self._attr_name = f"{name} Schedule" - self._attr_unique_id = f"uster_waste_{entry_id}" - self._attr_extra_state_attributes = { - ATTR_ENTRIES: [], + self.coordinator = coordinator + self._attr_unique_id = f"uster_waste_{entry.entry_id}" + self._attr_device_info = { + "name": coordinator.name, + "identifiers": {(DOMAIN, entry.entry_id)}, } - self.data = None - - async def async_update(self): - """Update sensor state.""" - # Fetch fresh data - data = await self._fetch_data() - self.data = data - self._attr_native_value = len(data.get("entries", [])) - self._attr_extra_state_attributes.update( - { - ATTR_NEXT_COLLECTION: data.get("next_collection"), - ATTR_DATE: data.get("date"), - ATTR_TYPE: data.get("type"), - ATTR_DAYS_UNTIL: data.get("days_until"), - ATTR_ENTRIES: data.get("entries", []), - ATTR_ERROR: data.get("error") - } - ) - - async def _fetch_data(self) -> dict: - """Fetch data from Uster website.""" - try: - session = async_get_clientsession(self.hass) - url = ( - "https://www.uster.ch/abfallstrassenabschnitt" - f"?strassenabschnitt%5B_token%5D={self.token}" - f"&strassenabschnitt%5BstrassenabschnittId%5D={self.waste_id}" - ) - - async with session.get(url, timeout=10) as response: - if response.status == 403 or response.status == 404: - raise Exception( - "Token expired or invalid. " - "Please get a fresh URL from https://www.uster.ch/abfallstrassenabschnitt" - ) - response.raise_for_status() - html = await response.text() - - # Parse HTML - soup = BeautifulSoup(html, "html.parser") - table = soup.find("table", class_="table table-striped") - if not table: - table = soup.find("table") - if not table: - raise ValueError("No table found on page.") - - rows = table.find_all("tr") - if len(rows) < 2: - raise ValueError("Table has no data rows.") - - entries = [] - now = datetime.now() - - for row in rows[1:4]: # Next 3 entries - cols = row.find_all("td") - if len(cols) < 2: - continue - - collection_type = cols[0].get_text(strip=True) - date_str = cols[1].get_text(strip=True).replace("  ", " ") # Clean no-break space - dt = _parse_date(date_str) - if not dt: - _LOGGER.warning(f"Skipping row with invalid date: {date_str}") - continue - - entries.append({ - "Sammlung": collection_type, - "Wann?": date_str, - "date_obj": dt, - "days_until": (dt - now).days - }) - - # Sort by date (ascending) - entries.sort(key=lambda x: x["date_obj"]) - - return { - "next_collection": entries[0]["Sammlung"] if entries else None, - "date": entries[0]["Wann?"] if entries else None, - "type": entries[0]["Sammlung"] if entries else None, - "days_until": entries[0]["days_until"] if entries else None, - "entries": [ - { - "type": e["Sammlung"], - "date": e["Wann?"], - "days_until": e["days_until"] - } - for e in entries[:3] - ] - } - except Exception as e: - _LOGGER.error("Error fetching Uster data: %s", e) - return { - ATTR_ERROR: str(e), - "next_collection": None, - "date": None, - "entries": [] - } - - async def async_added_to_hass(self): - """When entity is added to hass.""" - await super().async_added_to_hass() - # Force first update - await self.async_update() @property - def available(self) -> bool: - """Return if entity is available.""" - return self.data is not None + def native_value(self) -> Optional[str]: + """Return next collection type.""" + return self.coordinator.data.get("next_collection") - async def async_press(self): - """Handle the button press (manual refresh).""" - await self.async_update() - self.async_write_ha_state() + @property + def extra_state_attributes(self) -> dict: + """Return extended attributes.""" + data = self.coordinator.data or {} + return { + "date": data.get("date"), + "days_until": data.get("days_until"), + "entries": data.get("entries", []), + "url": self.coordinator.url, # For debugging + } + + async def async_update(self) -> None: + """Manually trigger update (used by refresh service).""" + await self.coordinator.async_request_refresh() \ No newline at end of file