Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions .devcontainer/devcontainer-lock.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
8 changes: 5 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
],
Expand All @@ -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,
Expand Down
142 changes: 38 additions & 104 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading