Compare commits
6 Commits
056c33fe8c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afd7f37323 | ||
|
|
08bb09c418 | ||
|
|
154c3a7af3 | ||
|
|
ac76798d81 | ||
|
|
674cf900c0 | ||
|
|
63644dedc7 |
@@ -1,45 +1,39 @@
|
|||||||
"""Uster Waste integration."""
|
"""Uster Waste integration."""
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.const import Platform, SERVICE_UPDATE_ENTITY
|
from homeassistant.const import Platform
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN, MANUAL_REFRESH_SERVICE
|
||||||
|
from .sensor import UsterWasteDataUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS = [Platform.SENSOR]
|
PLATFORMS = [Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up Uster Waste from a config entry."""
|
"""Set up Uster Waste from a config entry."""
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data
|
||||||
|
|
||||||
# Register manual refresh service
|
# Set up coordinator *once per entry*
|
||||||
hass.services.async_register(
|
coordinator = UsterWasteDataUpdateCoordinator(hass, entry)
|
||||||
DOMAIN,
|
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||||
entry.data.get("name", "uster_waste"),
|
|
||||||
async_handle_manual_refresh
|
|
||||||
)
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
hass.services.async_remove(DOMAIN, MANUAL_REFRESH_SERVICE)
|
||||||
if unload_ok:
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
return unload_ok
|
|
||||||
|
|
||||||
|
|
||||||
async def async_handle_manual_refresh(service):
|
|
||||||
"""Handle manual refresh service call."""
|
|
||||||
entry_id = service.data.get("entry_id")
|
|
||||||
if not entry_id or entry_id not in hass.data[DOMAIN]:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Trigger update for all sensors under this entry
|
|
||||||
await hass.services.async_call(
|
|
||||||
"homeassistant", "update_entity",
|
|
||||||
{"entity_id": [f"sensor.uster_waste_{entry_id}"]},
|
|
||||||
blocking=False,
|
|
||||||
)
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Button platform for Uster Waste."""
|
"""Button platform for Uster Waste."""
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from homeassistant.components.button import ButtonEntity
|
from homeassistant.components.button import ButtonEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@@ -18,12 +17,9 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the button."""
|
"""Set up the button."""
|
||||||
config = entry.data
|
|
||||||
name = config.get("name", "Uster Waste")
|
|
||||||
|
|
||||||
entity = UsterWasteButton(
|
entity = UsterWasteButton(
|
||||||
entry_id=entry.entry_id,
|
entry_id=entry.entry_id,
|
||||||
name=name
|
name=entry.data.get("name", "Uster Waste")
|
||||||
)
|
)
|
||||||
async_add_entities([entity])
|
async_add_entities([entity])
|
||||||
|
|
||||||
@@ -45,22 +41,16 @@ class UsterWasteButton(ButtonEntity):
|
|||||||
|
|
||||||
async def async_press(self) -> None:
|
async def async_press(self) -> None:
|
||||||
"""Handle the button press (manual refresh)."""
|
"""Handle the button press (manual refresh)."""
|
||||||
# Trigger a manual update of the sensor
|
# Trigger a manual update by calling the sensor's update method
|
||||||
hass = self.hass
|
# The sensor will fetch fresh data from the Uster website
|
||||||
coordinator = None
|
entity_registry = self.hass.data["entity_registry"]
|
||||||
|
|
||||||
# Find the coordinator for this entry
|
# Find the sensor entity for this entry
|
||||||
if DOMAIN in hass.data:
|
entity_id = f"sensor.uster_waste_{self._entry_id}"
|
||||||
for entry_id, entry_data in hass.data[DOMAIN].items():
|
|
||||||
if entry_id == self._entry_id and "coordinator" in entry_data:
|
|
||||||
coordinator = entry_data["coordinator"]
|
|
||||||
break
|
|
||||||
|
|
||||||
if coordinator:
|
# Trigger update for the sensor
|
||||||
await coordinator.async_update()
|
await self.hass.services.async_call(
|
||||||
# Force sensor to update
|
"homeassistant", "update_entity",
|
||||||
for entity in coordinator.entities:
|
{"entity_id": entity_id},
|
||||||
await entity.async_update()
|
blocking=False,
|
||||||
entity.async_write_ha_state()
|
)
|
||||||
else:
|
|
||||||
_LOGGER.error("Coordinator not found for manual refresh")
|
|
||||||
|
|||||||
@@ -1,48 +1,37 @@
|
|||||||
"""Config flow for Uster Waste integration."""
|
"""Config flow for Uster Waste."""
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
# Suggested defaults (update these as needed)
|
|
||||||
DEFAULT_NAME = "Uster Waste Schedule"
|
|
||||||
|
|
||||||
DATA_SCHEMA = vol.Schema({
|
DATA_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_NAME, default=DEFAULT_NAME): str,
|
vol.Required(CONF_NAME, default="Uster Waste"): str,
|
||||||
vol.Required("token", default=""): str,
|
vol.Required("token", default="").strip(),
|
||||||
vol.Required("id", default=""): str,
|
vol.Required("id", default="").strip(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class UsterWasteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for Uster Waste."""
|
"""Handle a config flow for Uster Waste."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(self, user_input=None):
|
||||||
self, user_input: dict[str, str] | None = None
|
|
||||||
) -> FlowResult:
|
|
||||||
"""Handle the initial step."""
|
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
|
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
|
||||||
|
|
||||||
token = user_input["token"].strip()
|
token = user_input["token"].strip()
|
||||||
waste_id = user_input["id"].strip()
|
waste_id = user_input["id"].strip()
|
||||||
|
|
||||||
if not token or not waste_id:
|
if not token or not waste_id:
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user", data_schema=DATA_SCHEMA,
|
||||||
data_schema=DATA_SCHEMA,
|
errors={"base": "missing_params"}
|
||||||
errors={"base": "missing_params"},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build the URL (store config, not raw URL)
|
return self.async_create_entry(
|
||||||
config = {
|
title=user_input[CONF_NAME],
|
||||||
|
data={
|
||||||
CONF_NAME: user_input[CONF_NAME],
|
CONF_NAME: user_input[CONF_NAME],
|
||||||
"token": token,
|
"token": token,
|
||||||
"id": waste_id,
|
"id": waste_id,
|
||||||
}
|
}
|
||||||
|
)
|
||||||
return self.async_create_entry(title=user_input[CONF_NAME], data=config)
|
|
||||||
@@ -11,3 +11,5 @@ ATTR_ERROR = "error"
|
|||||||
|
|
||||||
# UI Labels
|
# UI Labels
|
||||||
MANUAL_REFRESH = "manual_refresh"
|
MANUAL_REFRESH = "manual_refresh"
|
||||||
|
|
||||||
|
MANUAL_REFRESH_SERVICE = "refresh_waste_schedule"
|
||||||
@@ -1,237 +1,179 @@
|
|||||||
"""Sensor platform for Uster Waste."""
|
"""Uster Waste Sensor."""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
import asyncio
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorEntity
|
from homeassistant.components.sensor import SensorEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.update_coordinator import (
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
DataUpdateCoordinator,
|
||||||
|
UpdateFailed,
|
||||||
from .const import (
|
|
||||||
DOMAIN,
|
|
||||||
ATTR_NEXT_COLLECTION,
|
|
||||||
ATTR_DATE,
|
|
||||||
ATTR_TYPE,
|
|
||||||
ATTR_DAYS_UNTIL,
|
|
||||||
ATTR_ENTRIES,
|
|
||||||
ATTR_ERROR,
|
|
||||||
MANUAL_REFRESH,
|
|
||||||
)
|
)
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from .const import DOMAIN, MANUAL_REFRESH_SERVICE
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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
|
# For Swiss date parsing
|
||||||
MONTH_MAP = {
|
MONTHS = {
|
||||||
"Jan": "01", "Feb": "02", "Mrz": "03", "Mär": "03",
|
"Jan": "01", "Feb": "02", "Mrz": "03", "Mär": "03",
|
||||||
"Apr": "04", "Mai": "05", "Jun": "06", "Jul": "07",
|
"Apr": "04", "Mai": "05", "Jun": "06", "Jul": "07",
|
||||||
"Aug": "08", "Sep": "09", "Okt": "10", "Nov": "11", "Dez": "12"
|
"Aug": "08", "Sep": "09", "Okt": "10", "Nov": "11", "Dez": "12"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _parse_date(date_str: str) -> Optional[datetime]:
|
def parse_swiss_date(date_str: str) -> Optional[datetime]:
|
||||||
"""Convert Swiss date string (e.g., '24.10.2023' or '24. Okt. 2023') to datetime."""
|
"""Parse Swiss date like '24. Okt. 2023' or '24.10.2023'."""
|
||||||
date_str = date_str.strip()
|
date_str = date_str.strip()
|
||||||
# Normalize Swiss month abbreviations
|
if not date_str:
|
||||||
for key, value in MONTH_MAP.items():
|
return None
|
||||||
date_str = date_str.replace(key, value)
|
# Replace Swiss months (e.g., "Okt" → "10")
|
||||||
# Try formats: dd.mm.yyyy, d.m.yy
|
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"]:
|
for fmt in ["%d.%m.%Y", "%d.%m.%y"]:
|
||||||
try:
|
try:
|
||||||
return datetime.strptime(date_str, fmt)
|
return datetime.strptime(date_str, fmt)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
_LOGGER.warning(f"Could not parse date: '{date_str}'")
|
_LOGGER.warning(f"Failed to parse date: {date_str}")
|
||||||
return None
|
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>)
|
||||||
|
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(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the sensor."""
|
"""Set up the sensor."""
|
||||||
config = entry.data
|
coordinator: UsterWasteDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
token = config["token"]
|
async_add_entities([UsterWasteSensor(entry, coordinator)], update_before_add=True)
|
||||||
waste_id = config["id"]
|
|
||||||
name = config.get("name", "Uster Waste")
|
|
||||||
|
|
||||||
session = async_get_clientsession(hass)
|
|
||||||
coordinator = UsterWasteDataUpdateCoordinator(hass, session, token, waste_id)
|
|
||||||
|
|
||||||
entity = UsterWasteSensor(
|
|
||||||
entry_id=entry.entry_id,
|
|
||||||
coordinator=coordinator,
|
|
||||||
name=name
|
|
||||||
)
|
|
||||||
async_add_entities([entity], update_before_add=True)
|
|
||||||
|
|
||||||
|
|
||||||
class UsterWasteDataUpdateCoordinator:
|
|
||||||
"""Fetch data from Uster website."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
session: aiohttp.ClientSession,
|
|
||||||
token: str,
|
|
||||||
waste_id: str,
|
|
||||||
):
|
|
||||||
self.hass = hass
|
|
||||||
self.session = session
|
|
||||||
self.token = token
|
|
||||||
self.waste_id = waste_id
|
|
||||||
self.data = None
|
|
||||||
self.last_error = None
|
|
||||||
self.last_updated = None
|
|
||||||
|
|
||||||
async def async_update(self) -> dict:
|
|
||||||
"""Fetch data (cache if valid)."""
|
|
||||||
# 1. Check cache (valid for 24h)
|
|
||||||
if self.last_updated and (datetime.now() - self.last_updated) < SCAN_INTERVAL:
|
|
||||||
return self.data
|
|
||||||
|
|
||||||
url = (
|
|
||||||
"https://www.uster.ch/abfallstrassenabschnitt"
|
|
||||||
f"?strassenabschnitt%5B_token%5D={self.token}"
|
|
||||||
f"&strassenabschnitt%5BstrassenabschnittId%5D={self.waste_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with self.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(" \u00a0", " ") # 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"])
|
|
||||||
|
|
||||||
self.data = {
|
|
||||||
"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]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
self.last_updated = datetime.now()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
_LOGGER.error("Error fetching Uster data: %s", e)
|
|
||||||
self.data = {
|
|
||||||
ATTR_ERROR: str(e),
|
|
||||||
"next_collection": None,
|
|
||||||
"date": None,
|
|
||||||
"entries": []
|
|
||||||
}
|
|
||||||
self.last_error = str(e)
|
|
||||||
|
|
||||||
return self.data
|
|
||||||
|
|
||||||
|
|
||||||
class UsterWasteSensor(SensorEntity):
|
class UsterWasteSensor(SensorEntity):
|
||||||
"""Uster Waste Sensor Entity."""
|
"""Uster Waste Collection Sensor."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
_attr_name = None # Use coordinator name
|
||||||
_attr_icon = "mdi:recycle"
|
_attr_icon = "mdi:recycle"
|
||||||
_attr_device_class = None
|
_attr_native_unit_of_measurement = "days" if "days_until" else None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
entry_id: str,
|
entry: ConfigEntry,
|
||||||
coordinator: UsterWasteDataUpdateCoordinator,
|
coordinator: UsterWasteDataUpdateCoordinator,
|
||||||
name: str
|
|
||||||
):
|
):
|
||||||
self._entry_id = entry_id
|
|
||||||
self.coordinator = coordinator
|
self.coordinator = coordinator
|
||||||
self._attr_name = f"{name} Schedule"
|
self._attr_unique_id = f"uster_waste_{entry.entry_id}"
|
||||||
self._attr_unique_id = f"uster_waste_{entry_id}"
|
self._attr_device_info = {
|
||||||
self._attr_extra_state_attributes = {
|
"name": coordinator.name,
|
||||||
ATTR_ENTRIES: [],
|
"identifiers": {(DOMAIN, entry.entry_id)},
|
||||||
}
|
}
|
||||||
|
|
||||||
async def async_update(self):
|
|
||||||
"""Update sensor state."""
|
|
||||||
self._attr_native_value = len(self.coordinator.data.get("entries", []))
|
|
||||||
self._attr_extra_state_attributes.update(
|
|
||||||
{
|
|
||||||
ATTR_NEXT_COLLECTION: self.coordinator.data.get("next_collection"),
|
|
||||||
ATTR_DATE: self.coordinator.data.get("date"),
|
|
||||||
ATTR_TYPE: self.coordinator.data.get("type"),
|
|
||||||
ATTR_DAYS_UNTIL: self.coordinator.data.get("days_until"),
|
|
||||||
ATTR_ENTRIES: self.coordinator.data.get("entries", []),
|
|
||||||
ATTR_ERROR: self.coordinator.data.get("error")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
|
||||||
"""When entity is added to hass."""
|
|
||||||
await super().async_added_to_hass()
|
|
||||||
self.async_on_remove(
|
|
||||||
self.coordinator.async_add_listener(
|
|
||||||
self.async_write_ha_state, self.coordinator.last_updated
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# Force first update
|
|
||||||
await self.async_update()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def native_value(self) -> Optional[str]:
|
||||||
"""Return if entity is available."""
|
"""Return next collection type."""
|
||||||
return self.coordinator.last_updated is not None
|
return self.coordinator.data.get("next_collection")
|
||||||
|
|
||||||
async def async_press(self):
|
@property
|
||||||
"""Handle the button press (manual refresh)."""
|
def extra_state_attributes(self) -> dict:
|
||||||
await self.async_update()
|
"""Return extended attributes."""
|
||||||
self.async_write_ha_state()
|
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()
|
||||||
Reference in New Issue
Block a user