Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
dc58aed
chore: untrack .planning/ROADMAP.md
Pascal-ZeGerman Jul 27, 2026
4ce72ef
chore: untrack remaining legacy .planning/ files
Pascal-ZeGerman Jul 27, 2026
dfa1a5a
Merge remote-tracking branch 'origin/main'
Pascal-ZeGerman Jul 28, 2026
a9dcf0f
feat(41-03): add static demo page shell with pinned+SRI Leaflet
Pascal-ZeGerman Jul 29, 2026
c634ad0
test(41-01): add failing offline tests for demo dataset dumper
Pascal-ZeGerman Jul 29, 2026
2805f3e
feat(41-03): add dark-theme demo stylesheet
Pascal-ZeGerman Jul 29, 2026
0567df2
feat(41-01): implement offline demo dataset dumper
Pascal-ZeGerman Jul 29, 2026
a06f131
chore: merge executor worktree (worktree-agent-afefd087bfb078b8c)
Pascal-ZeGerman Jul 29, 2026
fb2df93
chore: merge executor worktree (worktree-agent-a25e28975eba6b2ba)
Pascal-ZeGerman Jul 29, 2026
c5b328d
feat(41-04): add demo client controller (map, HA card, next-move cale…
Pascal-ZeGerman Jul 29, 2026
72c155a
feat(41-02): generate committed demo dataset snapshot
Pascal-ZeGerman Jul 29, 2026
df047bc
chore: merge executor worktree (worktree-agent-ad81734d3ae05f2d3)
Pascal-ZeGerman Jul 29, 2026
337afd7
chore: merge executor worktree (worktree-agent-a7f086f09bb90af19)
Pascal-ZeGerman Jul 29, 2026
7a8b08e
feat(41-05): add GitHub Pages workflow to publish docs/ demo
Pascal-ZeGerman Jul 29, 2026
573243b
docs(41-05): document the live demo in README
Pascal-ZeGerman Jul 29, 2026
2c70052
chore: merge executor worktree (worktree-agent-a43ac62e0f908aecd)
Pascal-ZeGerman Jul 29, 2026
0629ee0
fix(41-02): replace unresolvable demo points and populate ASPActiveNo…
Pascal-ZeGerman Jul 30, 2026
9a154fd
fix(41-02): populate cleaning_days/schedule_summary for asp_active_no…
Pascal-ZeGerman Jul 30, 2026
76f1e4e
feat(42-03): add coverage explorer page shell (index.html)
Pascal-ZeGerman Jul 31, 2026
450df5c
test(42-01): add failing grouping-key + tier-partition tests
Pascal-ZeGerman Jul 31, 2026
d6c9fa4
feat(42-03): add coverage explorer stylesheet (styles.css)
Pascal-ZeGerman Jul 31, 2026
fe7e383
feat(42-01): implement coverage-dataset dumper deterministic core
Pascal-ZeGerman Jul 31, 2026
5f02b19
chore: merge executor worktree (worktree-agent-a2c4ae53fd75ddfa0)
Pascal-ZeGerman Aug 1, 2026
c567247
chore: merge executor worktree (worktree-agent-af0b6c0c17beb5ada)
Pascal-ZeGerman Aug 1, 2026
992581f
feat(42-04): canvas map init, dataset load/error, freshness, tier col…
Pascal-ZeGerman Aug 1, 2026
514ee45
feat(42-04): popup builder — fields, link-outs, explicit unresolved s…
Pascal-ZeGerman Aug 1, 2026
554230c
test(42-02): add grouped-resolve, per-segment-entry, and zero-record …
Pascal-ZeGerman Aug 1, 2026
9fedff3
feat(42-04): four AND filters + no-results state + GeoJSON export
Pascal-ZeGerman Aug 1, 2026
4183c9b
feat(42-02): grouped SODA resolve + worst-case-side pick (R1)
Pascal-ZeGerman Aug 1, 2026
39162b6
style(42-02): wrap long expected_count line to satisfy ruff format
Pascal-ZeGerman Aug 1, 2026
8094aa9
test(42-02): coverage.json writer + token-safety guards (R1, T-42-01)
Pascal-ZeGerman Aug 1, 2026
087017e
chore: merge executor worktree (worktree-agent-ade246ca4d940074e)
Pascal-ZeGerman Aug 1, 2026
26df1db
chore: merge executor worktree (worktree-agent-ae627b39b20642574)
Pascal-ZeGerman Aug 1, 2026
f4bc203
fix(caldav): resolve iCloud cross-host calendar sharding in _get_cale…
Pascal-ZeGerman Aug 1, 2026
2510d18
chore(release): bump version to 3.3.0-rc2
Pascal-ZeGerman Aug 1, 2026
62062b0
fix(caldav): narrow ValueError catch in _get_calendar fallback
Pascal-ZeGerman Aug 1, 2026
87ee1bb
chore(release): bump version to 3.3.0-rc3
Pascal-ZeGerman Aug 1, 2026
228fa94
style(tests): apply ruff format to test_caldav_sync.py
Pascal-ZeGerman Aug 1, 2026
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
52 changes: 52 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Deploy demo to GitHub Pages

# Publishes the already-committed static demo site under docs/ to GitHub Pages.
# This workflow deliberately does NOT run the precompute: the 95 MB spatial index
# is gitignored and the committed docs/demo/data/*.json snapshot is published as-is.
# The repo's other workflows run on self-hosted runners, but the Pages deploy
# actions require GitHub-hosted runners, so this uses ubuntu-latest.

on:
push:
branches: [main]
paths:
- "docs/**"
- ".github/workflows/pages.yml"
workflow_dispatch:

# Least-privilege permissions required by actions/deploy-pages.
permissions:
contents: read
pages: write
id-token: write

# Allow one concurrent deployment; don't cancel an in-progress publish.
concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
name: Package docs/ artifact
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Upload docs/ as Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs

deploy:
name: Deploy to GitHub Pages
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy artifact
id: deployment
uses: actions/deploy-pages@v4
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Supports all five NYC boroughs. Data is fetched live from NYC Open Data.

## Table of Contents

- [Live Demo](#live-demo)
- [Installation](#installation)
- [Configuration](#configuration)
- [Requirements](#requirements)
Expand All @@ -24,6 +25,57 @@ Supports all five NYC boroughs. Data is fetched live from NYC Open Data.

---

## Live Demo

Want to see what the integration produces before installing anything? A self-contained
demo page lives under [`docs/demo/`](docs/demo/). Open it in a browser, click a block on the
Leaflet map, and you'll see exactly what ASP Parking resolves for that location: the parking
rule for the block, the next time you'd need to move your car, the exact Home Assistant sensor
entities and states it would create, and an animated calendar of the weekly cleaning windows.
It's a plain HTML/CSS/JS page — no build step, no install, no server-side code.

**Where the data comes from.** The demo does **not** call any live API from the browser.
It reads a **dated snapshot** committed to the repo at
[`docs/demo/data/demo.json`](docs/demo/data/demo.json) (snapshot date: **2026-07-28**),
plus the matched segment geometries in `docs/demo/data/demo-segments.geojson`. The snapshot
stores *weekly recurring patterns* rather than absolute datetimes, and the page recomputes the
next move time in your browser at NYC time — so the "next move" stays correct even though the
underlying data is frozen. Because it's a static snapshot, the demo works offline and never
needs a token.

**Regenerating the snapshot.** The dataset is produced offline (not in CI) by
`scripts/build_demo_dataset.py`. From a checkout with the project installed:

```bash
.venv/bin/python scripts/build_demo_dataset.py --out-dir docs/demo/data
```

This requires two things the demo page itself does not: the **spatial index** must be present
locally, and the script needs **network access to the NYC Open Data (SODA) API**. The index is
gitignored (~95 MB), so build it once with `python scripts/build_index.py` **or** download the
released `index-v1` asset from the [Releases page](https://github.com/Pascal-ZeGerman/GPS2ASP-Resolver/releases).
Setting a `NYC_OPEN_DATA_APP_TOKEN` environment variable is optional but helps avoid SODA rate
limiting. The generated `demo.json`/`demo-segments.geojson` are the only files committed — the
index never is.

**Running it locally.** Serve the folder over HTTP (opening `index.html` via `file://` won't
let the page `fetch()` its JSON):

```bash
python -m http.server --directory docs/demo 8000
```

Then visit <http://localhost:8000/>.

**Hosting it.** The repo ships a [`.github/workflows/pages.yml`](.github/workflows/pages.yml)
workflow that publishes the committed `docs/` tree to **GitHub Pages** on every push to `docs/`
(and on demand via *workflow_dispatch*). The workflow publishes the snapshot as-is and never
runs the precompute. A maintainer only needs to enable Pages once, under
**Settings → Pages → Source: GitHub Actions**; after that the demo is reachable at the
repository's GitHub Pages URL.

---

## Installation

### Via HACS (recommended)
Expand Down
53 changes: 45 additions & 8 deletions custom_components/asp_parking/caldav_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from datetime import datetime, timezone
from typing import Any
from urllib.parse import quote as _url_quote
from urllib.parse import urlparse

import caldav # top-level package — present on all caldav versions
from caldav.lib import error as caldav_error
Expand Down Expand Up @@ -547,17 +548,53 @@ def build_vevent_ical(
async def _get_calendar(client: Any, calendar_url: str) -> Any:
"""Resolve a calendar by URL using the authenticated principal.

Uses ``principal.calendar(cal_url=...)`` for single-calendar lookup
(no extra collection roundtrip — the principal already knows the
calendar-home-set URL after ``get_principal``).
Tries ``principal.calendar(cal_url=...)`` first for single-calendar
lookup (no extra collection roundtrip — the principal already knows
the calendar-home-set URL after ``get_principal``). This is a
**synchronous** constructor in both caldav 2.x (via the
``_CompatPrincipal`` shim) and caldav 3.x (``AsyncPrincipal.calendar``):
it builds a Calendar object from the URL without any network I/O, so
it must NOT be awaited.

iCloud shards each account's calendar data onto a per-account host
(e.g. ``p117-caldav.icloud.com``) that differs from the generic login
entry point (``caldav.icloud.com``) ``client`` is constructed with.
caldav 2.x's ``Principal.calendar(cal_url=...)`` resolves the URL via
a purely local ``self.client.url.join(cal_url)`` (caldav/lib/url.py)
which raises ``ValueError`` whenever the two hosts differ — always,
for iCloud, since the stored ``calendar_url`` (captured during
``list_calendars()``, which follows the redirect over the network) is
on the sharded host while ``client.url`` stays pinned to the login
host. On that specific failure, fall back to ``principal.calendars()``
— a real network request that also follows the redirect, so the
returned Calendar objects carry the correct host — and match by URL
path, the one thing stable across iCloud's host sharding.

Only the specific ``URL.join`` cross-host failure triggers the
fallback — any other ``ValueError`` (e.g. a ``None`` client, or a
calendar_url containing spaces) is a genuine, unrelated bug and must
propagate unchanged rather than being misreported as "no calendar
found".

Note: ``principal.calendar()`` is a **synchronous** constructor in both
caldav 2.x (via the ``_CompatPrincipal`` shim) and caldav 3.x
(``AsyncPrincipal.calendar``). It builds a Calendar object from the URL
without any network I/O, so it must NOT be awaited.
Raises:
CalDAVWriteError: if the fallback finds no calendar whose path
matches ``calendar_url``.
"""
principal = await client.get_principal()
return principal.calendar(cal_url=calendar_url)
try:
return principal.calendar(cal_url=calendar_url)
except ValueError as exc:
if "can't be joined with" not in str(exc):
raise
target_path = urlparse(calendar_url).path.rstrip("/")
calendars = await principal.calendars()
for cal in calendars:
if urlparse(str(cal.url)).path.rstrip("/") == target_path:
return cal
raise CalDAVWriteError(
f"No calendar found matching path {target_path!r} on this "
f"principal (configured calendar_url={calendar_url!r})"
) from exc


def _build_event_url(calendar_url: Any, uid: str) -> str:
Expand Down
2 changes: 1 addition & 1 deletion custom_components/asp_parking/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"issue_tracker": "https://github.com/Pascal-ZeGerman/GPS2ASP-Resolver/issues",
"requirements": ["pyproj>=3.7.0", "rtree>=1.4.0", "shapely>=2.1.0", "numpy", "httpx>=0.28.0", "zstandard>=0.21.0", "icalendar>=6.3.1", "caldav==2.1.0"],
"single_config_entry": true,
"version": "3.3.0-rc1"
"version": "3.3.0-rc3"
}
Empty file added docs/.nojekyll
Empty file.
Loading
Loading