diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json
new file mode 100644
index 0000000..2ab500e
--- /dev/null
+++ b/.devcontainer/devcontainer-lock.json
@@ -0,0 +1,24 @@
+{
+ "features": {
+ "ghcr.io/devcontainers-extra/features/poetry:2": {
+ "version": "2.0.18",
+ "resolved": "ghcr.io/devcontainers-extra/features/poetry@sha256:7b5f2c66d25d8b3cc5d601af6b81b720c77dd315f24d5e6f84b342cccc3c9228",
+ "integrity": "sha256:7b5f2c66d25d8b3cc5d601af6b81b720c77dd315f24d5e6f84b342cccc3c9228"
+ },
+ "ghcr.io/devcontainers/features/github-cli:1": {
+ "version": "1.1.0",
+ "resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671",
+ "integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671"
+ },
+ "ghcr.io/devcontainers/features/node:1": {
+ "version": "1.7.1",
+ "resolved": "ghcr.io/devcontainers/features/node@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6",
+ "integrity": "sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6"
+ },
+ "ghcr.io/devcontainers/features/rust:1": {
+ "version": "1.5.0",
+ "resolved": "ghcr.io/devcontainers/features/rust@sha256:0c55e65f2e3df736e478f26ee4d5ed41bae6b54dac1318c443e31444c8ed283c",
+ "integrity": "sha256:0c55e65f2e3df736e478f26ee4d5ed41bae6b54dac1318c443e31444c8ed283c"
+ }
+ }
+}
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index d401329..2a0ac53 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -16,6 +16,10 @@
"ghcr.io/devcontainers/features/rust:1": {}
},
"postCreateCommand": "scripts/setup",
+ "mounts": [
+ "type=bind,source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh,target=/home/vscode/.ssh,readonly",
+ "type=volume,source=claude-config-cup-component,target=/home/vscode/.claude"
+ ],
"runArgs": [
"--network=host"
],
@@ -32,12 +36,10 @@
"vscode": {
"extensions": [
"ms-python.python",
- "github.vscode-pull-request-github",
"ms-python.vscode-pylance",
"charliermarsh.ruff",
"ms-python.black-formatter",
- "tamasfe.even-better-toml",
- "eamodio.gitlens"
+ "tamasfe.even-better-toml"
],
"settings": {
"files.insertFinalNewline": true,
diff --git a/CLAUDE.md b/CLAUDE.md
index e256b1d..148892b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,104 +1,38 @@
-# ha-clouding — Project context for Claude Code
-
-Home Assistant integration to monitor and control [Clouding.io](https://clouding.io) VMs.
-
-## File structure
-
-```
-custom_components/clouding/
-├── manifest.json domain=clouding, version=0.0.0, iot_class=cloud_polling, type=hub
-├── const.py Global constants
-├── __init__.py Entry setup, registers the 6 services
-├── coordinator.py DataUpdateCoordinator → GET /servers every N seconds
-├── config_flow.py UI config: api_key + name; options: update_interval (15-3600s)
-├── sensor.py 12 sensors per server
-├── binary_sensor.py 1 binary sensor per server (is_running)
-├── services.py Dispatcher for the 6 server actions
-├── device_info.py Builds HA DeviceInfo (manufacturer=Clouding.io)
-├── helpers.py purge_entities() — exists but IS NOT CALLED
-└── pythonclouding/
- ├── clouding.py aiohttp HTTP client, base_url=https://api.clouding.io/v1
- ├── models.py CloudingServer, CloudingServerImage, CloudingPublicPorts (mashumaro)
- ├── const.py BASE_URL
- └── exceptions.py CloudingError > Auth / BadRequest / Connection / InvalidAPIResponse
-```
-
-## Key constants (const.py)
-
-| Constant | Value |
-|---|---|
-| `DOMAIN` | `"clouding"` |
-| `MIN_TIME_BETWEEN_UPDATES` | `timedelta(seconds=300)` |
-| `CONF_UPDATE_INTERVAL` | `"update_interval"` |
-| `DEFAULT_NAME` | `"Clouding.io"` |
-| `PORTAL_URL` | `"https://portal.clouding.io"` |
-
-## Clouding.io API
-
-- **Base URL**: `https://api.clouding.io/v1`
-- **Auth**: header `X-API-KEY: {api_key}`
-- **Timeout**: 10s (aiohttp ClientTimeout)
-- **Endpoints used**:
- - `GET /servers` → lists all servers for the account
- - `POST /servers/{server_id}/{action}` → action: `start`, `stop`, `reboot`, `hard-reboot`, `archive`, `unarchive`
-
-## Exposed entities (per server)
-
-### Sensors (12)
-`flavor`, `hostname`, `private_ip`, `ram_gb` (DATA_SIZE, GB), `created_at` (TIMESTAMP), `dns_address`, `name`, `power_state`, `public_ip`, `status` (dynamic icon), `vcores`, `volume_size_gb` (DATA_SIZE, GB)
-
-**Status sensor** — dynamic icon based on value:
-- `archived` → `mdi:archive-check-outline`
-- `archiving`/`unarchiving` → `mdi:archive-clock-outline`
-- `stopped` → `mdi:close-circle-outline`
-- `starting`/`stopping` → `mdi:refresh-circle`
-- others → `mdi:check-circle-outline`
-
-### Binary sensor (1)
-- `is_running` — device_class=RUNNING, extra_attr: `{"Value": power_state_string}`
-
-## HA services (6, all device-targeted via device_id)
-
-| Service | API action |
-|---|---|
-| `start_server` | `start` |
-| `stop_server` | `stop` |
-| `reboot_server` | `reboot` |
-| `hard_reboot_server` | `hard-reboot` |
-| `archive_server` | `archive` |
-| `unarchive_server` | `unarchive` |
-
-After each action → `coordinator.async_refresh()` to pull the updated state.
-
-## CloudingServer model (mashumaro DataClassDictMixin)
-
-JSON fields → `attr_*` properties:
-- `id`, `name`, `hostname`, `flavor`, `status`, `powerState`, `privateIp`, `publicIp`
-- `dnsAddress`, `ramGb`, `vCores`, `volumeSizeGb`, `createdAt` (UTC datetime)
-- `image` (CloudingServerImage: id, name)
-- `publicPorts` (list of CloudingPublicPorts)
-- `attr_is_running` → bool: `powerState != "shutdown"`
-
-## Architectural patterns
-
-- **DataUpdateCoordinator**: centralised polling, all entities subscribe to it
-- **CoordinatorEntity**: base class for both sensors and binary sensors
-- **PARALLEL_UPDATES = 0**: sequential entity updates
-- `_handle_coordinator_update()`: comparison guard — only calls `async_write_ha_state` when value, icon, or attributes actually changed (DB optimisation)
-- **Config entry data**: `{CONF_NAME, CONF_API_KEY, CONF_UPDATE_INTERVAL}`
-- **No external dependencies**: `requirements=[]` in manifest, aiohttp is provided by HA
-
-## Known issues / constraints
-
-1. **`purge_entities()` not called** — servers deleted in Clouding.io leave ghost devices in HA (manual removal required)
-2. **No per-server filtering** — all servers in the API account are imported
-3. **Reload required** — changing `update_interval` requires an integration reload or HA restart
-4. **No webhooks** — pure polling, no event-driven updates
-5. **English only** — only `translations/en.json` provided
-6. **Server-side validation** — services do not check action feasibility before the API call (e.g. stopping an already-stopped server → 400)
-
-## Key decisions history
-
-- **Removed `Last Refresh`** from binary sensor extra_attributes: `dt_util.utcnow()` changed on every poll, forcing HA to write a new state row to the DB even when nothing had changed → DB bloat.
-- **Comparison guard** in `_handle_coordinator_update()` (sensor + binary sensor): only triggers `async_write_ha_state` when the value or attributes have actually changed.
-- **`_attr_is_on`** used in the binary sensor (native HA attribute) instead of `_attr_native_value`, which is reserved for `SensorEntity`.
+## Approach
+
+- Read existing files before writing. Don't re-read unless changed.
+- Thorough in reasoning, concise in output.
+- Skip files over 100KB unless required.
+- No sycophantic openers or closing fluff.
+- No emojis or em-dashes.
+- Do not guess APIs, versions, flags, commit SHAs, or package names. Verify by reading code or docs before asserting.
+
+## Output
+- Return code first. Explanation after, only if non-obvious.
+- No inline prose. Use comments sparingly - only where logic is unclear.
+- No boilerplate unless explicitly requested.
+
+## Code Rules
+- Simplest working solution. No over-engineering.
+- No abstractions for single-use operations.
+- No speculative features or "you might also want..."
+- Read the file before modifying it. Never edit blind.
+- No docstrings or type annotations on code not being changed.
+- No error handling for scenarios that cannot happen.
+- Three similar lines is better than a premature abstraction.
+
+## Review Rules
+- State the bug. Show the fix. Stop.
+- No suggestions beyond the scope of the review.
+- No compliments on the code before or after the review.
+
+## Debugging Rules
+- Never speculate about a bug without reading the relevant code first.
+- State what you found, where, and the fix. One pass.
+- If cause is unclear: say so. Do not guess.
+
+## Simple Formatting
+- No em dashes, smart quotes, or decorative Unicode symbols.
+- Plain hyphens and straight quotes only.
+- Natural language characters (accented letters, CJK, etc.) are fine when the content requires them.
+- Code output must be copy-paste safe.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b944274
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,1448 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ha-cup-component/LICENSE at develop · bastgau/ha-cup-component · GitHub
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
21 lines (17 loc) · 1.05 KB
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You can’t perform that action at this time.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/architecture.md b/architecture.md
new file mode 100644
index 0000000..e256b1d
--- /dev/null
+++ b/architecture.md
@@ -0,0 +1,104 @@
+# ha-clouding — Project context for Claude Code
+
+Home Assistant integration to monitor and control [Clouding.io](https://clouding.io) VMs.
+
+## File structure
+
+```
+custom_components/clouding/
+├── manifest.json domain=clouding, version=0.0.0, iot_class=cloud_polling, type=hub
+├── const.py Global constants
+├── __init__.py Entry setup, registers the 6 services
+├── coordinator.py DataUpdateCoordinator → GET /servers every N seconds
+├── config_flow.py UI config: api_key + name; options: update_interval (15-3600s)
+├── sensor.py 12 sensors per server
+├── binary_sensor.py 1 binary sensor per server (is_running)
+├── services.py Dispatcher for the 6 server actions
+├── device_info.py Builds HA DeviceInfo (manufacturer=Clouding.io)
+├── helpers.py purge_entities() — exists but IS NOT CALLED
+└── pythonclouding/
+ ├── clouding.py aiohttp HTTP client, base_url=https://api.clouding.io/v1
+ ├── models.py CloudingServer, CloudingServerImage, CloudingPublicPorts (mashumaro)
+ ├── const.py BASE_URL
+ └── exceptions.py CloudingError > Auth / BadRequest / Connection / InvalidAPIResponse
+```
+
+## Key constants (const.py)
+
+| Constant | Value |
+|---|---|
+| `DOMAIN` | `"clouding"` |
+| `MIN_TIME_BETWEEN_UPDATES` | `timedelta(seconds=300)` |
+| `CONF_UPDATE_INTERVAL` | `"update_interval"` |
+| `DEFAULT_NAME` | `"Clouding.io"` |
+| `PORTAL_URL` | `"https://portal.clouding.io"` |
+
+## Clouding.io API
+
+- **Base URL**: `https://api.clouding.io/v1`
+- **Auth**: header `X-API-KEY: {api_key}`
+- **Timeout**: 10s (aiohttp ClientTimeout)
+- **Endpoints used**:
+ - `GET /servers` → lists all servers for the account
+ - `POST /servers/{server_id}/{action}` → action: `start`, `stop`, `reboot`, `hard-reboot`, `archive`, `unarchive`
+
+## Exposed entities (per server)
+
+### Sensors (12)
+`flavor`, `hostname`, `private_ip`, `ram_gb` (DATA_SIZE, GB), `created_at` (TIMESTAMP), `dns_address`, `name`, `power_state`, `public_ip`, `status` (dynamic icon), `vcores`, `volume_size_gb` (DATA_SIZE, GB)
+
+**Status sensor** — dynamic icon based on value:
+- `archived` → `mdi:archive-check-outline`
+- `archiving`/`unarchiving` → `mdi:archive-clock-outline`
+- `stopped` → `mdi:close-circle-outline`
+- `starting`/`stopping` → `mdi:refresh-circle`
+- others → `mdi:check-circle-outline`
+
+### Binary sensor (1)
+- `is_running` — device_class=RUNNING, extra_attr: `{"Value": power_state_string}`
+
+## HA services (6, all device-targeted via device_id)
+
+| Service | API action |
+|---|---|
+| `start_server` | `start` |
+| `stop_server` | `stop` |
+| `reboot_server` | `reboot` |
+| `hard_reboot_server` | `hard-reboot` |
+| `archive_server` | `archive` |
+| `unarchive_server` | `unarchive` |
+
+After each action → `coordinator.async_refresh()` to pull the updated state.
+
+## CloudingServer model (mashumaro DataClassDictMixin)
+
+JSON fields → `attr_*` properties:
+- `id`, `name`, `hostname`, `flavor`, `status`, `powerState`, `privateIp`, `publicIp`
+- `dnsAddress`, `ramGb`, `vCores`, `volumeSizeGb`, `createdAt` (UTC datetime)
+- `image` (CloudingServerImage: id, name)
+- `publicPorts` (list of CloudingPublicPorts)
+- `attr_is_running` → bool: `powerState != "shutdown"`
+
+## Architectural patterns
+
+- **DataUpdateCoordinator**: centralised polling, all entities subscribe to it
+- **CoordinatorEntity**: base class for both sensors and binary sensors
+- **PARALLEL_UPDATES = 0**: sequential entity updates
+- `_handle_coordinator_update()`: comparison guard — only calls `async_write_ha_state` when value, icon, or attributes actually changed (DB optimisation)
+- **Config entry data**: `{CONF_NAME, CONF_API_KEY, CONF_UPDATE_INTERVAL}`
+- **No external dependencies**: `requirements=[]` in manifest, aiohttp is provided by HA
+
+## Known issues / constraints
+
+1. **`purge_entities()` not called** — servers deleted in Clouding.io leave ghost devices in HA (manual removal required)
+2. **No per-server filtering** — all servers in the API account are imported
+3. **Reload required** — changing `update_interval` requires an integration reload or HA restart
+4. **No webhooks** — pure polling, no event-driven updates
+5. **English only** — only `translations/en.json` provided
+6. **Server-side validation** — services do not check action feasibility before the API call (e.g. stopping an already-stopped server → 400)
+
+## Key decisions history
+
+- **Removed `Last Refresh`** from binary sensor extra_attributes: `dt_util.utcnow()` changed on every poll, forcing HA to write a new state row to the DB even when nothing had changed → DB bloat.
+- **Comparison guard** in `_handle_coordinator_update()` (sensor + binary sensor): only triggers `async_write_ha_state` when the value or attributes have actually changed.
+- **`_attr_is_on`** used in the binary sensor (native HA attribute) instead of `_attr_native_value`, which is reserved for `SensorEntity`.
diff --git a/scripts/run-ia b/scripts/run-ia
deleted file mode 100755
index 4952c2e..0000000
--- a/scripts/run-ia
+++ /dev/null
@@ -1,7 +0,0 @@
-npm install -g @modelcontextprotocol/server-filesystem
-
-pip install mcp-server-git
-pip install mcp-proxy
-
-mcp-proxy --port 8081 -- mcp-server-filesystem ./custom_components/ &
-mcp-proxy --port 8082 -- mcp-server-git --repository ./