Skip to content

Commit 62a884a

Browse files
author
Sergey
committed
Merge remote-tracking branch 'origin/master' into feat/ha-get-states-bulk
# Conflicts: # src/ha_mcp/tools/tools_search.py
2 parents bc3d772 + 99cec2f commit 62a884a

55 files changed

Lines changed: 3726 additions & 1210 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/e2e-tests.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ permissions:
2020
env:
2121
PYTHON_VERSION: "3.13"
2222
UV_CACHE_DIR: /tmp/.uv-cache
23+
# renovate: datasource=docker depName=ghcr.io/home-assistant/home-assistant
24+
HA_IMAGE_GHCR: "ghcr.io/home-assistant/home-assistant:2026.1.3"
2325

2426
jobs:
2527
# Comprehensive E2E validation for main branch
@@ -33,6 +35,8 @@ jobs:
3335

3436
- name: Set up Docker Buildx
3537
uses: docker/setup-buildx-action@v3
38+
with:
39+
cache-binary: true
3640

3741
- name: Install uv
3842
uses: astral-sh/setup-uv@v7
@@ -45,6 +49,37 @@ jobs:
4549
- name: Install dependencies
4650
run: uv sync --all-extras --dev
4751

52+
- name: Cache HA Docker image
53+
id: cache-ha-image
54+
uses: actions/cache@v4
55+
with:
56+
path: /tmp/ha-image.tar
57+
key: ha-image-${{ env.HA_IMAGE_GHCR }}-${{ runner.arch }}
58+
59+
- name: Load cached HA image
60+
if: steps.cache-ha-image.outputs.cache-hit == 'true'
61+
run: docker load -i /tmp/ha-image.tar
62+
63+
- name: Pull HA image (GHCR → Docker Hub fallback)
64+
if: steps.cache-ha-image.outputs.cache-hit != 'true'
65+
run: |
66+
HA_VERSION="${HA_IMAGE_GHCR##*:}"
67+
HA_IMAGE_DOCKERHUB="homeassistant/home-assistant:${HA_VERSION}"
68+
for registry in "$HA_IMAGE_GHCR" "$HA_IMAGE_DOCKERHUB"; do
69+
echo "Trying $registry..."
70+
if docker pull "$registry"; then
71+
if [ "$registry" != "$HA_IMAGE_GHCR" ]; then
72+
docker tag "$registry" "$HA_IMAGE_GHCR"
73+
fi
74+
docker save "$HA_IMAGE_GHCR" -o /tmp/ha-image.tar
75+
echo "Pulled and cached from $registry"
76+
exit 0
77+
fi
78+
echo "Failed to pull from $registry, trying next..."
79+
sleep 15
80+
done
81+
echo "All registries failed" && exit 1
82+
4883
- name: Run full E2E test suite
4984
run: |
5085
echo "🚀 Running full E2E test suite with 3 workers..."

