20 KiB
API examples
TOKEN is optional when GREE_CONTROLLER_APP_TOKEN is empty.
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:
GET /api/access-tokens
POST /api/access-tokens
DELETE /api/access-tokens/{id}
The Home Assistant custom integration uses a restricted API surface:
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 <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
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
{
"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.
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 and local quick-thermostat power ownership, 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:
{
"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:
# 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}'
# Start this zone as a local thermostat. This does not enable its climate group,
# but the full thermostat logic (sensor/hysteresis/profile/GREE modulation) remains active.
curl -X POST "$BASE/api/zones/ZONE_ID/control" -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"power":true}'
# Return local thermostat power ownership to the group/schedule.
curl -X POST "$BASE/api/zones/ZONE_ID/control" -H "$AUTH" -H 'Content-Type: application/json' \
-d '{"clear_local_thermostat_override":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.
power:true/false on /api/zones/{id}/control is a local quick-thermostat power override, not a physical/manual command. true lets that zone run through the full thermostat even when its climate group is off; false keeps only that zone locally off. clear_local_thermostat_override:true returns power ownership to the normal group/schedule path. Direct/pilot-style control remains /api/devices/{id}/command.
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. Controller-originated thermostat/group commands are correlated with a short-lived expected-state record when GREE status has not settled yet, so their delayed status transition is not mistaken for an external takeover. 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. Local quick-thermostat power is deliberately separate from this state. An explicit quick-zone action (local power, target, mode, preset or enabled state) resumes thermostat ownership from a physical/direct takeover automatically. It can also be resumed directly:
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 both physical/manual takeover and local quick-thermostat ownership before powering units off. Group power commands do not alter the physical unit while that zone has an explicit local thermostat power override, and group/thermostat commands do not fight a zone while its physical/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.
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.
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:
{
"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:
{
"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 configured enabled state, group-gated effective_enabled state, effective mode, configured_mode, inherit_house_mode, resolved preset, explicit preset_override (null = Auto schedule), current/target temperatures, physical device_setpoint, current schedule, manual schedule override expiry, local quick-thermostat power ownership, physical/manual device override state and expiry, upcoming schedule transitions, enabled automation rules and predictable time-triggered automation events. During physical/manual takeover, target_temperature remains the thermostat profile/schedule target while device_setpoint reports the actual GREE target; automation remains paused until resumed or another explicit zone control is sent.
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:
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.
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:
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.
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.
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.
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.