first commit

This commit is contained in:
Mateusz Gruszczyński
2026-08-23 21:34:07 +02:00
commit 1d3dcba1a9
62 changed files with 12456 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
# API examples
`TOKEN` is optional when `GREE_CONTROLLER_APP_TOKEN` is empty.
```bash
AUTH='Authorization: Bearer TOKEN'
BASE='http://127.0.0.1:8787'
```
## Home Assistant access tokens
Create and revoke integration tokens from the controller Web UI under **Settings -> Home Assistant integration access**. The clear-text secret is returned only once and the SQLite database stores only its SHA-256 hash.
Administrator endpoints:
```text
GET /api/access-tokens
POST /api/access-tokens
DELETE /api/access-tokens/{id}
```
The Home Assistant custom integration uses a restricted API surface:
```text
GET /api/integrations/home-assistant/devices
POST /api/integrations/home-assistant/devices/{id}/command
```
These two endpoints always require `Authorization: Bearer <generated-token>` (or the administrator `GREE_CONTROLLER_APP_TOKEN`). A generated HA token cannot update settings, run discovery, delete devices, manage tokens, or use the controller WebSocket.
## Discovery
```bash
curl -X POST "$BASE/api/discovery" -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"timeout_ms":3000,"broadcast":"255.255.255.255:7000"}'
```
## Device command
```json
{
"power": true,
"mode": "cool",
"target_temperature": 22.0,
"fan_speed": 3,
"swing_vertical": true,
"quiet": false,
"turbo": false,
"light": true
}
```
Supported modes: `auto`, `cool`, `dry`, `fan`, `heat`. Fan speed: `0..5`.
## Zone
```json
{
"name": "Living room",
"device_id": "gree-aabbccddeeff",
"enabled": true,
"mode": "cool",
"setpoint": 23.0,
"hysteresis": 0.6,
"min_on_seconds": 180,
"min_off_seconds": 180,
"sensor_source": "combined",
"ha_entity_id": "sensor.living_room_temperature",
"external_sensor_weight": 0.4,
"max_sensor_difference": 3.0
}
```
`sensor_source` supports:
- `device` — GREE indoor sensor only,
- `combined` — GREE + this zone's HA room sensor,
- `home_assistant` — this zone's HA room sensor, with GREE fallback.
For `combined` and `home_assistant`, set a per-zone `ha_entity_id`. `external_sensor_weight` is `0.0..1.0`. If a combined sensor pair differs by more than `max_sensor_difference`, the controller falls back to GREE. The returned zone object includes `device_temperature`, `external_temperature`, `current_temperature`, and `control_temperature_source`.
## Schedule
Weekdays use ISO numbers: Monday `1`, Sunday `7`.
```json
{
"zone_id": "UUID",
"name": "Night",
"enabled": true,
"weekdays": [1,2,3,4,5,6,7],
"start_time": "22:00",
"end_time": "06:00",
"setpoint": 24.0
}
```
## WebSocket
Connect to `ws://HOST:8787/ws?token=TOKEN`. The first message uses event type `bootstrap`; later events include `device.updated`, `zone.updated`, `settings.updated` and `log.created`.
## Localization assets
Localization endpoints are public because the login dialog also needs translations. Language packs are embedded in the Rust binary at build time.
```bash
curl "$BASE/lang/index.json"
curl "$BASE/lang/en.json"
```
`GET /lang/index.json` returns the automatically generated language catalog. `GET /lang/<code>.json` returns the corresponding language pack. Add a valid `lang/<code>.json` file and rebuild to expose a new language.
+67
View File
@@ -0,0 +1,67 @@
# Home Assistant entity-ID migration
## Goal
Replace an existing GREE climate entity while preserving its `entity_id`.
Example:
```text
Before: climate.klima_salon -> default GREE integration -> AC
After: climate.klima_salon -> GREE Controller integration -> Rust controller -> AC
```
Keeping the same entity ID allows existing dashboards, scripts, scenes and automations that reference the entity by ID to continue working.
## Why a takeover step is required
Home Assistant's entity registry reserves entity IDs. A second integration cannot create another active `climate.klima_salon` while the original entity still exists. The new integration therefore checks for conflicts and stops setup rather than allowing HA to generate a suffixed name such as `climate.klima_salon_2`.
## Generate the mapping
For one entity:
```bash
./scripts/generate_ha_migration.py \
--entity climate.klima_salon \
--device gree-aabbccddeeff
```
For several:
```bash
./scripts/generate_ha_migration.py \
--map climate.klima_salon=gree-aabbccddeeff \
--map climate.klima_sypialnia=gree-112233445566
```
Generated structure:
```json
{
"version": 1,
"entities": [
{
"entity_id": "climate.klima_salon",
"device_id": "gree-aabbccddeeff"
}
]
}
```
Copy it to `/config/gree_controller_entities.json` on the Home Assistant host.
## Migration sequence
1. Add the physical AC to the standalone Rust application.
2. Test power, mode and target temperature from its web UI.
3. Generate the HA entity mapping.
4. Copy the custom component to `/config/custom_components/gree_controller/`.
5. Disable/remove the previous GREE integration in HA so it no longer controls or publishes the old climate entity.
6. Remove any stale entity registry entry only after the old integration is unloaded.
7. Add the **GREE Controller** integration and provide its URL/token.
8. Confirm the exact old entity ID is present again.
9. Test `climate.set_temperature`, HVAC modes and power from HA.
10. Verify automations and dashboards that use the preserved entity ID.
The standalone controller remains available during the HA migration, so the AC can still be controlled from its own web UI if HA is restarting.
+71
View File
@@ -0,0 +1,71 @@
# Localization
The web interface uses JSON language packs from `lang/`. Language files are discovered at Rust build time and embedded in the application binary.
English (`lang/en.json`) is required and is always the fallback language. Polish (`lang/pl.json`) is included by default.
## Add a language
1. Copy `lang/en.json` to a file named with the new language code, for example `lang/de.json`.
2. Update the `meta` object.
3. Translate values inside `translations`. Do not rename translation keys.
4. Run `./scripts/dev.sh --check` or build the project again.
5. Start the rebuilt binary. The new language appears automatically in the language selector.
Example structure:
```json
{
"meta": {
"code": "de",
"name": "German",
"native_name": "Deutsch",
"locale": "de-DE"
},
"translations": {
"controls.language": "Sprache",
"controls.theme": "Darstellung"
}
}
```
The filename and `meta.code` must match (`de.json` -> `"code": "de"`). File names may contain ASCII letters, digits, `-` and `_` only.
## Fallback behavior
A language pack does not have to duplicate every English key while it is being developed. If a key is missing from the selected language, the UI uses the value from `en.json`. If a key is also missing from English, the translation key itself is shown, which makes incomplete strings visible during development.
## Build-time validation
`build.rs` checks that:
- at least one JSON language file exists,
- `en.json` exists,
- every language file contains valid JSON,
- every file has `meta.code`, `meta.name`, `meta.native_name` and `meta.locale`,
- `meta.code` matches the filename,
- `translations` is a JSON object.
A malformed language pack fails the Rust build instead of producing a broken selector at runtime.
## Runtime endpoints
The embedded language catalog is available at:
```text
GET /lang/index.json
```
Individual embedded packs are available at:
```text
GET /lang/en.json
GET /lang/pl.json
GET /lang/<code>.json
```
These endpoints are intentionally public so that localization also works before API authentication is completed.
## Browser preference
The selected language code is stored for one year in the `gree_controller_language` cookie. If the stored language is no longer present in a later build, the UI falls back to English.
+110
View File
@@ -0,0 +1,110 @@
# LXC installation and update
This document describes the supported Debian/Ubuntu systemd deployment used for LXC testing.
## First installation
Unpack the source archive inside the container and run:
```bash
cd gree-controller
chmod +x scripts/*.sh
sudo ./scripts/install.sh
```
The installer:
1. installs required build packages when missing,
2. installs stable Rust with rustup when Cargo is unavailable,
3. runs `cargo test --all-targets`,
4. builds `target/release/gree-controller`,
5. creates the `gree-controller` system user/group,
6. creates `/var/lib/gree-controller`, `/opt/gree-controller` and `/var/backups/gree-controller`,
7. installs the systemd unit,
8. creates `/etc/gree-controller.env` only when it does not already exist,
9. enables/restarts the service,
10. verifies `GET /api/health`.
Installed state is intentionally outside the extracted source directory:
```text
/opt/gree-controller/gree-controller installed binary
/etc/gree-controller.env persistent configuration/secrets
/var/lib/gree-controller/gree-controller.db persistent SQLite database
/var/backups/gree-controller/ update backups
/etc/systemd/system/gree-controller.service systemd service
```
The default installation starts in simulator mode. Change `GREE_CONTROLLER_SIMULATE=false` and `GREE_CONTROLLER_AUTO_SEED=false` in `/etc/gree-controller.env` when moving to physical units, then restart the service.
## Update
Unpack a newer source archive and run from that new directory:
```bash
sudo ./scripts/update.sh
```
The update workflow is designed to minimize downtime and preserve the database:
1. build dependencies/Rust are checked,
2. tests run while the currently installed controller stays online,
3. the new release binary is built while the old service stays online,
4. the service is stopped,
5. the installed binary, service unit, environment file and SQLite database are copied to `/var/backups/gree-controller/<UTC timestamp>/`,
6. the new binary/unit is installed,
7. systemd starts the new version,
8. `/api/health` is checked,
9. on failure the old binary, unit and database are restored automatically.
The updater does not overwrite `/etc/gree-controller.env` during a successful update.
Use `--skip-tests` only for deliberate fast testing:
```bash
sudo ./scripts/update.sh --skip-tests
```
## Service helper
```bash
./scripts/service.sh status
./scripts/service.sh health
./scripts/service.sh logs
sudo ./scripts/service.sh restart
sudo ./scripts/service.sh stop
sudo ./scripts/service.sh start
```
Equivalent native commands remain available:
```bash
systemctl status gree-controller
journalctl -u gree-controller -f
```
## Configuration
Edit:
```text
/etc/gree-controller.env
```
and restart:
```bash
sudo ./scripts/service.sh restart
```
Do not store controller or Home Assistant secrets in the source tree.
## Database and SQL layout
Runtime persistence uses SQLite. All schema definitions and SQL statements in the Rust application are centralized in:
```text
src/queries.rs
```
`src/db.rs` contains connection/transaction logic and maps database rows to Rust domain models, but it does not embed SQL statements.
+162
View File
@@ -0,0 +1,162 @@
# GREE Controller project specification
## Product goal
Build an autonomous local GREE HVAC controller running primarily as a Rust service in a dedicated Linux/LXC environment. Home Assistant is optional: it can provide external sensor data and can consume controller devices through a thin custom integration, but it must not contain the core GREE protocol or heating logic.
## Architecture
```text
Home Assistant / Web UI / REST clients
|
REST + WebSocket
|
GREE Controller (Rust)
+--------------------------------+
| Device/state manager |
| GREE protocol UDP/AES |
| Zone/heating engine |
| Scheduler |
| Automation engine |
| SQLite |
| Mobile-first web UI |
+--------------------------------+
|
UDP/7000
|
GREE AC
```
## Independence requirements
- GREE control must work when Home Assistant is stopped.
- Local schedules and automations must continue without HA.
- HA sensor failures must never remove basic access to the AC.
- Device state is persisted by the controller and can be restored to clients after reconnect/restart.
## GREE protocol layer
The protocol implementation is isolated from application logic and covers:
- discovery,
- packet encoding/decoding,
- AES-128-ECB support,
- AES-128-GCM envelope support,
- bind/key acquisition,
- status polling,
- command transport,
- reconnect/offline handling.
The rest of the application works with generic device state and commands rather than raw protocol packets.
## Device model
Each device stores:
- stable controller ID,
- MAC/CID,
- name,
- IP/port,
- protocol version,
- model/firmware when available,
- encryption key when bound,
- enabled/simulated flags,
- power and HVAC mode,
- target/current/outdoor temperature,
- fan, swing, quiet, turbo, light,
- online/last-seen/error state.
## Temperature zones
A zone connects an AC to a temperature-control policy. It contains:
- target setpoint,
- heat/cool mode,
- hysteresis,
- minimum ON time,
- minimum OFF time,
- temperature source,
- optional Home Assistant sensor entity.
Each zone can assign its own Home Assistant room sensor. Combined mode fuses the GREE and room measurements with a configurable weight (40% room sensor by default), while GREE remains the primary input. If HA is unavailable or the two sensors differ beyond the configured limit, control falls back to the GREE sensor.
## Scheduler and automations
Schedules are weekly time windows and may cross midnight. Automations currently support time or temperature triggers and send device commands with cooldown protection.
The automation engine runs in Rust and does not require Home Assistant YAML automation logic.
## API
The controller exposes REST for configuration/commands and WebSocket for live state updates. The HA integration and web UI use the same controller API.
## Home Assistant direction 1: external sensor input
The Rust application can read a specifically configured HA entity using a Long-Lived Access Token. This input is optional and is not a prerequisite for GREE control.
## Home Assistant direction 2: native climate entities
A custom integration creates HA climate entities and translates HA service calls to controller API commands. It does not communicate with GREE directly.
For migrations from the default GREE integration, a mapping file can request an existing entity ID such as `climate.klima_salon`. The previous integration must release that ID before takeover.
## Web UI
Primary use is from a phone. Requirements:
- responsive mobile-first layout,
- touch-friendly controls,
- live state updates,
- device, zone, schedule, automation, history and diagnostics screens,
- JSON-based language packs from `lang/*.json`, with English as the required default/fallback and Polish included,
- automatic language discovery at build time, so a new valid `<code>.json` file adds a language without JavaScript changes,
- language stored in the `gree_controller_language` cookie,
- system/light/dark appearance modes,
- appearance stored in a cookie,
- flat visual design without decorative shadows,
- no letter-logo badge next to the application name.
## Storage
SQLite stores configuration, state, readings and events. PostgreSQL or other external database services are not required for the single-node deployment target.
## Deployment
Primary target:
```text
LXC / Debian or Ubuntu
/opt/gree-controller/gree-controller
/etc/gree-controller.env
/var/lib/gree-controller/gree-controller.db
systemd: gree-controller.service
```
A development script prepares dependencies/build/runtime configuration, and a separate installer creates the systemd deployment.
## Security
- optional administrator Bearer token for the controller Web/API,
- separately generated, revocable Home Assistant client tokens stored only as SHA-256 hashes,
- Home Assistant client tokens are restricted to device read/control endpoints,
- secrets kept outside source control,
- no direct public Internet exposure,
- TLS delegated to a trusted reverse proxy/VPN when required,
- local device keys and HA token protected in environment/database files.
## Per-zone room sensors
A zone represents one room and normally maps one GREE indoor unit to one optional external room-temperature sensor. External sensors are not global. For example, Living Room can use `sensor.living_room_temperature` while Bedroom independently uses `sensor.bedroom_temperature`.
The zone controller supports `device`, `combined`, and `home_assistant` temperature strategies. Combined mode uses a configurable external-sensor weight (40% by default), validates the difference between sensors, and falls back to the GREE sensor when the external source is missing or outside the configured discrepancy limit. The local GREE measurement therefore remains available even when Home Assistant is offline.
## Operational packaging conventions
- All operator-facing shell/Python utilities live under `scripts/`.
- `scripts/install.sh` performs the first systemd/LXC installation.
- `scripts/update.sh` performs an in-place update with a stopped SQLite backup, health check and automatic rollback.
- `scripts/service.sh` provides common systemd operations.
- Cargo's `build.rs` stays at the package root because Cargo requires that location.
- Every SQLite statement and schema definition is centralized in `src/queries.rs`; database/domain code must not embed SQL strings elsewhere.
File diff suppressed because it is too large Load Diff