Scripting local Library Website

My local library sends me an email when a book is due three days in advance. But I want this information in Homeassistant, of course without manually inserting the date there.

By using playwright I login to their website and get the first entry of the books I borrowed. The secrets (username and password) are stored in a json file and loaded at the beginning. When successfully retrieved the next return date, an upload function is called with the date. The code for this function is at the end of this post.

Code to get the next return date for my local library:

import asyncio
import datetime
import json

from playwright.async_api import async_playwright

async def get_return_dates():
    async with async_playwright() as p:
        # load username / password from a json file
        # format: {"username": "0123456", "password": "your_password"}
        secrets = json.load(open(".secrets.json"))

        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto("https://stadtbibliothek-stuttgart.de")

        # go to login mask
        await page.get_by_role("link", name="Mein Konto").click()
        # fill login form
        await page.locator("#IDENT_1").fill(secrets.get("username"))
        await page.locator("#LPASSW_1").fill(secrets.get("password"))
        # click on Anmelden button
        await page.get_by_role("button", name="Anmelden").click()
        # goto list of rentals
        await page.get_by_role("link", name="Ausleihen").click()
        # there is only one table; this could be more granular - currently this is good enough
        lines = await page.query_selector_all("tr")
        for item in lines:
            columns = await item.query_selector_all("td")
            # filter empty columns
            if len(columns):
                # second column is the date
                dt = datetime.datetime.strptime(
                    (await columns[1].inner_html()).strip(), "%d.%m.%Y"
                )
                date = str(dt.date())
                # print result
                print("next_return_date:", date))
                # upload to homeassistant
                upload(date)
                # first line with a date is all we need
                break
        else:
            # nothing found; untested - because I have no empty list here atm
            print("no return date found.")

        await browser.close()

asyncio.run(get_return_dates())

Add this trigger to your Homeassistant configuration.yaml:

trigger:

  - trigger:
      - platform: webhook
        webhook_id: !secret stadtbib
        allowed_methods:
          - POST
    unique_id: "stadtbib"
    sensor:
      - name: "next return date"
        state: "{{ trigger.json.stadtbib_next_return }}"
        device_class: date
        unique_id: "stadtbib_next_return"

And add a (unique) secret to use in the upload function.

Function references above upload the date to Homeassistant. The code needs httpx (or requests by replacing every httpx with requests).

def upload(date: datetime.date):
    import httpx

    r = httpx.post(
        f"http://IP_OF_HOMEASSISTANT:8123/api/webhook/your_unique_secret",
        json={"stadtbib_next_return": date},
    )
    assert r.status_code == 200

Calling this once a day is enough. The next return date could only change when I return the book with the earliest return date.

Of course, this pattern can be used for any other website to get the desired information into Homeassistant. Some time ago I scraped the water levels of rivers in Baden Württemberg with playwright (code). I didn't use the data over years so I stopped the Github Action that downloaded the data. But scraping the river near your home could be an interesting datapoint to upload to Homeassistant.