zigporter 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zigporter-0.1.0/PKG-INFO +158 -0
- zigporter-0.1.0/README.md +137 -0
- zigporter-0.1.0/pyproject.toml +54 -0
- zigporter-0.1.0/src/zigporter/__init__.py +0 -0
- zigporter-0.1.0/src/zigporter/commands/__init__.py +0 -0
- zigporter-0.1.0/src/zigporter/commands/check.py +230 -0
- zigporter-0.1.0/src/zigporter/commands/export.py +221 -0
- zigporter-0.1.0/src/zigporter/commands/inspect.py +473 -0
- zigporter-0.1.0/src/zigporter/commands/list_z2m.py +53 -0
- zigporter-0.1.0/src/zigporter/commands/migrate.py +808 -0
- zigporter-0.1.0/src/zigporter/commands/setup.py +194 -0
- zigporter-0.1.0/src/zigporter/config.py +79 -0
- zigporter-0.1.0/src/zigporter/ha_client.py +240 -0
- zigporter-0.1.0/src/zigporter/main.py +307 -0
- zigporter-0.1.0/src/zigporter/migration_state.py +73 -0
- zigporter-0.1.0/src/zigporter/models.py +58 -0
- zigporter-0.1.0/src/zigporter/utils.py +6 -0
- zigporter-0.1.0/src/zigporter/z2m_client.py +222 -0
zigporter-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: zigporter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI tool to migrate Zigbee devices from ZHA to Zigbee2MQTT in Home Assistant
|
|
5
|
+
Author: Even Nordstad
|
|
6
|
+
Author-email: Even Nordstad <even.nordstad@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Dist: typer>=0.12
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Requires-Dist: websockets>=12
|
|
11
|
+
Requires-Dist: pydantic>=2.7
|
|
12
|
+
Requires-Dist: python-dotenv>=1.0
|
|
13
|
+
Requires-Dist: rich>=13
|
|
14
|
+
Requires-Dist: questionary>=2.1.1
|
|
15
|
+
Requires-Dist: platformdirs>=4.0
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Project-URL: Homepage, https://github.com/nordstad/zigporter
|
|
18
|
+
Project-URL: Repository, https://github.com/nordstad/zigporter
|
|
19
|
+
Project-URL: Issues, https://github.com/nordstad/zigporter/issues
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
[](https://github.com/nordstad/zigporter/actions/workflows/ci.yml)
|
|
23
|
+
[](https://codecov.io/gh/nordstad/zigporter)
|
|
24
|
+
[](https://nordstad.github.io/zigporter)
|
|
25
|
+
[](https://pypi.org/project/zigporter/)
|
|
26
|
+
[](https://pepy.tech/project/zigporter)
|
|
27
|
+
[](https://www.python.org/downloads/)
|
|
28
|
+
[](https://opensource.org/licenses/MIT)
|
|
29
|
+
|
|
30
|
+
# zigporter
|
|
31
|
+
|
|
32
|
+
*Because migrating 30 Zigbee devices in Home Assistant by hand is a special kind of misery.*
|
|
33
|
+
|
|
34
|
+
CLI tool that automates the ZHA → Zigbee2MQTT migration in Home Assistant — one device at a
|
|
35
|
+
time, with checkpoints so you can stop and pick up where you left off.
|
|
36
|
+
|
|
37
|
+
> **Early Development Notice**
|
|
38
|
+
> This tool is in early development and has only been tested with one specific setup:
|
|
39
|
+
> - Home Assistant OS 2026.2.3
|
|
40
|
+
> - Supervisor 2026.02.2
|
|
41
|
+
> - Zigbee2MQTT 2.8.0-1
|
|
42
|
+
>
|
|
43
|
+
> I have not had the possibility to test with different HA or Z2M versions and setups.
|
|
44
|
+
> Feedback is very welcome — please open an [issue](https://github.com/nordstad/zigporter/issues) or submit a [PR](https://github.com/nordstad/zigporter/pulls) if you test with a different configuration.
|
|
45
|
+
|
|
46
|
+
> **Early Development Notice**
|
|
47
|
+
> This tool is in early development and has only been tested with one specific setup:
|
|
48
|
+
> - Home Assistant OS 2026.2.3
|
|
49
|
+
> - Supervisor 2026.02.2
|
|
50
|
+
> - Zigbee2MQTT 2.8.0-1
|
|
51
|
+
>
|
|
52
|
+
> I have not had the possibility to test with different HA or Z2M versions and setups.
|
|
53
|
+
> Feedback is very welcome — please open an [issue](https://github.com/nordstad/zigporter/issues) or submit a [PR](https://github.com/nordstad/zigporter/pulls) if you test with a different configuration.
|
|
54
|
+
|
|
55
|
+
## Requirements
|
|
56
|
+
|
|
57
|
+
- Python 3.12+
|
|
58
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
59
|
+
- Home Assistant with ZHA and Zigbee2MQTT add-on
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uv tool install zigporter
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
**Option 1 — Setup wizard (recommended)**
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
zigporter setup
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Prompts for all values and saves to `~/.config/zigporter/.env`.
|
|
76
|
+
|
|
77
|
+
**Option 2 — Manual config file**
|
|
78
|
+
|
|
79
|
+
Create `~/.config/zigporter/.env` (see `.env.example` for the template):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
mkdir -p ~/.config/zigporter
|
|
83
|
+
cp .env.example ~/.config/zigporter/.env
|
|
84
|
+
# edit the file with your values
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Option 3 — Environment variables**
|
|
88
|
+
|
|
89
|
+
Export directly in your shell or add to `~/.zshenv` / `~/.bashrc`:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
export HA_URL=https://your-ha-instance.local
|
|
93
|
+
export HA_TOKEN=your_token
|
|
94
|
+
export Z2M_URL=https://your-ha-instance.local/abc123_zigbee2mqtt
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
| Variable | Required | Description |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `HA_URL` | Yes | Home Assistant URL |
|
|
100
|
+
| `HA_TOKEN` | Yes | [Long-Lived Access Token](https://www.home-assistant.io/docs/authentication/#your-account-profile) |
|
|
101
|
+
| `HA_VERIFY_SSL` | No | `true` / `false` (default: `true`; use `false` for self-signed certs) |
|
|
102
|
+
| `Z2M_URL` | Yes | Zigbee2MQTT ingress URL |
|
|
103
|
+
| `Z2M_MQTT_TOPIC` | No | Z2M base topic (default: `zigbee2mqtt`) |
|
|
104
|
+
|
|
105
|
+
## Usage
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Verify your setup before migrating (recommended first step)
|
|
109
|
+
zigporter check
|
|
110
|
+
|
|
111
|
+
# Run the migration wizard (runs checks automatically on first run)
|
|
112
|
+
zigporter migrate
|
|
113
|
+
|
|
114
|
+
# Check migration progress without entering the wizard
|
|
115
|
+
zigporter migrate --status
|
|
116
|
+
|
|
117
|
+
# (Optional) manually export your ZHA device inventory
|
|
118
|
+
zigporter export
|
|
119
|
+
|
|
120
|
+
# (Optional) inspect what's already in Z2M
|
|
121
|
+
zigporter list-z2m
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`zigporter migrate` handles everything automatically on first run:
|
|
125
|
+
1. Runs pre-flight checks (HA reachable, ZHA active, Z2M running)
|
|
126
|
+
2. Prompts you to back up Home Assistant and your ZHA network
|
|
127
|
+
3. Fetches a ZHA export if one is not found, or offers to refresh an existing one
|
|
128
|
+
4. Opens the interactive migration wizard
|
|
129
|
+
|
|
130
|
+
All files are stored in `~/.config/zigporter/` so the tool works from any directory.
|
|
131
|
+
Use `--skip-checks` on subsequent runs to skip the pre-flight checks.
|
|
132
|
+
|
|
133
|
+
## How it works
|
|
134
|
+
|
|
135
|
+
The wizard migrates one device at a time through five steps:
|
|
136
|
+
|
|
137
|
+
1. **Remove from ZHA** — confirms deletion in the HA registry
|
|
138
|
+
2. **Reset device** — prompts you to factory-reset the physical device
|
|
139
|
+
3. **Pair with Z2M** — opens a 120 s permit-join window and polls by IEEE address
|
|
140
|
+
4. **Rename** — applies the original ZHA name and area in Z2M and HA
|
|
141
|
+
5. **Validate** — polls HA entity states until all are online
|
|
142
|
+
|
|
143
|
+
State is written to `zha-migration-state.json` after every step. `Ctrl-C` marks the device `FAILED` — rerun to retry.
|
|
144
|
+
|
|
145
|
+
See the [wiki](https://github.com/nordstad/zigporter/wiki) for detailed diagrams and architecture docs.
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
uv sync --dev
|
|
151
|
+
uv run pytest
|
|
152
|
+
uv run ruff check .
|
|
153
|
+
uv run ruff format .
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
[](https://github.com/nordstad/zigporter/actions/workflows/ci.yml)
|
|
2
|
+
[](https://codecov.io/gh/nordstad/zigporter)
|
|
3
|
+
[](https://nordstad.github.io/zigporter)
|
|
4
|
+
[](https://pypi.org/project/zigporter/)
|
|
5
|
+
[](https://pepy.tech/project/zigporter)
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
# zigporter
|
|
10
|
+
|
|
11
|
+
*Because migrating 30 Zigbee devices in Home Assistant by hand is a special kind of misery.*
|
|
12
|
+
|
|
13
|
+
CLI tool that automates the ZHA → Zigbee2MQTT migration in Home Assistant — one device at a
|
|
14
|
+
time, with checkpoints so you can stop and pick up where you left off.
|
|
15
|
+
|
|
16
|
+
> **Early Development Notice**
|
|
17
|
+
> This tool is in early development and has only been tested with one specific setup:
|
|
18
|
+
> - Home Assistant OS 2026.2.3
|
|
19
|
+
> - Supervisor 2026.02.2
|
|
20
|
+
> - Zigbee2MQTT 2.8.0-1
|
|
21
|
+
>
|
|
22
|
+
> I have not had the possibility to test with different HA or Z2M versions and setups.
|
|
23
|
+
> Feedback is very welcome — please open an [issue](https://github.com/nordstad/zigporter/issues) or submit a [PR](https://github.com/nordstad/zigporter/pulls) if you test with a different configuration.
|
|
24
|
+
|
|
25
|
+
> **Early Development Notice**
|
|
26
|
+
> This tool is in early development and has only been tested with one specific setup:
|
|
27
|
+
> - Home Assistant OS 2026.2.3
|
|
28
|
+
> - Supervisor 2026.02.2
|
|
29
|
+
> - Zigbee2MQTT 2.8.0-1
|
|
30
|
+
>
|
|
31
|
+
> I have not had the possibility to test with different HA or Z2M versions and setups.
|
|
32
|
+
> Feedback is very welcome — please open an [issue](https://github.com/nordstad/zigporter/issues) or submit a [PR](https://github.com/nordstad/zigporter/pulls) if you test with a different configuration.
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
- Python 3.12+
|
|
37
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
38
|
+
- Home Assistant with ZHA and Zigbee2MQTT add-on
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv tool install zigporter
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
**Option 1 — Setup wizard (recommended)**
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
zigporter setup
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Prompts for all values and saves to `~/.config/zigporter/.env`.
|
|
55
|
+
|
|
56
|
+
**Option 2 — Manual config file**
|
|
57
|
+
|
|
58
|
+
Create `~/.config/zigporter/.env` (see `.env.example` for the template):
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
mkdir -p ~/.config/zigporter
|
|
62
|
+
cp .env.example ~/.config/zigporter/.env
|
|
63
|
+
# edit the file with your values
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Option 3 — Environment variables**
|
|
67
|
+
|
|
68
|
+
Export directly in your shell or add to `~/.zshenv` / `~/.bashrc`:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
export HA_URL=https://your-ha-instance.local
|
|
72
|
+
export HA_TOKEN=your_token
|
|
73
|
+
export Z2M_URL=https://your-ha-instance.local/abc123_zigbee2mqtt
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Variable | Required | Description |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `HA_URL` | Yes | Home Assistant URL |
|
|
79
|
+
| `HA_TOKEN` | Yes | [Long-Lived Access Token](https://www.home-assistant.io/docs/authentication/#your-account-profile) |
|
|
80
|
+
| `HA_VERIFY_SSL` | No | `true` / `false` (default: `true`; use `false` for self-signed certs) |
|
|
81
|
+
| `Z2M_URL` | Yes | Zigbee2MQTT ingress URL |
|
|
82
|
+
| `Z2M_MQTT_TOPIC` | No | Z2M base topic (default: `zigbee2mqtt`) |
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Verify your setup before migrating (recommended first step)
|
|
88
|
+
zigporter check
|
|
89
|
+
|
|
90
|
+
# Run the migration wizard (runs checks automatically on first run)
|
|
91
|
+
zigporter migrate
|
|
92
|
+
|
|
93
|
+
# Check migration progress without entering the wizard
|
|
94
|
+
zigporter migrate --status
|
|
95
|
+
|
|
96
|
+
# (Optional) manually export your ZHA device inventory
|
|
97
|
+
zigporter export
|
|
98
|
+
|
|
99
|
+
# (Optional) inspect what's already in Z2M
|
|
100
|
+
zigporter list-z2m
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`zigporter migrate` handles everything automatically on first run:
|
|
104
|
+
1. Runs pre-flight checks (HA reachable, ZHA active, Z2M running)
|
|
105
|
+
2. Prompts you to back up Home Assistant and your ZHA network
|
|
106
|
+
3. Fetches a ZHA export if one is not found, or offers to refresh an existing one
|
|
107
|
+
4. Opens the interactive migration wizard
|
|
108
|
+
|
|
109
|
+
All files are stored in `~/.config/zigporter/` so the tool works from any directory.
|
|
110
|
+
Use `--skip-checks` on subsequent runs to skip the pre-flight checks.
|
|
111
|
+
|
|
112
|
+
## How it works
|
|
113
|
+
|
|
114
|
+
The wizard migrates one device at a time through five steps:
|
|
115
|
+
|
|
116
|
+
1. **Remove from ZHA** — confirms deletion in the HA registry
|
|
117
|
+
2. **Reset device** — prompts you to factory-reset the physical device
|
|
118
|
+
3. **Pair with Z2M** — opens a 120 s permit-join window and polls by IEEE address
|
|
119
|
+
4. **Rename** — applies the original ZHA name and area in Z2M and HA
|
|
120
|
+
5. **Validate** — polls HA entity states until all are online
|
|
121
|
+
|
|
122
|
+
State is written to `zha-migration-state.json` after every step. `Ctrl-C` marks the device `FAILED` — rerun to retry.
|
|
123
|
+
|
|
124
|
+
See the [wiki](https://github.com/nordstad/zigporter/wiki) for detailed diagrams and architecture docs.
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
uv sync --dev
|
|
130
|
+
uv run pytest
|
|
131
|
+
uv run ruff check .
|
|
132
|
+
uv run ruff format .
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "zigporter"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "CLI tool to migrate Zigbee devices from ZHA to Zigbee2MQTT in Home Assistant"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Even Nordstad", email = "even.nordstad@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
license = { text = "MIT" }
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"typer>=0.12",
|
|
13
|
+
"httpx>=0.27",
|
|
14
|
+
"websockets>=12",
|
|
15
|
+
"pydantic>=2.7",
|
|
16
|
+
"python-dotenv>=1.0",
|
|
17
|
+
"rich>=13",
|
|
18
|
+
"questionary>=2.1.1",
|
|
19
|
+
"platformdirs>=4.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/nordstad/zigporter"
|
|
24
|
+
Repository = "https://github.com/nordstad/zigporter"
|
|
25
|
+
Issues = "https://github.com/nordstad/zigporter/issues"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
zigporter = "zigporter.main:app"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["uv_build>=0.9.26,<0.11.0"]
|
|
32
|
+
build-backend = "uv_build"
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
docs = [
|
|
36
|
+
"zensical",
|
|
37
|
+
]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=8",
|
|
40
|
+
"pytest-asyncio>=0.23",
|
|
41
|
+
"pytest-cov>=5",
|
|
42
|
+
"respx>=0.21",
|
|
43
|
+
"pytest-mock>=3.14",
|
|
44
|
+
"ruff>=0.4",
|
|
45
|
+
"twine>=5",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
asyncio_mode = "auto"
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 100
|
|
53
|
+
target-version = "py313"
|
|
54
|
+
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import questionary
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
from zigporter.ha_client import HAClient
|
|
8
|
+
from zigporter.models import CheckResult, CheckStatus
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
_STYLE = questionary.Style(
|
|
13
|
+
[
|
|
14
|
+
("qmark", "fg:ansicyan bold"),
|
|
15
|
+
("question", "bold"),
|
|
16
|
+
("answer", "fg:ansicyan bold"),
|
|
17
|
+
("pointer", "fg:ansicyan bold"),
|
|
18
|
+
("highlighted", "fg:ansicyan bold"),
|
|
19
|
+
("selected", "fg:ansicyan"),
|
|
20
|
+
("separator", "fg:ansibrightblack"),
|
|
21
|
+
("instruction", "fg:ansibrightblack"),
|
|
22
|
+
("text", ""),
|
|
23
|
+
("disabled", "fg:ansibrightblack italic"),
|
|
24
|
+
]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_STATUS_ICON = {
|
|
28
|
+
CheckStatus.OK: "[green]✓[/green]",
|
|
29
|
+
CheckStatus.FAILED: "[red]✗[/red]",
|
|
30
|
+
CheckStatus.WARNING: "[yellow]![/yellow]",
|
|
31
|
+
CheckStatus.SKIPPED: "[dim]–[/dim]",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Individual checks
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def _check_config(ha_url: str, token: str, z2m_url: str) -> CheckResult:
|
|
41
|
+
missing = [
|
|
42
|
+
name
|
|
43
|
+
for name, val in [("HA_URL", ha_url), ("HA_TOKEN", token), ("Z2M_URL", z2m_url)]
|
|
44
|
+
if not val
|
|
45
|
+
]
|
|
46
|
+
if missing:
|
|
47
|
+
return CheckResult(
|
|
48
|
+
name="Configuration",
|
|
49
|
+
status=CheckStatus.FAILED,
|
|
50
|
+
message=f"Missing: {', '.join(missing)} — add to .env or set as environment variables",
|
|
51
|
+
)
|
|
52
|
+
return CheckResult(
|
|
53
|
+
name="Configuration", status=CheckStatus.OK, message="HA_URL, HA_TOKEN, Z2M_URL are set"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def _check_ha_reachable(ha_url: str, token: str, verify_ssl: bool) -> CheckResult:
|
|
58
|
+
if not ha_url:
|
|
59
|
+
return CheckResult(
|
|
60
|
+
name="HA reachable",
|
|
61
|
+
status=CheckStatus.SKIPPED,
|
|
62
|
+
message="Skipped (no HA_URL configured)",
|
|
63
|
+
)
|
|
64
|
+
try:
|
|
65
|
+
async with httpx.AsyncClient(
|
|
66
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
67
|
+
verify=verify_ssl,
|
|
68
|
+
timeout=10,
|
|
69
|
+
) as client:
|
|
70
|
+
resp = await client.get(f"{ha_url}/api/")
|
|
71
|
+
resp.raise_for_status()
|
|
72
|
+
return CheckResult(name="HA reachable", status=CheckStatus.OK, message=ha_url)
|
|
73
|
+
except Exception as exc:
|
|
74
|
+
return CheckResult(
|
|
75
|
+
name="HA reachable",
|
|
76
|
+
status=CheckStatus.FAILED,
|
|
77
|
+
message=f"Cannot reach {ha_url} — {exc}",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def _check_zha_active(ha_url: str, token: str, verify_ssl: bool) -> CheckResult:
|
|
82
|
+
if not ha_url:
|
|
83
|
+
return CheckResult(
|
|
84
|
+
name="ZHA active",
|
|
85
|
+
status=CheckStatus.SKIPPED,
|
|
86
|
+
message="Skipped (no HA_URL configured)",
|
|
87
|
+
)
|
|
88
|
+
try:
|
|
89
|
+
client = HAClient(ha_url, token, verify_ssl)
|
|
90
|
+
devices = await client.get_zha_devices()
|
|
91
|
+
count = len(devices)
|
|
92
|
+
if count == 0:
|
|
93
|
+
return CheckResult(
|
|
94
|
+
name="ZHA active",
|
|
95
|
+
status=CheckStatus.WARNING,
|
|
96
|
+
message="ZHA is reachable but no devices found — is ZHA configured?",
|
|
97
|
+
blocking=False,
|
|
98
|
+
)
|
|
99
|
+
return CheckResult(
|
|
100
|
+
name="ZHA active",
|
|
101
|
+
status=CheckStatus.OK,
|
|
102
|
+
message=f"{count} device(s) found",
|
|
103
|
+
)
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
return CheckResult(
|
|
106
|
+
name="ZHA active",
|
|
107
|
+
status=CheckStatus.FAILED,
|
|
108
|
+
message=f"Could not query ZHA — {exc}",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def _check_z2m_running(
|
|
113
|
+
ha_url: str, token: str, z2m_url: str, verify_ssl: bool
|
|
114
|
+
) -> CheckResult:
|
|
115
|
+
if not z2m_url:
|
|
116
|
+
return CheckResult(
|
|
117
|
+
name="Z2M running",
|
|
118
|
+
status=CheckStatus.SKIPPED,
|
|
119
|
+
message="Skipped (no Z2M_URL configured)",
|
|
120
|
+
)
|
|
121
|
+
try:
|
|
122
|
+
async with httpx.AsyncClient(
|
|
123
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
124
|
+
verify=verify_ssl,
|
|
125
|
+
timeout=10,
|
|
126
|
+
) as client:
|
|
127
|
+
resp = await client.get(f"{z2m_url}/api/devices")
|
|
128
|
+
# Any HTTP response (even 401) means the server is reachable
|
|
129
|
+
if resp.status_code < 500:
|
|
130
|
+
try:
|
|
131
|
+
devices = resp.json()
|
|
132
|
+
count = len(devices) if isinstance(devices, list) else "?"
|
|
133
|
+
return CheckResult(
|
|
134
|
+
name="Z2M running",
|
|
135
|
+
status=CheckStatus.OK,
|
|
136
|
+
message=f"{count} device(s) paired",
|
|
137
|
+
)
|
|
138
|
+
except Exception:
|
|
139
|
+
return CheckResult(
|
|
140
|
+
name="Z2M running",
|
|
141
|
+
status=CheckStatus.OK,
|
|
142
|
+
message="Z2M is responding",
|
|
143
|
+
)
|
|
144
|
+
resp.raise_for_status()
|
|
145
|
+
return CheckResult(
|
|
146
|
+
name="Z2M running", status=CheckStatus.OK, message="Z2M is responding"
|
|
147
|
+
)
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
return CheckResult(
|
|
150
|
+
name="Z2M running",
|
|
151
|
+
status=CheckStatus.FAILED,
|
|
152
|
+
message=f"Cannot reach Zigbee2MQTT at {z2m_url} — {exc}",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Orchestrator
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def _run_checks(
|
|
162
|
+
ha_url: str,
|
|
163
|
+
token: str,
|
|
164
|
+
verify_ssl: bool,
|
|
165
|
+
z2m_url: str,
|
|
166
|
+
) -> list[CheckResult]:
|
|
167
|
+
results: list[CheckResult] = []
|
|
168
|
+
|
|
169
|
+
config_result = await _check_config(ha_url, token, z2m_url)
|
|
170
|
+
results.append(config_result)
|
|
171
|
+
|
|
172
|
+
# Only run network checks if config is valid
|
|
173
|
+
if config_result.status == CheckStatus.OK:
|
|
174
|
+
ha_result = await _check_ha_reachable(ha_url, token, verify_ssl)
|
|
175
|
+
results.append(ha_result)
|
|
176
|
+
|
|
177
|
+
# ZHA depends on HA being reachable
|
|
178
|
+
if ha_result.status == CheckStatus.OK:
|
|
179
|
+
results.append(await _check_zha_active(ha_url, token, verify_ssl))
|
|
180
|
+
else:
|
|
181
|
+
results.append(
|
|
182
|
+
CheckResult(
|
|
183
|
+
name="ZHA active",
|
|
184
|
+
status=CheckStatus.SKIPPED,
|
|
185
|
+
message="Skipped (HA not reachable)",
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
results.append(await _check_z2m_running(ha_url, token, z2m_url, verify_ssl))
|
|
190
|
+
else:
|
|
191
|
+
for name in ("HA reachable", "ZHA active", "Z2M running"):
|
|
192
|
+
results.append(
|
|
193
|
+
CheckResult(
|
|
194
|
+
name=name, status=CheckStatus.SKIPPED, message="Skipped (invalid config)"
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
return results
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _print_results(results: list[CheckResult]) -> None:
|
|
202
|
+
console.print()
|
|
203
|
+
for r in results:
|
|
204
|
+
icon = _STATUS_ICON[r.status]
|
|
205
|
+
label = f"[bold]{r.name:<20}[/bold]"
|
|
206
|
+
console.print(f" {icon} {label} {r.message}")
|
|
207
|
+
console.print()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def check_command(
|
|
211
|
+
ha_url: str,
|
|
212
|
+
token: str,
|
|
213
|
+
verify_ssl: bool,
|
|
214
|
+
z2m_url: str,
|
|
215
|
+
) -> bool:
|
|
216
|
+
"""Run all preflight checks. Returns True if the user should proceed, False to abort."""
|
|
217
|
+
console.rule("[bold cyan]Pre-flight checks[/bold cyan]")
|
|
218
|
+
|
|
219
|
+
results = asyncio.run(_run_checks(ha_url, token, verify_ssl, z2m_url))
|
|
220
|
+
_print_results(results)
|
|
221
|
+
|
|
222
|
+
blocking_failures = [r for r in results if r.status == CheckStatus.FAILED and r.blocking]
|
|
223
|
+
if blocking_failures:
|
|
224
|
+
console.print("[yellow]One or more checks failed.[/yellow]")
|
|
225
|
+
proceed = questionary.confirm("Proceed anyway?", default=False, style=_STYLE).ask()
|
|
226
|
+
if not proceed:
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
console.rule()
|
|
230
|
+
return True
|