From cb773dbb6d5e7b2d4d5928df7130814dc9eceb52 Mon Sep 17 00:00:00 2001 From: Bastien Gautier Date: Sun, 21 Jun 2026 14:43:20 +0000 Subject: [PATCH 1/4] add documentation --- CLAUDE.md | 142 +++++++++++++----------------------------------- architecture.md | 104 +++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 104 deletions(-) create mode 100644 architecture.md 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/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`. From cb0c2829b8c5074b61537422e4404a75b282ed74 Mon Sep 17 00:00:00 2001 From: Bastien Gautier Date: Sun, 21 Jun 2026 14:43:54 +0000 Subject: [PATCH 2/4] upgrade configuration for claude --- .devcontainer/devcontainer-lock.json | 24 ++++++++++++++++++++++++ .devcontainer/devcontainer.json | 8 +++++--- 2 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 .devcontainer/devcontainer-lock.json 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, From 234080da85f0356cb0f3cf3b444ad53695e97594 Mon Sep 17 00:00:00 2001 From: Bastien Gautier Date: Sun, 21 Jun 2026 14:44:10 +0000 Subject: [PATCH 3/4] remove irrelevant file --- scripts/run-ia | 7 ------- 1 file changed, 7 deletions(-) delete mode 100755 scripts/run-ia 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 ./ From e827c70a7afcb570685aed433fbdb70d8eb2716a Mon Sep 17 00:00:00 2001 From: Bastien Gautier Date: Sun, 21 Jun 2026 14:44:53 +0000 Subject: [PATCH 4/4] add licence --- LICENSE | 1448 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1448 insertions(+) create mode 100644 LICENSE 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Skip to content + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + + / + + ha-cup-component + + + Public +
+ + +
+ +
+ + +
+
+ +
+
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + +

Latest commit

 

History

History
21 lines (17 loc) · 1.05 KB

File metadata and controls

21 lines (17 loc) · 1.05 KB
+
+ + + + +
+ +
+ +
+
+ +
+ +
+

Footer

+ + + + +
+
+ + + + + © 2026 GitHub, Inc. + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + +