# 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 **More -> Home Assistant / Sensors**. 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 GET /api/integrations/home-assistant/control-plan GET /api/integrations/home-assistant/groups POST /api/integrations/home-assistant/groups/{id}/control POST /api/integrations/home-assistant/house/control POST /api/integrations/home-assistant/house/preset POST /api/integrations/home-assistant/house/power POST /api/integrations/home-assistant/zones/{id}/control ``` These endpoints always require `Authorization: Bearer ` (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":6000,"broadcast":"255.255.255.255:7000","protocol_version":0,"passes":3}' ``` `protocol_version` is `0` for auto/both, `1` for AES-ECB only, and `2` for AES-GCM only. `passes` is `1..10`. Auto is recommended when different GREE Wi-Fi module generations share the network. ## Device command ```json { "power": true, "mode": "cool", "target_temperature": 22.0, "fan_speed": 3, "swing_vertical": true, "quiet": false, "turbo": false, "light": true, "xfan": false, "air": false, "health": false, "sleep": false } ``` Supported modes: `auto`, `cool`, `dry`, `fan`, `heat`. Fan speed: `0..5`. Optional properties map to GREE `Blo` (X-FAN), `Air`, `Health` and `SwhSlp` (Sleep) and should only be used when the device reports the matching `supports_*` capability. Physical GREE Celsius setpoints are normalized to whole degrees in the `8..30°C` range. ## Smart thermostat / house control The controller separates the seasonal house mode from per-zone profiles. Normal operation uses setpoint modulation: units remain powered while the controller moves the device target between an active and satisfied setpoint. House mode `off` means **do not control** inherited zones: it does not power a manually operated unit down. A zone explicitly switched to `cool` or `heat` remains independent and can continue to run. ```bash curl -X POST "$BASE/api/house/control" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"mode":"cool"}' curl -X POST "$BASE/api/house/power" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"power":false}' curl -X POST "$BASE/api/house/preset" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"preset":"sleep"}' ``` House modes: `cool`, `heat`, `off` (`off` = no house-level thermostat control). House presets: `auto`, `comfort`, `sleep`, `away`. A non-`auto` house preset creates per-zone overrides that expire at each zone's next schedule boundary; a zone with no future transition keeps the override until it is cleared. `POST /api/house/power` controls a separate whole-house master power state. With master power off, zones and controller automations cannot restart units; the selected house thermostat mode is preserved. This is independent from house mode `off`, which only means no house-level thermostat control and allows direct/manual device operation. Choosing whole-house `cool`/`heat` or any whole-house preset is treated as an explicit whole-house activation: master power is set back to on, all group power gates are enabled and only devices assigned to enabled thermostat zones that are not in manual-device override are powered on. `power:false` is authoritative: it clears manual-device overrides, disables every group and powers off every enabled physical device. Disabled zones otherwise remain outside house/group/automation control and their underlying unit can be operated manually through technical device control or its physical remote. Choosing house mode `off` does not change master power. A zone stores separate profile temperatures for both seasons: ```json { "name": "Paweł", "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, "min_adjust_seconds": 120, "standby_offset_c": 2.0, "smart_fan": true, "sensor_source": "combined", "ha_entity_id": "sensor.pawel_temperature", "external_sensor_weight": 0.4, "max_sensor_difference": 3.0 } ``` Quick control does not directly fight the schedule engine: ```bash # Sleep until the next schedule boundary (or until cleared if there is no future transition) curl -X POST "$BASE/api/zones/ZONE_ID/control" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"preset":"sleep"}' # Return to automatic schedule curl -X POST "$BASE/api/zones/ZONE_ID/control" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"preset":"auto"}' # Temporary custom room target curl -X POST "$BASE/api/zones/ZONE_ID/control" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"setpoint":22.5}' # Manual power for only this zone's physical unit. This behaves like a remote and # does not enable the containing climate group. curl -X POST "$BASE/api/zones/ZONE_ID/manual-power" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"power":true}' ``` `mode` on the quick zone endpoint accepts `house`, `cool`, or `heat`. `house` means **Follow global mode** in the UI and should be left unchanged when the zone is meant to inherit the global GREE Controller mode. A fixed `cool`/`heat` is an explicit per-zone override; therefore house mode `off` stops inherited zones but does not disable a zone deliberately fixed to Cooling or Heating. Whole-house master power remains authoritative over all zones. ### Physical/manual device takeover The poller compares climate-relevant unit state with the last controller-known state. An external change of power, HVAC mode, target temperature or fan speed (for example from the IR remote) sets `device_manual_override=true` on the assigned zone. The zone continues sensor/history updates but thermostat modulation, schedules, groups and direct-device automations stop issuing corrective climate commands. The controller stores the pre-takeover climate state; if the user later returns the unit to that operational state, the takeover is cleared automatically and the stale **Resume automation** prompt disappears. When the pre-takeover state was OFF, switching the unit OFF again is sufficient even if the remote kept a different dormant target/mode internally. Otherwise the override expires at the next schedule transition when one exists; without a future transition it stays active until explicitly resumed. The known GREE standby normalization from Low fan back to Auto is ignored so it does not create a false takeover. The technical `POST /api/devices/{id}/command` path uses the same takeover semantics for climate-relevant changes; light-only and unrelated feature changes do not suspend the thermostat. An explicit quick-zone action (target, mode, preset, enabled state) resumes thermostat ownership automatically. It can also be resumed directly: ```bash curl -X POST "$BASE/api/zones/ZONE_ID/control" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"clear_device_manual_override":true}' ``` Whole-house `power:false` is the deliberate exception and always clears the takeover and powers the unit off. Group power/mode/profile commands do not fight a zone while its manual-device override is active. ### Climate groups Groups persist a set of thermostat zone IDs and an independent group power gate. Group control reuses the normal zone engine, so profiles, schedules, hysteresis and sensor selection remain unchanged. ```bash curl -X POST "$BASE/api/groups" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"name":"Upstairs","zone_ids":["ZONE_A","ZONE_B"]}' curl -X POST "$BASE/api/groups/GROUP_ID/control" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"mode":"cool","preset":"sleep","power":true}' ``` Group `mode` accepts `house`/`auto` (follow whole-house Heating/Cooling), `cool`, or `heat`. Group `preset` accepts `auto`, `comfort`, `sleep`, or `away`. Automations may set `action_group_id` and optional `action_preset`; direct-device `action_device_id` remains backward compatible. ### Ready-made schedule templates Templates generate normal schedule records and replace the current schedules for the selected zone. Every generated entry remains editable. ```bash curl -X POST "$BASE/api/zones/ZONE_ID/schedule-template" -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"template":"child"}' ``` Templates: `family`, `child`, `bedroom`, `workday`, `always`. Schedules use a profile (`comfort`, `sleep`, `away`) or a custom temperature: ```json { "zone_id": "UUID", "name": "Sleep", "enabled": true, "weekdays": [1,2,3,4,5,6,7], "start_time": "20:30", "end_time": "06:30", "preset": "sleep", "setpoint": 24.0 } ``` For non-`custom` schedules, `setpoint` is only a compatibility value; the actual target comes from the zone's seasonal profile. `start_time == end_time` means a 24-hour block for each selected weekday. Enabled schedules belonging to the same zone are rejected when their active windows overlap, including overnight overlaps. ### Home Assistant outdoor assist and local HTTPS Runtime settings can specify `home_assistant.outdoor_entity_id` and `outdoor_assist_enabled`. Outdoor temperature never replaces the zone room temperature. It is only used to make active setpoints/fan speed slightly more assertive in extreme weather. For trusted local Home Assistant servers with self-signed/invalid HTTPS certificates, set: ```json { "home_assistant": { "url": "https://10.87.65.2", "allow_invalid_tls": true } } ``` This setting is opt-in and applies only to the controller's outbound Home Assistant sensor client. ## Current control plan When cooling is driven by a valid per-zone Home Assistant room sensor (`external` or `combined` control source), active demand forces the next lower whole-degree unit target (typically 0.5-1.0 C below the room target). It is not stacked on top of a larger outdoor-assist correction, and it is disabled automatically whenever control falls back to the GREE sensor. This helps the independent room sensor reach the requested temperature without increasing overcooling risk during sensor failures. On process startup the thermostat/schedule/automation engine stays command-passive until one full live poll of all enabled GREE units completes. Persisted device/zone/group state is still available immediately for the UI, but it is not used to emit climate commands before the physical state has been synchronized. `GET /api/control-plan` returns a machine-readable view of what the controller is doing now and what is expected next. It includes the house mode, uniform house preset (or `null` for mixed zone profiles), whole-house master-power state, strategy, each zone's effective `mode`, `configured_mode`, `inherit_house_mode`, preset/current and target temperatures, current schedule, manual schedule override expiry, physical/manual device override state and expiry, upcoming schedule transitions, enabled automation rules and predictable time-triggered automation events. ```bash curl "$BASE/api/control-plan" -H "$AUTH" ``` The restricted Home Assistant equivalent is `GET /api/integrations/home-assistant/control-plan`. HA may also read derived group state from `GET /api/integrations/home-assistant/groups`, control a group through `POST /api/integrations/home-assistant/groups/{id}/control`, change house mode/preset/master power through `/api/integrations/home-assistant/house/*`, and change a zone target/preset/mode/enabled state through: Direct HA device commands are rejected while the device belongs to a disabled thermostat zone. Re-enable the zone for normal controller/HA ownership; the unrestricted `/api/devices/{id}/command` endpoint remains the technical/manual path used by the Devices view. Climate-relevant direct device commands on an enabled zone create the same manual-device override as a physical remote change, preventing the thermostat engine from immediately undoing them. The group list reports the configured group power state, common mode/profile (or `mixed`), member zones, online device count, average room temperature, demand count and upcoming member schedule events. Group control accepts the same semantics as the Web UI: ```bash curl -X POST "$BASE/api/integrations/home-assistant/groups/GROUP_ID/control" \ -H 'Authorization: Bearer HA_TOKEN' -H 'Content-Type: application/json' \ -d '{"mode":"house","preset":"comfort"}' ``` Valid group modes are `house` (inherit global), `cool` and `heat`; valid profiles are `auto`, `comfort`, `sleep` and `away`. `power` can be sent independently as a boolean. ```bash curl -X POST "$BASE/api/integrations/home-assistant/zones/ZONE_ID/control" \ -H 'Authorization: Bearer HA_TOKEN' -H 'Content-Type: application/json' \ -d '{"setpoint":22.5}' ``` ## Settings backup and debug `GET /api/settings/export` downloads configuration JSON (settings, devices, zones, groups, schedules and automations). It intentionally excludes metric history, event logs and generated API-token records. The export includes GREE device binding keys plus configured Home Assistant and InfluxDB credentials, so treat it as a secret. `POST /api/settings/import` accepts that JSON format and replaces application configuration while preserving metric history, events and generated API tokens. Debug overlay state can be read or changed independently of the full settings document: ```text GET /api/debug PUT /api/debug ``` Example body: `{"overlay_enabled":true,"gree_frames":true}`. With the overlay enabled, live `api.request` WebSocket events contain only HTTP method, path, status and duration. When GREE frame debug is enabled, sanitized `gree.frame` events are also emitted; the bound encryption key is not exposed. ## 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`, `debug.settings`, `api.request`, `gree.frame` 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/.json` returns the corresponding language pack. Add a valid `lang/.json` file and rebuild to expose a new language. ### `GET /api/history` The history API exposes three independent data families and a combined overview. `hours` may cover up to 10 years and `limit` is capped at 20,000 rows. Query buckets become progressively wider: 30 s (<=6 h), 2 min (<=24 h), 10 min (<=7 d), 30 min (<=30 d), 2 h (<=90 d), 6 h (<=1 y), then 24 h. ```text GET /api/history?scope=overview&hours=24 GET /api/history?scope=devices&device_id=DEVICE_ID&hours=2160 GET /api/history?scope=zones&zone_id=ZONE_ID&hours=8760 GET /api/history?scope=sensors&entity_id=sensor.room_temperature&hours=24 ``` Local SQLite is the hot store. When compaction is enabled, full-resolution samples are kept for 24 h, then one sample per 10-minute bucket through day 7 and one per 30-minute bucket afterwards. `history_retention_days` controls final local pruning. When InfluxDB is enabled, samples are written in parallel. Maintenance also backfills compacted legacy SQLite samples older than `influxdb.history_threshold_days`; those local rows are deleted only after the archive batch is accepted. Requests that cross the threshold read the older portion from InfluxDB 1.x or 2.x and merge it with recent SQLite data. If the Influx query fails, the endpoint falls back to the available SQLite history and returns `storage_warning`. `scope=overview` returns `zones`, `devices`, `sensors`, row `counts`, `bucket_seconds`, plus `storage`/optional `storage_warning`. Device rows contain indoor/outdoor/target/power; zone rows contain GREE/external/control/target/device-setpoint/outdoor/power/mode/fan/demand/source/preset; HA rows contain entity, optional zone, kind and temperature. ## InfluxDB long-term storage Runtime settings support either InfluxDB 1.x (`version=1`, URL, database and optional username/password) or InfluxDB 2.x (`version=2`, URL, org, bucket and token). The same values can be supplied with `GREE_CONTROLLER_INFLUX_*` environment variables; see `.env.example`. An explicitly configured environment value overrides the persisted setting at startup. ## Event log retention `GET /api/events/retention` returns the current event/debug log retention window. `PUT /api/events/retention` accepts `{"days":30}` (1-3650), persists it and immediately prunes older event rows. The same value is part of runtime settings as `event_log_retention_days` and can be set at startup with `GREE_CONTROLLER_EVENT_LOG_RETENTION_DAYS`. ## Night mode and sensor aliases Runtime settings include `night_mode` with `enabled`, `start_time`, `end_time`, `max_fan_speed` (1-5), `force_quiet` and `use_native_sleep`. Overnight windows crossing midnight are supported. During the window thermostat-generated fan commands are limited to the configured maximum; Auto is converted to a bounded low speed, while `Quiet` and native `Sleep` are requested only when the unit supports them. `home_assistant.sensor_aliases` is a map of Home Assistant `entity_id` to a friendly UI name, for example `{"sensor.gabinet_temperature":"Gabinet"}`. Aliases affect labels in the controller UI/history only. The backend canonicalizes aliases back to their original keys before Home Assistant requests and metric storage; an alias accidentally saved in a sensor field is also resolved defensively.