# GREE Controller API reference HTTP and WebSocket API for GREE Controller **0.9.2**. [← Main documentation](../README.md) ## Base URL and content type Default local address: ```text http://127.0.0.1:8787 ``` JSON requests use: ```text Content-Type: application/json ``` If `GREE_CONTROLLER_BASE_PATH=/gree` is configured, every HTTP and WebSocket path below is prefixed with `/gree`. ## Authentication There are three access levels. ### Public No token is required for: ```text GET /api/health GET / GET /index.html GET /app.js GET /theme-init.js GET /styles.css GET /manifest.webmanifest GET /sw.js GET /favicon.svg GET /lang/index.json GET /lang/{file} ``` ### Administrator API All normal `/api/*` routes are administrator routes. If `GREE_CONTROLLER_APP_TOKEN` is empty, the controller intentionally operates in trusted-LAN mode and these routes do not require authentication. When an app token is configured, send either: ```text Authorization: Bearer APP_TOKEN ``` or: ```text x-api-token: APP_TOKEN ``` Example: ```bash BASE='http://127.0.0.1:8787' TOKEN='replace-me' curl -H "Authorization: Bearer $TOKEN" "$BASE/api/bootstrap" ``` ### Restricted Home Assistant API Generated Home Assistant access tokens always authenticate only the restricted integration surface under `/api/integrations/home-assistant/*`. The administrator app token is also accepted there. Generated token secrets are returned only at creation time. SQLite stores their SHA-256 hash and a display prefix. ## Errors and HTTP status codes API errors are JSON: ```json { "error": "message" } ``` Common statuses: | Status | Meaning | | --- | --- | | `200 OK` | Successful read/update/action. | | `201 Created` | Resource created. | | `204 No Content` | Successful delete/revoke. | | `400 Bad Request` | Validation error or unsafe/invalid operation. | | `401 Unauthorized` | Missing/incorrect token. | | `404 Not Found` | Resource ID does not exist. | | `409 Conflict` | Revision/concurrency conflict. | | `502 Bad Gateway` | GREE/HA/integration communication failure. | | `500 Internal Server Error` | Unexpected server/storage error. | ## Endpoint index ### Public and system | Method | Endpoint | Description | | --- | --- | --- | | GET | `/api/health` | Lightweight process/control-engine health. | | GET | `/api/bootstrap` | Complete initial application snapshot. | | GET | `/api/system/info` | Runtime/system diagnostic information. | | GET | `/ws` | Live WebSocket event stream. | ### Devices and discovery | Method | Endpoint | Description | | --- | --- | --- | | POST | `/api/discovery` | Discover/bind GREE devices. | | GET | `/api/devices` | List devices. | | POST | `/api/devices` | Add a device manually. | | GET | `/api/devices/{id}` | Read a device. | | PATCH | `/api/devices/{id}` | Edit technical device configuration. | | DELETE | `/api/devices/{id}` | Delete a device after safety checks. | | POST | `/api/devices/{id}/bind` | Bind/re-bind a physical unit. | | POST | `/api/devices/{id}/poll` | Poll one unit immediately. | | POST | `/api/devices/{id}/command` | Send a direct/manual device command. | ### Thermostat zones, groups and house | Method | Endpoint | Description | | --- | --- | --- | | GET | `/api/zones` | List zones. | | POST | `/api/zones` | Create a zone. | | GET | `/api/zones/{id}` | Read a zone. | | PUT | `/api/zones/{id}` | Replace editable zone configuration. | | DELETE | `/api/zones/{id}` | Delete zone after safe device shutdown. | | POST | `/api/zones/{id}/control` | Quick/thermostat control of a zone. | | POST | `/api/zones/{id}/schedule-template` | Replace zone schedules with a built-in template. | | GET | `/api/groups` | List climate groups. | | POST | `/api/groups` | Create a climate group. | | GET | `/api/groups/{id}` | Read a group. | | PUT | `/api/groups/{id}` | Replace group definition. | | DELETE | `/api/groups/{id}` | Delete a group. | | POST | `/api/groups/{id}/control` | Group control enable/mode/preset/custom-temperature control. | | POST | `/api/house/control` | Set global thermostat mode. | | POST | `/api/house/power` | Set whole-house master power. | | POST | `/api/house/preset` | Set/clear whole-house preset override. | ### Schedules and automations | Method | Endpoint | Description | | --- | --- | --- | | GET | `/api/schedules` | List schedules. | | POST | `/api/schedules` | Create schedule. | | GET | `/api/schedules/{id}` | Read schedule. | | PUT | `/api/schedules/{id}` | Replace schedule. | | DELETE | `/api/schedules/{id}` | Delete schedule. | | GET | `/api/automations` | List automations. | | POST | `/api/automations` | Create automation. | | GET | `/api/automations/{id}` | Read automation. | | PUT | `/api/automations/{id}` | Replace automation. | | DELETE | `/api/automations/{id}` | Delete automation. | ### History, control plan and events | Method | Endpoint | Description | | --- | --- | --- | | GET | `/api/readings` | Legacy/device reading history. | | GET | `/api/history` | Rich device/zone/HA history. | | GET | `/api/control-plan` | Current resolved thermostat plan. | | GET | `/api/events` | Event/debug log. | | GET | `/api/events/retention` | Current event retention. | | PUT | `/api/events/retention` | Update retention and prune immediately. | ### Settings, backup and diagnostics | Method | Endpoint | Description | | --- | --- | --- | | GET | `/api/settings` | Public-safe runtime settings. | | PUT | `/api/settings` | Update runtime settings. | | GET | `/api/settings/export` | Export full application configuration. | | POST | `/api/settings/import` | Import/replace application configuration. | | GET | `/api/debug` | Read debug overlay settings. | | PUT | `/api/debug` | Update debug overlay settings. | | POST | `/api/integrations/home-assistant/test` | Test HA temperature read. | | POST | `/api/integrations/notifications/test` | Send a test notification. | ### Access tokens and restricted Home Assistant API | Method | Endpoint | Description | | --- | --- | --- | | GET | `/api/access-tokens` | List generated HA tokens without secrets. | | POST | `/api/access-tokens` | Create restricted HA token. | | DELETE | `/api/access-tokens/{id}` | Revoke token. | | GET | `/api/integrations/home-assistant/devices` | Restricted device list. | | POST | `/api/integrations/home-assistant/devices/{id}/command` | Restricted direct device command. | | GET | `/api/integrations/home-assistant/control-plan` | Restricted control plan. | | GET | `/api/integrations/home-assistant/groups` | HA-oriented group state. | | POST | `/api/integrations/home-assistant/groups/{id}/control` | Restricted group control. | | POST | `/api/integrations/home-assistant/house/control` | Restricted house mode. | | POST | `/api/integrations/home-assistant/house/preset` | Restricted house preset. | | POST | `/api/integrations/home-assistant/house/power` | Restricted master power. | | POST | `/api/integrations/home-assistant/zones/{id}/control` | Restricted thermostat-zone control. | --- ## System endpoints ### `GET /api/health` Public lightweight health check. Response: ```json { "status": "ok", "name": "gree-controller", "version": "0.9.2", "uptime_seconds": 1234, "control_ready": true, "time": "2026-08-30T06:54:00Z" } ``` `control_ready=false` means the process is running but the thermostat engine has not yet completed its initial physical device synchronization. ### `GET /api/bootstrap` Returns the initial Web UI snapshot: ```json { "devices": [], "zones": [], "groups": [], "schedules": [], "automations": [], "access_tokens": [], "settings": {}, "outdoor_temperature": null, "system": { "version": "0.9.2", "uptime_seconds": 1234, "auth_required": false, "control_ready": true, "database": "./data/gree-controller.db", "device_count": 2, "online_count": 2, "simulator_count": 0, "bind": "0.0.0.0:8787", "base_path": "/", "gree_interface": "auto", "gree_received_frames": 809, "gree_received_frames_by_device": {} } } ``` ### `GET /api/system/info` Returns the `system` diagnostic object independently of the full bootstrap. Useful for monitoring and **Settings → System status**. --- ## Device API ### Device object A device response contains: | Field | Type | Description | | --- | --- | --- | | `id` | string | Stable controller ID. | | `mac` | string | Normalized GREE MAC/CID identity. | | `name` | string | User-visible name. | | `ip` | string | Device IPv4 address. | | `port` | integer | Usually `7000`. | | `protocol_version` | integer | `0` unknown/auto, `1` legacy AES-ECB, `2` AES-GCM. | | `model`, `firmware` | string | Discovered metadata when available. | | `key` | string/null | GREE binding key. Treat as secret. | | `cid` | string/null | GREE client/device identifier. | | `enabled` | boolean | Technical device enable state. | | `simulated` | boolean | Simulated vs physical. | | `power` | boolean | Last known power. | | `mode` | string | `auto`, `cool`, `dry`, `fan`, `heat`. | | `target_temperature` | number | Last known unit setpoint. | | `fan_speed` | integer | `0..5`; `0` is Auto. | | `swing_vertical`, `swing_horizontal` | boolean | Swing state. | | `quiet`, `turbo`, `light`, `air`, `xfan`, `health`, `sleep` | boolean | Optional GREE features. | | `supports_*` | boolean/null | Capability learned from device status. | | `current_temperature` | number/null | GREE indoor temperature. | | `outdoor_temperature` | number/null | GREE outdoor temperature if available. | | `temperature_sensor_offset` | boolean/null | Whether +40 °C wire offset behavior was detected. | | `online` | boolean | Current communication state. | | `response_time_ms` | integer/null | Latest successful controller round-trip. | | `last_seen` | ISO-8601/null | Last successful communication. | | `last_error` | string/null | Latest communication error. | | `communication_failures` | integer | Consecutive/recorded communication failure counter. | | `created_at`, `updated_at` | ISO-8601 | Resource timestamps. | ### `POST /api/discovery` Request body, all fields optional: ```json { "timeout_ms": 6000, "broadcast": "255.255.255.255:7000", "protocol_version": 0, "passes": 3 } ``` Rules: - `timeout_ms`: effective range `500..30000` ms. - `protocol_version`: `0` auto/both, `1` AES-ECB only, `2` AES-GCM only. - `passes`: `1..10`. - Missing values use runtime GREE settings. Successful discovery merges known devices, tries binding devices that do not have a key, persists results and returns: ```json { "count": 1, "devices": [], "new_device_ids": ["gree-aabbccddeeff"] } ``` ### `GET /api/devices` Returns `Device[]`. ### `POST /api/devices` Manual add request: ```json { "name": "Living room", "mac": "AABBCCDDEEFF", "ip": "192.168.50.30", "port": 7000, "protocol_version": 1, "key": null, "simulated": false } ``` Defaults: `port=7000`, `protocol_version=1`, `simulated=false`. MAC values are normalized. Duplicate MACs are rejected. Returns `201 Created` with `Device`. ### `GET /api/devices/{id}` Returns one `Device` or `404`. ### `PATCH /api/devices/{id}` All fields optional: ```json { "name": "Bedroom", "ip": "192.168.50.31", "port": 7000, "protocol_version": 2, "key": "optional-binding-key", "enabled": true } ``` `key:null` clears the key. Changing protocol version clears the existing key/capability cache so the unit can be re-bound cleanly. Disabling a device goes through the controller's safe disable path. ### `DELETE /api/devices/{id}` Returns `204`. Deletion is rejected if the device is referenced by an automation or cannot be safely detached from thermostat ownership. Associated zones/groups are cleaned only after the safety checks pass. ### `POST /api/devices/{id}/bind` Performs/repeats GREE binding and returns updated `Device`. Simulated devices return unchanged. ### `POST /api/devices/{id}/poll` Immediately polls one unit and returns updated `Device`. ### `POST /api/devices/{id}/command` Direct/manual device control. This is deliberately different from thermostat-zone control. All fields optional; at least one meaningful field should be sent: ```json { "power": true, "mode": "cool", "target_temperature": 22, "fan_speed": 3, "swing_vertical": true, "swing_horizontal": false, "quiet": false, "turbo": false, "light": true, "air": false, "xfan": false, "health": false, "sleep": false } ``` Rules: - modes: `auto`, `cool`, `dry`, `fan`, `heat`, - target temperature is normalized to the supported GREE Celsius range `8..30`, - fan speed is `0..5`, - optional feature commands should be used only when the corresponding `supports_*` capability is true. The backend sends only properties that differ from the last known device state. Climate-relevant direct commands can create/continue a manual-device takeover for an enabled thermostat zone so automation does not immediately fight the user. --- ## Zones ### Zone configuration `POST /api/zones` and `PUT /api/zones/{id}` use this editable shape: ```json { "name": "Living room", "device_id": "gree-aabbccddeeff", "enabled": true, "mode": "cool", "inherit_house_mode": true, "setpoint": 23.0, "cool_comfort_setpoint": 23.0, "cool_sleep_setpoint": 24.5, "cool_away_setpoint": 27.0, "heat_comfort_setpoint": 21.0, "heat_sleep_setpoint": 19.0, "heat_away_setpoint": 17.0, "hysteresis": 0.6, "separate_hysteresis": false, "cool_hysteresis": 0.6, "heat_hysteresis": 0.6, "min_on_seconds": 180, "min_off_seconds": 180, "min_adjust_seconds": 120, "standby_offset_c": 2.0, "smart_fan": true, "sensor_source": "combined", "ha_entity_id": "sensor.living_room_temperature", "external_sensor_weight": 0.4, "max_sensor_difference": 3.0, "sensor_stale_after_seconds": 300, "revision": 12 } ``` Important rules: - `name` is required. - `device_id` must reference an existing device and thermostat ownership must remain valid/safe. - `mode`: `cool` or `heat` when not inheriting house mode. - temperature/profile values are constrained to the supported thermostat range. - `hysteresis`: shared controller hysteresis, valid range `0.1..5.0` °C. - `separate_hysteresis`: when `true`, cooling uses `cool_hysteresis` and heating uses `heat_hysteresis`; both use the same `0.1..5.0` °C range. - `standby_offset_c`: bounded thermostat offset. - `sensor_source`: `device`, `home_assistant` or `combined`. - `external_sensor_weight`: `0..1`. - `revision` is used for optimistic concurrency where supplied; stale updates can return `409`. The returned `Zone` also contains runtime state including sensor readings, resolved/effective setpoints, demand, current preset, manual overrides, local Quick Thermostat ownership, temporary session state, device-manual takeover, control owner/source/reason, lockout timestamps and `created_at`/`updated_at`. ### `GET /api/zones` Returns `Zone[]`. ### `POST /api/zones` Creates a zone and returns `201 Created` with `Zone`. ### `GET /api/zones/{id}` Returns one `Zone`. ### `PUT /api/zones/{id}` Replaces editable zone configuration while preserving/reconciling runtime safety state. Returns updated `Zone`. ### `DELETE /api/zones/{id}` Safely powers the owned device off before detaching thermostat ownership. Returns `204`. ### `POST /api/zones/{id}/control` Manual `setpoint` values are retained with 0.1 °C precision. The physical GREE unit setpoint is still rounded to the whole-degree resolution supported by the protocol. Quick thermostat endpoint. Body fields are optional and can be combined: ```json { "setpoint": 22.5, "power": true, "mode": "house", "enabled": true, "preset": "comfort", "clear_override": false, "clear_device_manual_override": false, "clear_local_thermostat_override": false, "temporary_quick_thermostat": null, "clear_temporary_quick_thermostat": false } ``` Semantics: - `setpoint`: creates a quick custom thermostat target. - `preset`: `auto`, `comfort`, `sleep`, `away`, `custom`; `auto` clears the profile override. - `mode`: `house`, `cool`, `heat`; `house` restores global mode inheritance. - `enabled`: zone automation enable state. - `power:true`: local Quick Thermostat ownership — this zone can run through full thermostat logic regardless of whether group-level control is enabled for its climate group. - `power:false`: turns this zone off and creates a fresh backend-owned local hand-back timer (currently 15 minutes). - `clear_local_thermostat_override:true`: immediately return local Quick Thermostat ownership to normal group/schedule control. - `clear_device_manual_override:true`: explicitly hand a physical/direct manual takeover back to the thermostat. - `clear_override:true`: clear ordinary quick preset/setpoint override. - `temporary_quick_thermostat`: start/replace a persisted temporary session. - `clear_temporary_quick_thermostat:true`: cancel that temporary session only. Returns updated `Zone`. #### Temporary Quick Thermostat request ```json { "start_kind": "now", "start_delay_minutes": null, "start_at": null, "finish_kind": "duration", "duration_minutes": 90, "until": null, "target_temperature": 23.0, "temperature_operator": "within", "tolerance_c": 0.3, "hold_minutes": 60, "max_duration_minutes": 240 } ``` `start_kind`: - `now` - `delay` + `start_delay_minutes` - `at` + ISO-8601 `start_at` `finish_kind`: - `duration` + `duration_minutes` - `until` + ISO-8601 `until` - `temperature_reached` - `temperature_stable` + `hold_minutes` - `schedule_boundary` Temperature operators: - `within` - `at_or_below` - `at_or_above` `max_duration_minutes` is an optional fail-safe for temperature-based sessions. Delayed sessions do not own the zone until their effective start. Runtime session state is persisted and exposed inside the returned zone. Examples: ```bash # Preset until next schedule boundary curl -X POST "$BASE/api/zones/ZONE_ID/control" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"preset":"sleep"}' # Return to schedule curl -X POST "$BASE/api/zones/ZONE_ID/control" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"preset":"auto"}' # Run a 90-minute temporary thermostat curl -X POST "$BASE/api/zones/ZONE_ID/control" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"temporary_quick_thermostat":{"start_kind":"now","finish_kind":"duration","duration_minutes":90,"target_temperature":23}}' ``` ### `POST /api/zones/{id}/schedule-template` Body: ```json { "template": "family" } ``` Built-in templates: | Template | Result | | --- | --- | | `family` | Comfort `06:30–22:30`, Sleep overnight. | | `child` | Comfort `06:30–20:30`, Sleep overnight. | | `bedroom` | Comfort `06:30–22:00`, Sleep overnight. | | `workday` | Weekday morning/away/evening/sleep plus weekend blocks. | | `always` | 24-hour Comfort. | Existing schedules for the zone are replaced after overlap validation. Response: ```json { "zone": {}, "schedules": [] } ``` --- ## Groups ### Group object ```json { "id": "uuid", "name": "Bedrooms", "zone_ids": ["zone-1", "zone-2"], "power_enabled": true, "created_at": "...", "updated_at": "..." } ``` ### `GET /api/groups` Returns groups. ### `POST /api/groups` ```json { "name": "Bedrooms", "zone_ids": ["zone-1", "zone-2"], "power_enabled": true } ``` A group must contain at least one existing zone. Returns `201 Created`. ### `GET /api/groups/{id}` / `PUT /api/groups/{id}` / `DELETE /api/groups/{id}` Read, replace or delete a group. `DELETE` returns `204`. ### `POST /api/groups/{id}/control` ```json { "power": true, "mode": "house", "preset": "custom", "setpoint": 22.3 } ``` All fields optional: - `power`: enable/disable group-level control (legacy field name; this is not a device power gate), - `mode`: `house`, `cool`, `heat`, - `preset`: `auto`, `comfort`, `sleep`, `away`, `custom`, - `setpoint`: custom group target in the `8–30°C` range; requires `preset: "custom"`. A custom setpoint creates the same temporary manual override for every member zone and normally expires at that zone's next schedule boundary. Returns a group control/result object including updated members/state. Group `power` is a scoped bulk-power/control action. `power=false` immediately powers member units off and releases `group:*` ownership; members are stored as individually-off thermostats rather than being blocked by membership in an OFF group. This group-created OFF is indefinite (no 15-minute local hand-back), so the regulator cannot restart the unit by itself; a user can still turn an individual thermostat back on independently. `power=true` clears that scoped thermostat-OFF state and immediately re-runs group thermostat arbitration. Explicit Web/Home Assistant group power actions take over older member manual/temporary ownership; scheduled group automations still respect higher-priority manual/local ownership. Group mode/preset/setpoint changes are accepted only while the group is ON. Whole-house power remains independent and authoritative. --- ## Whole-house control House thermostat mode and master power are intentionally separate. ### `POST /api/house/control` ```json { "mode": "cool" } ``` Valid modes: `cool`, `heat`, `off`. - `cool`/`heat` are explicit whole-house activation requests: master power is enabled and the thermostat arbiter starts eligible managed zones. Existing per-group control enable/disable settings are preserved. - `off` means **do not perform house-level thermostat control** for inherited zones. It does not itself change master power and does not forcibly power direct/manual devices down. Returns public runtime settings. ### `POST /api/house/power` ```json { "power": false } ``` `false` is authoritative: it clears local/manual ownership markers as required and powers every enabled physical device down. It does **not** modify per-group control enable/disable settings. `true` resumes thermostat arbitration rather than blindly sending a bare ON frame. Response includes: ```json { "power": false, "devices": [], "groups": [], "settings": {}, "failed": [] } ``` ### `POST /api/house/preset` ```json { "preset": "sleep" } ``` Valid: `auto`, `comfort`, `sleep`, `away`. A non-`auto` preset creates zone overrides that normally expire at each zone's next schedule boundary. `auto` clears them. Selecting a house preset explicitly re-enables master power while preserving each group's control enable/disable setting. --- ## Schedules Schedule object/request: ```json { "zone_id": "zone-1", "name": "Night", "enabled": true, "weekdays": [1, 2, 3, 4, 5, 6, 7], "start_time": "22:30", "end_time": "06:30", "preset": "sleep", "setpoint": 24.5 } ``` Rules: - weekdays use ISO numbers `1=Monday ... 7=Sunday`, - times use local `HH:MM`, - crossing midnight is supported, - `start_time == end_time` represents a 24-hour window for selected weekdays, - preset: `comfort`, `sleep`, `away`, `custom`, - custom setpoint: `8..30` °C, - enabled schedules for the same zone cannot overlap. For non-custom presets, the effective target comes from the zone's seasonal profile; `setpoint` is retained for compatibility. Routes: ```text GET /api/schedules POST /api/schedules GET /api/schedules/{id} PUT /api/schedules/{id} DELETE /api/schedules/{id} ``` Create returns `201`; delete returns `204`. --- ## Automations Automation request/object fields: ```json { "name": "Hot room", "enabled": true, "trigger_kind": "temperature_above", "trigger_device_id": "gree-aabbccddeeff", "threshold": 27.0, "at_time": null, "action_device_id": "gree-aabbccddeeff", "action_group_id": null, "action_preset": null, "action": { "power": true, "mode": "cool", "target_temperature": 23 }, "cooldown_seconds": 300 } ``` Triggers: - `temperature_above`: requires `trigger_device_id` + `threshold`, - `temperature_below`: requires `trigger_device_id` + `threshold`, - `time`: requires local `at_time` in `HH:MM`. Action target is either: - direct device: `action_device_id` + full `DeviceCommand`, or - group: `action_group_id`; group automation supports only power, `house`/`cool`/`heat` mode and optional `action_preset` (`auto|comfort|sleep|away`). The response also contains runtime `last_fired_at`, `created_at`, `updated_at`. Routes: ```text GET /api/automations POST /api/automations GET /api/automations/{id} PUT /api/automations/{id} DELETE /api/automations/{id} ``` Create returns `201`; delete returns `204`. --- ## History and readings ### `GET /api/readings` Legacy/lightweight device history. Query parameters: | Parameter | Default | Description | | --- | --- | --- | | `device_id` | all | Optional device filter. | | `hours` | `24` | Clamped to `1..87600` (10 years). | | `limit` | `1500` | Row limit. | Response: ```json { "readings": [ { "id": 1, "device_id": "gree-aabbccddeeff", "timestamp": "...", "indoor_temperature": 23.4, "outdoor_temperature": 30.1, "target_temperature": 23, "power": true, "source": "poll" } ] } ``` ### `GET /api/history` Rich chart/history API. Query parameters: | Parameter | Description | | --- | --- | | `scope` | `overview`, `zones`/`zone`, `devices`, `sensors`; default `zones`. | | `zone_id` | Zone filter for zone scope. | | `device_id` | Device filter for device scope. | | `entity_id` | Home Assistant entity filter for sensor scope. | | `hours` | Default `24`, clamped to 10 years. | | `limit` | Default `12000`, clamped to `1..20000`. | Bucket resolution: | Range | Bucket | | --- | --- | | ≤ 6 h | 30 s | | ≤ 24 h | 2 min | | ≤ 7 d | 10 min | | ≤ 30 d | 30 min | | ≤ 90 d | 2 h | | ≤ 1 y | 6 h | | > 1 y | 24 h | Zone reading fields: ```text id, zone_id, device_id, timestamp, gree_temperature, external_temperature, control_temperature, target_temperature, device_setpoint, outdoor_temperature, power, mode, fan_speed, demand, control_source, active_preset ``` Device reading fields are the `Reading` fields documented above. HA sensor rows contain: ```text id, entity_id, zone_id, kind, timestamp, temperature ``` `scope=overview` returns all three families plus counts and per-family storage source. When InfluxDB is enabled, older history can be read from Influx and merged with recent SQLite rows. A failed Influx query falls back to available SQLite data and reports `storage_warning` rather than failing the entire chart response. --- ## Control plan ### `GET /api/control-plan` Returns the resolved machine-readable thermostat plan: Top-level fields: ```text generated_at house_mode house_preset house_power outdoor_temperature control_strategy night_mode_active night_mode_start night_mode_end night_mode_max_fan_speed next_events[] zones[] rules[] ``` Each zone plan includes: ```text zone_id, zone_name, device_id, device_name, enabled, effective_enabled, mode, configured_mode, inherit_house_mode, preset, preset_override, current_temperature, target_temperature, device_setpoint, desired_power, desired_mode, actual_power, actual_mode, actual_setpoint, demand, control_source, manual_override_until, local_thermostat_power, local_thermostat_resume_at, device_manual_override, device_manual_override_until, control_owner, control_command_source, control_since, resume_at, control_reason, blocked_reason, lockout_until, current_schedule_id, current_schedule_name, next_events[] ``` This endpoint is the best way for another client to understand **desired vs actual state**, who owns control, and why a zone is blocked/paused. --- ## Events and retention ### `GET /api/events?limit=100` Response: ```json { "events": [ { "id": 1, "timestamp": "...", "level": "info", "kind": "device.updated", "message": "...", "metadata": {} } ] } ``` ### `GET /api/events/retention` ```json { "days": 30 } ``` ### `PUT /api/events/retention` ```json { "days": 30 } ``` Value is clamped to `1..3650`; pruning happens immediately. Response includes `days` and number of removed rows. --- ## Runtime settings ### `GET /api/settings` Returns a public-safe settings document. Secrets are blanked and accompanied by `*_configured` booleans where relevant. Shape: ```json { "controller_id": "gree-controller", "simulator_enabled": false, "poll_interval_seconds": 15, "zone_interval_seconds": 5, "discovery_timeout_ms": 3000, "discovery_broadcast": "255.255.255.255:7000", "house_mode": "cool", "house_power_enabled": true, "control_strategy": "setpoint", "outdoor_assist_enabled": true, "history_retention_days": 30, "history_compaction_enabled": true, "event_log_retention_days": 30, "suppress_device_beep": false, "debug": { "overlay_enabled": false, "gree_frames": false }, "night_mode": { "enabled": false, "start_time": "22:00", "end_time": "06:00", "max_fan_speed": 1, "force_quiet": true, "use_native_sleep": true }, "notifications": {}, "influxdb": {}, "home_assistant": {} } ``` #### Home Assistant settings ```json { "url": "http://homeassistant.local:8123", "token": "", "token_configured": true, "default_entity_id": "sensor.room_temperature", "outdoor_entity_id": "sensor.outdoor_temperature", "sensor_stale_after_seconds": 300, "allow_invalid_tls": false, "sensor_aliases": { "sensor.room_temperature": "Living room" } } ``` #### InfluxDB settings ```json { "enabled": true, "version": "2", "url": "http://influxdb:8086", "database": "gree_controller", "username": "", "password": "", "password_configured": false, "org": "home", "bucket": "gree_controller", "token": "", "token_configured": true, "history_threshold_days": 30 } ``` Version `1` uses database/optional username/password. Version `2` uses org/bucket/token. #### Notification settings ```json { "enabled": true, "mode": "problems", "provider": "pushover", "pushover_app_token": "", "pushover_user_key": "", "pushover_configured": true, "slack_webhook_url": "", "slack_configured": false, "discord_webhook_url": "", "discord_configured": false, "cooldown_seconds": 300, "communication_failure_threshold": 3, "target_timeout_minutes": 60, "alert_types": { "stale_sensor": true, "sensor_errors": true, "communication": true, "target_timeout": true, "automation": true, "control_errors": true, "important_events": true, "other": true } } ``` Modes: `problems`, `important`. Providers: `pushover`, `slack`, `discord`. ### `PUT /api/settings` Accepts the complete `RuntimeSettings` document. Important behavior: - `house_mode` and `house_power_enabled` cannot be changed here; use House Control API. - polling interval is clamped `2..3600` seconds. - zone interval is clamped `2..3600` seconds. - discovery timeout is clamped `300..30000` ms. - `control_strategy` is normalized to `setpoint`. - discovery broadcast must be `auto`, `auto:*` or a valid socket address. - blank HA token preserves the saved token. - blank Influx token/password preserve saved secrets. - blank Pushover/Slack/Discord secret fields preserve saved secrets. - HA sensor age is clamped `30..86400` seconds. - notification cooldown: `30..86400` seconds. - communication failure threshold: `2..100`. - target timeout: `5..1440` minutes. - retention windows: `1..3650` days. - night mode times must be `HH:MM`; max fan is clamped `1..5`. Returns the safe public settings form. ### `GET /api/settings/export` Returns configuration format version `1`: ```text format_version exported_at settings devices[] zones[] groups[] schedules[] automations[] ``` Export includes GREE binding keys and integration credentials. It excludes metric history, event rows and generated API-token records. Treat the export as a secret. ### `POST /api/settings/import` Accepts exactly the export document. The backend validates IDs/references/schedules/settings, safely stops devices whose ownership is being removed, clears transient ownership/timers/stale live state, replaces configuration, re-polls devices, then re-enables thermostat control. Metric/event history and generated access-token records are preserved. Response: ```json { "ok": true } ``` --- ## Debug API ### `GET /api/debug` ```json { "overlay_enabled": true, "gree_frames": true } ``` ### `PUT /api/debug` Accepts the same object, persists it and broadcasts `debug.settings`. When overlay diagnostics are enabled, live HTTP requests generate `api.request` WebSocket events containing method, path, status and duration. When `gree_frames=true`, sanitized GREE protocol events are also sent as `gree.frame`. The Web UI can display **All**, **Requests** or **GREE** subsets. --- ## Integration tests ### `POST /api/integrations/home-assistant/test` ```json { "entity_id": "sensor.room_temperature" } ``` `entity_id` is optional; controller defaults/aliases are resolved. Response: ```json { "ok": true, "temperature_c": 23.4, "entity_id": "sensor.room_temperature" } ``` ### `POST /api/integrations/notifications/test` Accepts a `NotificationSettings` object. Blank secret/webhook fields reuse saved secrets for the test. Response: ```json { "ok": true } ``` --- ## Access tokens ### `GET /api/access-tokens` Returns: ```json [ { "id": "uuid", "name": "Home Assistant", "token_prefix": "gree_controller_abc...", "created_at": "..." } ] ``` ### `POST /api/access-tokens` ```json { "name": "Home Assistant" } ``` Name length: `1..80`. If omitted, default is `Home Assistant`. Response `201 Created`: ```json { "token": "gree_controller_FULL_SECRET_SHOWN_ONCE", "item": { "id": "uuid", "name": "Home Assistant", "token_prefix": "gree_controller_...", "created_at": "..." } } ``` ### `DELETE /api/access-tokens/{id}` Revokes token and returns `204`. --- ## Restricted Home Assistant API These routes always require a generated token or the administrator app token. ### `GET /api/integrations/home-assistant/devices` Returns `Device[]`. ### `POST /api/integrations/home-assistant/devices/{id}/command` Accepts `DeviceCommand`. A direct command is rejected if the device belongs to a disabled thermostat zone; re-enable the zone for normal HA/controller ownership or use the administrator technical device endpoint deliberately. ### `GET /api/integrations/home-assistant/control-plan` Same payload as administrator `GET /api/control-plan`. ### `GET /api/integrations/home-assistant/groups` Returns an HA-oriented derived group list. Each object includes: ```text id, name, zone_ids, zone_names, power_enabled, effective_power, mode, preset, house_mode, zone_count, enabled_zones, active_zones, demanding_zones, device_count, online_devices, current_temperature, members[], next_events[] ``` Member rows include zone/device identity, configured/effective enable state, mode/preset, room/target temperature, demand, source, schedule and current manual/local ownership markers. ### `POST /api/integrations/home-assistant/groups/{id}/control` Same body/semantics as normal group control. ### `POST /api/integrations/home-assistant/house/control` Same `{ "mode": "cool|heat|off" }` semantics as administrator house mode. ### `POST /api/integrations/home-assistant/house/preset` Same `{ "preset": "auto|comfort|sleep|away" }` semantics. ### `POST /api/integrations/home-assistant/house/power` Same `{ "power": true|false }` semantics. ### `POST /api/integrations/home-assistant/zones/{id}/control` Same `ZoneControlPatch` thermostat semantics as the normal zone control endpoint. The internal source is recorded as Home Assistant thermostat control. --- ## WebSocket ### Connection Without administrator authentication: ```text ws://HOST:8787/ws ``` When `GREE_CONTROLLER_APP_TOKEN` is configured: ```text ws://HOST:8787/ws?token=APP_TOKEN ``` Generated restricted HA tokens are not WebSocket administrator tokens. ### Message envelope Every server event uses: ```json { "event": "device.updated", "timestamp": "2026-08-30T06:54:00Z", "data": {} } ``` The first frame is always `bootstrap` with the same payload as `GET /api/bootstrap`, unless bootstrap generation itself fails. Common live events include: ```text bootstrap device.created device.updated device.deleted devices.discovered zone.created zone.updated zone.deleted group.created group.updated group.deleted schedule.created schedule.updated schedule.deleted schedule.template_applied automation.created automation.updated automation.deleted settings.updated configuration.imported debug.settings api.request gree.frame log.created ``` Additional engine/integration events may be introduced without changing the envelope. `api.request` data: ```json { "method": "GET", "path": "/api/system/info", "status": 200, "duration_ms": 2 } ``` GREE debug events are emitted only when enabled and are intended for diagnostics, not as a stable protocol API. --- ## Localization endpoints Language files are public so the UI can localize before administrator authentication. ### `GET /lang/index.json` Returns the generated catalog of embedded packs. ### `GET /lang/{code}.json` Returns one embedded language pack, e.g.: ```text GET /lang/en.json GET /lang/pl.json ``` --- ## Practical API examples Assume: ```bash BASE='http://127.0.0.1:8787' AUTH='Authorization: Bearer APP_TOKEN' ``` Discover devices: ```bash curl -X POST "$BASE/api/discovery" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"protocol_version":0,"passes":3}' ``` Directly poll a device: ```bash curl -X POST "$BASE/api/devices/DEVICE_ID/poll" -H "$AUTH" ``` Set a zone target: ```bash curl -X POST "$BASE/api/zones/ZONE_ID/control" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"setpoint":22.5}' ``` Return zone to automatic scheduling: ```bash curl -X POST "$BASE/api/zones/ZONE_ID/control" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"preset":"auto","clear_local_thermostat_override":true}' ``` Turn the whole managed house off: ```bash curl -X POST "$BASE/api/house/power" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"power":false}' ``` Read current ownership/desired-vs-actual state: ```bash curl "$BASE/api/control-plan" -H "$AUTH" ``` Read 90 days of zone history: ```bash curl "$BASE/api/history?scope=zones&zone_id=ZONE_ID&hours=2160" -H "$AUTH" ``` Create a restricted Home Assistant token: ```bash curl -X POST "$BASE/api/access-tokens" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"name":"Home Assistant"}' ``` Use that token: ```bash curl -H 'Authorization: Bearer gree_controller_RESTRICTED_TOKEN' \ "$BASE/api/integrations/home-assistant/control-plan" ``` ## Compressor protection queue Runtime settings expose `compressor_protection_enabled` and `compressor_protection_seconds` (30–1800; default 180). While enabled, thermostat starts and Heat/Cool reversals that fall inside the protection window are represented on the owning zone by `compressor_pending_action`, `compressor_pending_since`, and `compressor_pending_until`. - `POST /api/zones/:id/compressor-queue/cancel` cancels the currently pending compressor-protection task for one thermostat. - `POST /api/compressor-queue/cancel-all` cancels all currently pending compressor-protection tasks. Cancellation suppresses the same pending intent until a new explicit thermostat/group/house command re-arms it, a scheduled Temporary Quick Thermostat session takes ownership, or a different mode/target creates a new intent. Safety OFF commands are not delayed by compressor protection. Direct technical device commands remain immediate manual-control operations.