.github/workflows/notify-dev-channel.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,14 @@ jobs:
3232
# Extract PR number from commit message
3333
# GitHub merge commits have format: "Merge pull request #123 from user/branch"
3434
# GitHub squash merges have format: "Title with (#123)" in first line
35-
# Take only the first match to avoid capturing PR references in the title
36-
PR_NUM=$(echo "$COMMIT_MSG" | head -1 | grep -oP '(?:Merge pull request #|[[(]#)\K\d+' | head -1 || echo "")
35+
# For merge commits, always use the "Merge pull request #" pattern (handles branch names with #)
36+
# For squash merges with multiple refs like "(#591) (#592)", use the last one (merged PR)
37+
FIRST_LINE=$(echo "$COMMIT_MSG" | head -1)
38+
if [[ "$FIRST_LINE" =~ ^Merge\ pull\ request\ \#([0-9]+) ]]; then
39+
PR_NUM="${BASH_REMATCH[1]}"
40+
else
41+
PR_NUM=$(echo "$FIRST_LINE" | grep -oP '\(#\K\d+' | tail -1 || echo "")
42+
fi
3743
3844
if [ -z "$PR_NUM" ]; then
3945
echo "No PR number found in commit message. Skipping notification."

.github/workflows/pr.yml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,43 @@ on:
1010
env:
1111
PYTHON_VERSION: "3.13"
1212
UV_CACHE_DIR: /tmp/.uv-cache
13+
# renovate: datasource=docker depName=ghcr.io/home-assistant/home-assistant
14+
HA_IMAGE_GHCR: "ghcr.io/home-assistant/home-assistant:2026.1.3"
1315

1416
jobs:
17+
lint:
18+
name: Ruff Lint
19+
runs-on: ubuntu-latest
20+
container:
21+
image: ghcr.io/astral-sh/uv:0.9.30-python3.13-bookworm-slim
22+
timeout-minutes: 5
23+
24+
steps:
25+
- uses: actions/checkout@v6
26+
27+
- name: Install dependencies
28+
run: uv sync --dev
29+
30+
- name: Run ruff check
31+
run: uv run ruff check src/ tests/
32+
33+
# Fast unit tests (no Docker, no HA instance needed)
34+
unit-tests:
35+
name: Unit Tests
36+
runs-on: ubuntu-latest
37+
container:
38+
image: ghcr.io/astral-sh/uv:0.9.30-python3.13-bookworm-slim
39+
timeout-minutes: 5
40+
41+
steps:
42+
- uses: actions/checkout@v6
43+
44+
- name: Install dependencies
45+
run: uv sync --all-extras --dev
46+
47+
- name: Run unit tests
48+
run: uv run pytest tests/src/unit/ -n auto --tb=short -v
49+
1550
# Comprehensive E2E validation for all PRs
1651
e2e-validation:
1752
name: E2E Validation (${{ matrix.os }})
@@ -34,6 +69,8 @@ jobs:
3469

3570
- name: Set up Docker Buildx
3671
uses: docker/setup-buildx-action@v3
72+
with:
73+
cache-binary: true
3774

3875
- name: Install uv
3976
uses: astral-sh/setup-uv@v7
@@ -46,6 +83,37 @@ jobs:
4683
- name: Install dependencies
4784
run: uv sync --all-extras --dev
4885

86+
- name: Cache HA Docker image
87+
id: cache-ha-image
88+
uses: actions/cache@v4
89+
with:
90+
path: /tmp/ha-image.tar
91+
key: ha-image-${{ env.HA_IMAGE_GHCR }}-${{ runner.arch }}
92+
93+
- name: Load cached HA image
94+
if: steps.cache-ha-image.outputs.cache-hit == 'true'
95+
run: docker load -i /tmp/ha-image.tar
96+
97+
- name: Pull HA image (GHCR → Docker Hub fallback)
98+
if: steps.cache-ha-image.outputs.cache-hit != 'true'
99+
run: |
100+
HA_VERSION="${HA_IMAGE_GHCR##*:}"
101+
HA_IMAGE_DOCKERHUB="homeassistant/home-assistant:${HA_VERSION}"
102+
for registry in "$HA_IMAGE_GHCR" "$HA_IMAGE_DOCKERHUB"; do
103+
echo "Trying $registry..."
104+
if docker pull "$registry"; then
105+
if [ "$registry" != "$HA_IMAGE_GHCR" ]; then
106+
docker tag "$registry" "$HA_IMAGE_GHCR"
107+
fi
108+
docker save "$HA_IMAGE_GHCR" -o /tmp/ha-image.tar
109+
echo "Pulled and cached from $registry"
110+
exit 0
111+
fi
112+
echo "Failed to pull from $registry, trying next..."
113+
sleep 15
114+
done
115+
echo "All registries failed" && exit 1
116+
49117
- name: Run full E2E test suite
50118
run: |
51119
echo "🚀 Running full E2E test suite with ${{ matrix.pytest_workers }} workers..."
@@ -68,6 +136,8 @@ jobs:
68136

69137
- name: Set up Docker Buildx
70138
uses: docker/setup-buildx-action@v3
139+
with:
140+
cache-binary: true
71141

72142
- name: Install uv
73143
uses: astral-sh/setup-uv@v7

.pre-commit-config.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
repos:
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
rev: v0.15.0
4+
hooks:
5+
- id: ruff
6+
args: [--fix]
7+
- repo: local
8+
hooks:
9+
- id: unit-tests
10+
name: unit tests
11+
entry: uv run pytest tests/src/unit/ -n auto -m "not slow" --tb=short -q
12+
language: system
13+
pass_filenames: false
14+
files: ^(src/|tests/|pyproject\.toml)

AGENTS.md

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,20 @@ Balance improvement against regression risk. Consider:
437437
| **Tests exist, quality is low** | Improve test quality if it's straightforward (better assertions, clearer names, remove duplication) |
438438
| **Code quality is really low** | Open an issue describing the technical debt instead of fixing it inline |
439439

440+
### Test Coverage Requirements
441+
442+
**When tests ARE required:**
443+
- New MCP tools in `src/ha_mcp/tools/` without any E2E tests
444+
- Tools that previously had NO tests — add E2E tests even if not part of current PR
445+
- Core functionality changes in `client/`, `server.py`, or `errors.py` without coverage
446+
- Bug fixes without regression tests
447+
448+
**When tests may NOT be required:**
449+
- Refactoring with existing comprehensive test coverage
450+
- Documentation-only changes (`*.md` files)
451+
- Minor parameter additions to well-tested tools
452+
- Internal utilities already covered by E2E tests
453+
440454
**Examples:**
441455

442456
```python
@@ -598,11 +612,11 @@ def register_<domain>_tools(mcp, client, **kwargs):
598612
```
599613

600614
### Safety Annotations
601-
| Annotation | Use For |
602-
|------------|---------|
603-
| `readOnlyHint: True` | No side effects |
604-
| `idempotentHint: True` | Safe to retry |
605-
| `destructiveHint: True` | Deletes data |
615+
| Annotation | Default | Use For |
616+
|------------|---------|--------|
617+
| `readOnlyHint: True` | `False` | Tool does not modify its environment |
618+
| `destructiveHint: True` | `True` | Tool may perform destructive updates (only meaningful when `readOnlyHint` is false). Set to `False` for non-destructive writes (e.g., creating a record) |
619+
| `idempotentHint: True` | `False` | Repeated calls with same args have no additional effect (only meaningful when `readOnlyHint` is false) |
606620

607621
### Error Handling
608622

@@ -674,9 +688,7 @@ A change is **BREAKING** only if it removes functionality that users depend on w
674688

675689
**Principle**: MCP tools should wait for operations to complete before returning, not just acknowledge API success.
676690

677-
**Current State (#365)**: Tests use polling helpers to wait for completion after tool calls.
678-
679-
**Future State (#381)**: Tools will have optional `wait` parameter (default `True`) to handle waiting internally:
691+
**Implementation (#381)**: Tools have an optional `wait` parameter (default `True`) that controls whether they poll for completion:
680692

681693
```python
682694
# Config operations wait by default
@@ -689,12 +701,15 @@ await _verify_all_created(entity_ids) # Batch verification
689701
```
690702

691703
**Tool Categories**:
692-
- **Config ops** (automations, helpers, scripts): MUST wait by default
693-
- **Service calls** (lights, switches): SHOULD wait for state change
694-
- **Async ops** (automation triggers, external integrations): Return immediately, users poll
695-
- **Query ops** (get_state, search): Return immediately
696-
697-
See issue #381 for implementation plan.
704+
- **Config ops** (automations, helpers, scripts): Wait by default (poll until entity queryable/removed)
705+
- **Service calls** (lights, switches): Wait for state change on state-changing services (turn_on, turn_off, toggle, etc.)
706+
- **Async ops** (automation triggers, external integrations): Return immediately (not state-changing)
707+
- **Query ops** (get_state, search): Return immediately (no `wait` parameter)
708+
709+
**Shared utilities** in `src/ha_mcp/tools/util_helpers.py`:
710+
- `wait_for_entity_registered(client, entity_id)` — polls until entity accessible via state API
711+
- `wait_for_entity_removed(client, entity_id)` — polls until entity no longer accessible
712+
- `wait_for_state_change(client, entity_id, expected_state)` — polls until state changes
698713

699714
## Context Engineering & Progressive Disclosure
700715

CONTRIBUTING.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ Thank you for your interest in contributing!
66

77
1. **Fork and clone** the repository
88
2. **Install**: `uv sync --group dev`
9-
3. **Test**: `uv run pytest tests/src/e2e/ -v` (requires Docker)
10-
4. **Make changes** and commit
11-
5. **Open Pull Request**
9+
3. **Install hooks**: `uv run pre-commit install`
10+
4. **Test**: `uv run pytest tests/src/e2e/ -v` (requires Docker)
11+
5. **Make changes** and commit
12+
6. **Open Pull Request**
1213

1314
## 🧪 Testing
1415

@@ -20,6 +21,7 @@ See **[tests/README.md](tests/README.md)**.
2021
```bash
2122
cp .env.example .env # Edit with your HA details
2223
uv sync --group dev
24+
uv run pre-commit install # Install pre-commit hooks
2325
```
2426

2527
**Code quality:**
@@ -29,6 +31,8 @@ uv run ruff check --fix src/ tests/ # Lint
2931
uv run mypy src/ # Type check
3032
```
3133

34+
On every commit, a `pre-commit` hook runs `ruff check --fix` to auto-fix and catch lint violations. The **Ruff Lint** CI job also enforces this on pull requests.
35+
3236
## 📋 Guidelines
3337

3438
- **Code**: Follow existing patterns, add type hints, test new features

README.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,14 +192,22 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
192192

193193
## 👥 Contributors
194194

195-
- **[@julienld](https://github.com/julienld)** — Project maintainer & core contributor.
196-
- **[@kingbear2](https://github.com/kingbear2)** — Windows UV setup guide.
197-
- **[@sergeykad](https://github.com/sergeykad)** — Dashboard card-level CRUD operations, better changelogs and removed the dependency to textdistance/numpy.
198-
- **[@konradwalsh](https://github.com/konradwalsh)** — Financial support via [GitHub Sponsors](https://github.com/sponsors/julienld). Thank you! ☕
199-
- **[@cj-elevate](https://github.com/cj-elevate)** — Integration & entity management tools (enable/disable/delete).
200-
- **[@kingpanther13](https://github.com/kingpanther13)** — Dev channel documentation, bulk control validation, OAuth 2.1 docs, tool consolidation, error handling improvements, and native solutions guidance.
195+
### Maintainers
196+
197+
- **[@julienld](https://github.com/julienld)** — Project creator & core maintainer.
198+
- **[@sergeykad](https://github.com/sergeykad)** — Dashboard CRUD, search pagination, `__main__` security refactor, pre-commit hooks & CI lint, addon Docker fixes, `.gitattributes` enforcement, human-readable log timestamps, and removed the textdistance/numpy dependency.
199+
- **[@kingpanther13](https://github.com/kingpanther13)** — Dev channel documentation, bulk control validation, OAuth 2.1 docs, tool consolidation, error handling improvements, native solutions guidance, default dashboard editing fix, and search response optimization.
200+
201+
### Contributors
202+
203+
- **[@airlabno](https://github.com/airlabno)** — Support for `data` field in schedule time blocks.
204+
- **[@ryphez](https://github.com/ryphez)** — Codex Desktop UI MCP quick setup guide.
201205
- **[@Danm72](https://github.com/Danm72)** — Entity registry tools (`ha_set_entity`, `ha_get_entity`) for managing entity properties.
202206
- **[@Raygooo](https://github.com/Raygooo)** — SOCKS proxy support.
207+
- **[@cj-elevate](https://github.com/cj-elevate)** — Integration & entity management tools (enable/disable/delete).
208+
- **[@maxperron](https://github.com/maxperron)** — Beta testing.
209+
- **[@kingbear2](https://github.com/kingbear2)** — Windows UV setup guide.
210+
- **[@konradwalsh](https://github.com/konradwalsh)** — Financial support via [GitHub Sponsors](https://github.com/sponsors/julienld). Thank you! ☕
203211

204212
---
205213

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ dev = [
172172
"pytest-cov>=5.0.0",
173173
"pytest-xdist>=3.8.0",
174174
"requests>=2.25.0",
175+
"pre-commit>=4.0.0",
175176
"ruff>=0.12.12",
176177
"testcontainers>=4.13.0",
177178
]

renovate.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
{
1717
"customType": "regex",
1818
"managerFilePatterns": [
19-
"/tests/.*\\.py$/"
19+
"/tests/.*\\.py$/",
20+
"/.github/workflows/.*\\.yml$/"
2021
],
2122
"matchStrings": [
2223
"# renovate: datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)\\s+.*?\\S+:(?<currentValue>[\\d.]+)"

site/src/content/clients/codex.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,30 @@ codex mcp add homeassistant --env HOMEASSISTANT_URL={{HOMEASSISTANT_URL}} --env
6161
codex mcp add home-assistant --url {{MCP_SERVER_URL}}
6262
```
6363

64+
## Quick Setup with Codex Desktop UI
65+
66+
Codex Desktop supports MCP servers via **STDIO** or **Streamable HTTP**.
67+
For local machine setup with `ha-mcp`, use **STDIO** so Codex launches `ha-mcp` as a subprocess.
68+
69+
### Local Machine (STDIO)
70+
71+
1. Open Codex Desktop
72+
2. Go to **Settings****MCP**
73+
3. Click **Add Server**
74+
4. Set:
75+
- **Type:** `STDIO`
76+
- **Name:** `home-assistant` (or `ha-mcp`)
77+
- **Command:** `uvx`
78+
- **Args:** `ha-mcp@latest`
79+
5. Add environment variables:
80+
- `HOMEASSISTANT_URL={{HOMEASSISTANT_URL}}`
81+
- `HOMEASSISTANT_TOKEN={{HOMEASSISTANT_TOKEN}}`
82+
83+
### When to Use Streamable HTTP
84+
85+
Use **Streamable HTTP** only when `ha-mcp` is running as a network HTTP server (for example behind a reverse proxy or tunnel).
86+
For local machine setup, use **STDIO**.
87+
6488
## Management Commands
6589

6690
```bash

0 commit comments

Comments
 (0)