diff --git a/Cargo.lock b/Cargo.lock index d768eeb..7df0308 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.13.9" +version = "0.13.10" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index 83d4eb2..65162c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gree-controller" -version = "0.13.9" +version = "0.13.10" edition = "2021" authors = ["GREE Controller contributors"] description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support" diff --git a/FILE_MANIFEST.sha256 b/FILE_MANIFEST.sha256 index 79234fd..3152ffe 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -1,16 +1,16 @@ d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -36a2cb85329ae82d097a335a6afd81c1ba7bc450eebdac09c600e12befe36ba2 ./Cargo.lock -5f9afa17638997837880ab244301e3921a6444972154b49797a73bc3103fbcad ./Cargo.toml +4b2973e384f48aef569a66e2da448f6a82f33adbbc8d11dd33e7c20d8552536d ./Cargo.lock +71acebe2a2ebb705089abf834eb7b2494daff9a4c93eae3f61ed6d4b0d21ef99 ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -491aeceb202b240807d0d3cb72cdae622ee9ae8cbfd89439dc4add0bc648144f ./README.md +4cf04e8f22fb4e2551ee5fe2fe96fc50dda615b42a0274ba87aba60a59f021fc ./README.md bb549919b96b0413152dd905aec8640bb53301f07ec7d0b1be48ccbd37936fef ./build.rs -d590a6df7ec31c274a2437075d76305cfafbe56385c800fbf39b18b54875119e ./docs/API.md +abe109ed04cd90336e2e136fc77e9cfeefd6a9818a8c354ece08e96a5edc0730 ./docs/API.md 2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md -3e995a5bb3ec40b26c2676818f90adbd5fa42e760b487d18a16b09a586702484 ./docs/openapi.json -fef9a676b9ce82d0539d44e456237a814169a17a158557d7b803a455985bc020 ./ha-addon/.env.example +e3da335c88fe66baaeaf9b1f33cb69b13f47da7415bda39929d86f51174e9026 ./docs/openapi.json +c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example a18d2460340264d34c267fa704ee51a42e59bcc1783ee4c0dbb2a8ef42fa0f16 ./ha-addon/Dockerfile -9266b7885849007ac7626c0151b401da2fcb8e0a2721a6d196089dee1a67fbf3 ./ha-addon/README.md +0417c88a990c72aecf071fba64d5d9da65dcda4d396cd7565477f7a4532c8c5e ./ha-addon/README.md cd7fd4b937a1d6138699f7effd184e3c077912b881e9c73c37b90bbafba68477 ./ha-addon/build.sh fd041032f353ceb0a2b02005f5a78d74a460b12e4e78de13dcb60129859f8c4e ./ha-addon/home-assistant/README.md f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./ha-addon/home-assistant/custom_components/gree_controller/__init__.py @@ -21,7 +21,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom 41a8958a6fe6d10e5679c95ac34832583a2a15ecfd67d09b4e094a83d06f5c47 ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py 5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png -500f87cbfac7c933a5c22f974bf15ec8ee25ed1136eac7eeafe6a2918f8cd503 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json +8fff53290ad57c49d124777083bbae9f9804e3af398c9d51d44ee29987e2d033 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py 39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py @@ -29,10 +29,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom 6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json 13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json -fc6cad1fe53f1ad365cdf0a089ce4156460e6b07ba404b65b78aafd1a0a8ba01 ./ha-addon/repository/gree-controller/CHANGELOG.md +7421c100b66152f3a513abd436afbae8ced4a91968ac89159431d7237f50d2c5 ./ha-addon/repository/gree-controller/CHANGELOG.md 3c06bd11671452ea6472afd5543fb91a39aba73a1d2d95c7762c04f94ab64030 ./ha-addon/repository/gree-controller/DOCS.md c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md -fdfb371926690e26a3693acd32d899e222ea66e4cb3476f717f55382e303e0d0 ./ha-addon/repository/gree-controller/config.yaml +3cfb0dfc0c41d91d533a889796645c3818eb8e74a8be53610da79c88c0e88efa ./ha-addon/repository/gree-controller/config.yaml 3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png 0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png d13e4c2b993d209078342c0c86d852b300692089b377aa044f811308dd188a1e ./ha-addon/repository/gree-controller/translations/en.yaml @@ -84,7 +84,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json 01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md -b48677a5d38684a524c6a1b0275bb6640fa1fb87e05ba29e92470f2d0aa89957 ./scripts/api_dev_test.py +9ca0b7780699d542d6fc6fa567dc5c3f847a57dcb676b02bb83b905862a4ff73 ./scripts/api_dev_test.py 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh 1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh @@ -103,7 +103,7 @@ b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/upda 9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs 368cd843dcac970f1aa6800ba132a4bbc3429e8379ed7abf9878dcb0c633af40 ./src/api/configuration.rs 19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs -a58fb7dcf3f37e3f9d90363623ec91edb667bca97f5faa6405c8b8f539c3d998 ./src/api/devices.rs +e219dafc951207cb70bc7808054a0553a03fb0a948d11af2eb5a119d1340e29e ./src/api/devices.rs 4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs 44b74965f3256c2972d6b3cd384a63c68e3b422f414a0902c085bc92e17369bf ./src/api/flows.rs b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs @@ -159,7 +159,7 @@ da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb 920e028d96a14cca9f74a964bb296e68a109cfead314d3164c180201e606d151 ./src/models/automation.rs 60cac63e6cb41d835e35cc654363f6ffd4386a79f562df0e656b69034acfbac2 ./src/models/control_plan.rs 9cd1904a0f822c557f6ad4576cdb5f96d9668d04e79ea9783885c9f39519752e ./src/models/defaults.rs -f935d35b4a409f12774209f3fa2ffe60f579a8bc1722badbb27d4766efd6368e ./src/models/device.rs +fa8ddca35171680dd9afb3bc46236e0a4922a9bbbf3e8e9939991973281534d3 ./src/models/device.rs aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs cf63af00d98410a1452f205ce00b8a6c5f0f3525b994cba3e971d631db288396 ./src/models/history.rs e1621255217ee6128151cc898373dd404123cb6d54017beec6c7e3e5c3030486 ./src/models/integrations.rs @@ -171,13 +171,13 @@ c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/t cca742bb2b500eb9269f69385fcf88053cd08aabf6a98b7669b8a7a2fd67df13 ./src/protocol/crypto.rs 28969da06a454ddd1ebea5fa606e8446a83dfd21e21352767c189013c508cb90 ./src/protocol/gree.rs 7907c33e6396c7e0d61705b735b21c84d6ed784012fb61022b872c6745540f1d ./src/protocol/gree/binding.rs -d4df52f0202eb32290e68dc945c4b0b44f3be8aa90bd57fd116eb20285960616 ./src/protocol/gree/commands.rs +d71771855d5ac90fae4994debf53ed09472b216c17bb2155d80aa2de13c57e18 ./src/protocol/gree/commands.rs 4e419ba3b86ca9d154e8c04445cf005965175c54041093b2daad3666d4d852bd ./src/protocol/gree/core.rs -322bcb70016653f8bf71cc620020aa87d4f3f56b4d800ee9425a95b1bcc67d2e ./src/protocol/gree/discovery.rs +87d8d9686a416413489d34cb4fc2e5ac4c05f7e1e7e5bc362e9351c06c89fa51 ./src/protocol/gree/discovery.rs 0ab094f609edb99715402d8eeaa7e38cbfd2d271036aec787dcbb3be3e005720 ./src/protocol/gree/merge.rs -6c18d870f98525e87ba821d69424e6b19a22b2c06106b7fd657f30912cd2705f ./src/protocol/gree/network.rs -de37931303fc92a9399e6edc17dfabd6870a38cd2ddb27257427c76768a66ec2 ./src/protocol/gree/polling.rs -c868422c02ba457df21ae92acbd00698c243855ddd1d4e95ac2166796eba9a44 ./src/protocol/gree/tests.rs +c09c931fa8d512a8883fb14f3142be694af3c51290e023d25c7c972e156e4bd2 ./src/protocol/gree/network.rs +0a901ff258a74072af3812c5417196da1a9b8daa8a2eff30c96d038558601739 ./src/protocol/gree/polling.rs +50152bcad4a843ae04000b991a2365abb302fc1583ea69f09320e25740f3c38c ./src/protocol/gree/tests.rs 3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs 923bb4b2d342d6c1870474e43f33b0aba16dc88590c561d765b874412e83df1f ./src/protocol/mod.rs 830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs diff --git a/README.md b/README.md index 1d7730a..f587c69 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API. -**Current release: 0.13.9** +**Current release: 0.13.10** + +Release 0.13.10 improves compatibility with GREE units that report optional feature fields as multi-state values instead of strict 0/1 flags. In particular, `Quiet=2` or `Quiet=3` no longer invalidates the whole status response, and the controller remembers the active Quiet encoding reported by the unit while keeping `Quiet=1` as the legacy default. Existing AES-ECB/AES-GCM behavior and API contracts remain unchanged. Release 0.13.9 fixes dashboard thermostat card sizing when local thermostat or manual device control is active. Cards now keep their intrinsic height instead of stretching every thermostat in the same grid row; API contracts are unchanged. diff --git a/docs/API.md b/docs/API.md index cf1103f..5bc8bc6 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,6 @@ # GREE Controller API reference -HTTP and WebSocket API for GREE Controller **0.13.9**. +HTTP and WebSocket API for GREE Controller **0.13.10**. [← Main documentation](../README.md) @@ -232,7 +232,7 @@ Response: { "status": "ok", "name": "gree-controller", - "version": "0.13.9", + "version": "0.13.10", "uptime_seconds": 1234, "control_ready": true, "time": "2026-08-30T06:54:00Z" @@ -295,7 +295,7 @@ Returns the initial Web UI snapshot: "control_plan": {"generated_at": "2026-09-04T08:00:00Z", "zones": [], "rules": []}, "control_plan_revision": 42, "system": { - "version": "0.13.9", + "version": "0.13.10", "uptime_seconds": 1234, "auth_required": false, "control_ready": true, diff --git a/docs/openapi.json b/docs/openapi.json index de07034..8a5ab3b 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2,9 +2,9 @@ "openapi": "3.1.0", "info": { "title": "GREE Controller API", - "version": "0.13.9", + "version": "0.13.10", "summary": "Local HTTP/WebSocket API for GREE HVAC control", - "description": "Self-hosted API used by the GREE Controller Web UI and Home Assistant integration.\n\n### Authentication\nNormal administrator endpoints accept `Authorization: Bearer ` or `x-api-token: ` when `GREE_CONTROLLER_APP_TOKEN` is configured. If the app token is empty, those endpoints intentionally run in trusted-LAN mode without authentication.\n\nThe restricted `/api/integrations/home-assistant/*` control surface always requires either a generated access token or the administrator app token. Generated tokens do **not** grant normal administrator API access.\n\nIn Swagger UI use **Authorize** and enter the token in either the bearer field or `x-api-token` field.\n\n### Control ownership\nThermostat zones, groups, house rules, direct device control, schedules and visual Flows share an ownership/arbitration model. Prefer zone/group/house endpoints for thermostat intent; direct device commands are technical/manual control and can deliberately take ownership.\n\n### 0.12.0 breaking API\nRuntime settings are split into functional `/api/settings/*` resources. The former `/api/settings`, `/api/debug`, `/api/events/retention`, `/api/settings/export` and `/api/settings/import` endpoints were removed without compatibility aliases.\n\n### 0.13.0\n`control-plan` keeps its existing HTTP response shape but is materialized in memory and pushed to WebSocket clients as `control_plan.updated`; HTTP remains the fallback/resync path. A restricted Home Assistant snapshot endpoint combines devices, groups and the control plan.\n\n### 0.13.1\nRuntime WebSocket churn is reduced without changing contracts: no-op zone regulator heartbeats are deduplicated and device heartbeat polls only invalidate the materialized control plan when plan-relevant device state changes.\n\n### 0.13.2\nCompletes live WebSocket synchronization for entity creation, schedules, automations, configuration imports and runtime device/automation state while preserving the existing HTTP API contracts and control-plan fallback behavior.\n\n### 0.13.3\nLogical thermostat targets are normalized to 0.1 C consistently, including Temporary Quick Thermostat and thermostat-managed automation/Flow actions. The bundled Web UI accepts comma or dot decimal input for the temporary thermostat target; physical GREE setpoint rounding is unchanged.\n\n### 0.13.4\nDisabled-zone thermostat controls remain visually active in the bundled Web UI because explicit temporary/manual thermostat and direct manual control remain available while zone automation is disabled. HTTP and WebSocket contracts are unchanged.\n\n### 0.13.5\nThe zone settings dialog now explains directly beside the enabled/disabled option that disabling a zone stops automation but does not block manual thermostat, temporary thermostat or direct manual control. HTTP and WebSocket contracts remain unchanged.\n\n### 0.13.6\nNotification settings add a dedicated sensor_discrepancy alert type for GREE vs Home Assistant room-temperature divergence. Disabling it suppresses only that notification; the GREE safety fallback remains active.\n\n### 0.13.7\nThe bootstrap snapshot now embeds all eight redacted runtime settings views. The bundled Web UI consumes those settings directly for HTTP startup and WebSocket resynchronization, removing the previous settings-request fan-out while preserving the split settings endpoints for direct reads and writes.\n\n### 0.13.8\nThe bundled Web UI suppresses the initial English-to-selected-language flash with a dedicated pre-paint language initializer and reveals the application only after localization is initialized. Language-pack URLs are content-versioned so immutable browser caching stays safe across updates.\n\n### 0.13.9\nThe bundled Web UI keeps dashboard thermostat cards at their intrinsic height when a local thermostat or manual device-control takeover panel is shown, preventing unrelated thermostat cards in the same grid row from stretching. HTTP and WebSocket contracts are unchanged.", + "description": "Self-hosted API used by the GREE Controller Web UI and Home Assistant integration.\n\n### Authentication\nNormal administrator endpoints accept `Authorization: Bearer ` or `x-api-token: ` when `GREE_CONTROLLER_APP_TOKEN` is configured. If the app token is empty, those endpoints intentionally run in trusted-LAN mode without authentication.\n\nThe restricted `/api/integrations/home-assistant/*` control surface always requires either a generated access token or the administrator app token. Generated tokens do **not** grant normal administrator API access.\n\nIn Swagger UI use **Authorize** and enter the token in either the bearer field or `x-api-token` field.\n\n### Control ownership\nThermostat zones, groups, house rules, direct device control, schedules and visual Flows share an ownership/arbitration model. Prefer zone/group/house endpoints for thermostat intent; direct device commands are technical/manual control and can deliberately take ownership.\n\n### 0.12.0 breaking API\nRuntime settings are split into functional `/api/settings/*` resources. The former `/api/settings`, `/api/debug`, `/api/events/retention`, `/api/settings/export` and `/api/settings/import` endpoints were removed without compatibility aliases.\n\n### 0.13.0\n`control-plan` keeps its existing HTTP response shape but is materialized in memory and pushed to WebSocket clients as `control_plan.updated`; HTTP remains the fallback/resync path. A restricted Home Assistant snapshot endpoint combines devices, groups and the control plan.\n\n### 0.13.1\nRuntime WebSocket churn is reduced without changing contracts: no-op zone regulator heartbeats are deduplicated and device heartbeat polls only invalidate the materialized control plan when plan-relevant device state changes.\n\n### 0.13.2\nCompletes live WebSocket synchronization for entity creation, schedules, automations, configuration imports and runtime device/automation state while preserving the existing HTTP API contracts and control-plan fallback behavior.\n\n### 0.13.3\nLogical thermostat targets are normalized to 0.1 C consistently, including Temporary Quick Thermostat and thermostat-managed automation/Flow actions. The bundled Web UI accepts comma or dot decimal input for the temporary thermostat target; physical GREE setpoint rounding is unchanged.\n\n### 0.13.4\nDisabled-zone thermostat controls remain visually active in the bundled Web UI because explicit temporary/manual thermostat and direct manual control remain available while zone automation is disabled. HTTP and WebSocket contracts are unchanged.\n\n### 0.13.5\nThe zone settings dialog now explains directly beside the enabled/disabled option that disabling a zone stops automation but does not block manual thermostat, temporary thermostat or direct manual control. HTTP and WebSocket contracts remain unchanged.\n\n### 0.13.6\nNotification settings add a dedicated sensor_discrepancy alert type for GREE vs Home Assistant room-temperature divergence. Disabling it suppresses only that notification; the GREE safety fallback remains active.\n\n### 0.13.7\nThe bootstrap snapshot now embeds all eight redacted runtime settings views. The bundled Web UI consumes those settings directly for HTTP startup and WebSocket resynchronization, removing the previous settings-request fan-out while preserving the split settings endpoints for direct reads and writes.\n\n### 0.13.8\nThe bundled Web UI suppresses the initial English-to-selected-language flash with a dedicated pre-paint language initializer and reveals the application only after localization is initialized. Language-pack URLs are content-versioned so immutable browser caching stays safe across updates.\n\n### 0.13.9\nThe bundled Web UI keeps dashboard thermostat cards at their intrinsic height when a local thermostat or manual device-control takeover panel is shown, preventing unrelated thermostat cards in the same grid row from stretching. HTTP and WebSocket contracts are unchanged.\n\n### 0.13.10\nGREE status parsing now tolerates multi-state non-zero encodings for optional feature flags. `Quiet` preserves active wire values 1/2/3 reported by the unit and reuses the observed encoding when enabled, while legacy behavior still defaults to `Quiet=1`. Existing API contracts are unchanged.", "license": { "name": "MIT" } @@ -4891,7 +4891,7 @@ }, "version": { "type": "string", - "example": "0.13.9" + "example": "0.13.10" }, "uptime_seconds": { "type": "integer", diff --git a/scripts/api_dev_test.py b/scripts/api_dev_test.py index 21b7010..2262fd9 100644 --- a/scripts/api_dev_test.py +++ b/scripts/api_dev_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Developer smoke/integration tests for GREE Controller API 0.13.9. +"""Developer smoke/integration tests for GREE Controller API 0.13.10. Default mode is read-only and safe to run against a real controller. Use --settings-write to additionally round-trip all split settings resources and @@ -26,7 +26,7 @@ from typing import Any, Callable, Iterable, Optional DEFAULT_BASE_URL = os.environ.get("GREE_API_URL", "http://127.0.0.1:8787") DEFAULT_TOKEN = os.environ.get("GREE_API_TOKEN", "") -DEFAULT_EXPECTED_VERSION = "0.13.9" +DEFAULT_EXPECTED_VERSION = "0.13.10" SETTINGS_PATHS: dict[str, set[str]] = { "/api/settings/application": {"simulator_enabled"}, @@ -553,7 +553,7 @@ def run_suite(args: argparse.Namespace) -> int: print(f"Settings write tests: {'ENABLED' if args.settings_write else 'disabled'}\n") runner.run("public health", lambda: test_health(client, args.expected_version)) - runner.run("OpenAPI 0.13.9 contract", lambda: test_openapi(client, args.expected_version)) + runner.run("OpenAPI 0.13.10 contract", lambda: test_openapi(client, args.expected_version)) runner.run("bootstrap snapshot contract", lambda: test_bootstrap_contract(client)) runner.run("protected API requires auth", lambda: test_protected_auth(client)) @@ -622,7 +622,7 @@ def run_suite(args: argparse.Namespace) -> int: def parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace: parser = argparse.ArgumentParser( - description="Developer smoke/integration tests for GREE Controller API 0.13.9.", + description="Developer smoke/integration tests for GREE Controller API 0.13.10.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="""Examples: python3 scripts/api_dev_test.py diff --git a/src/api/devices.rs b/src/api/devices.rs index 627bdd2..fb8e0c6 100644 --- a/src/api/devices.rs +++ b/src/api/devices.rs @@ -112,6 +112,7 @@ async fn add_device( swing_vertical: false, swing_horizontal: false, quiet: false, + quiet_wire_value: None, turbo: false, light: true, air: false, diff --git a/src/models/device.rs b/src/models/device.rs index 2053c09..0a840e6 100644 --- a/src/models/device.rs +++ b/src/models/device.rs @@ -34,6 +34,14 @@ pub struct Device { pub swing_horizontal: bool, #[serde(default)] pub quiet: bool, + /// Raw non-zero value last reported by the unit for the GREE `Quiet` property. + /// + /// Most of the application intentionally exposes Quiet as a boolean, but the wire + /// protocol is not consistently boolean across GREE firmware. Known units report + /// values 1, 2 or 3 for active Quiet modes. Remembering the observed value lets us + /// preserve the device-specific encoding when a later command enables Quiet again. + #[serde(default)] + pub quiet_wire_value: Option, #[serde(default)] pub turbo: bool, #[serde(default)] @@ -106,6 +114,7 @@ impl Device { swing_vertical: false, swing_horizontal: false, quiet: false, + quiet_wire_value: None, turbo: false, light: true, air: false, diff --git a/src/protocol/gree/commands.rs b/src/protocol/gree/commands.rs index c818f4e..7cc715d 100644 --- a/src/protocol/gree/commands.rs +++ b/src/protocol/gree/commands.rs @@ -20,13 +20,17 @@ impl GreeClient { command: &DeviceCommand, suppress_beep: bool, ) -> Result { + // Keep legacy behaviour (Quiet=1) until a device proves that it uses another + // active encoding. Once polling observes Quiet=2/3, persist and reuse that value. + // This makes newer/multi-state units work without changing working older units. + let quiet_on_value = device.quiet_wire_value.unwrap_or(1).clamp(1, 3); let try_buzzer_suppression = suppress_beep && self .buzzer_unsupported .lock() .map(|items| !items.contains(&device.id)) .unwrap_or(true); - let inner = Self::command_payload(command, try_buzzer_suppression)?; + let inner = Self::command_payload(command, try_buzzer_suppression, quiet_on_value)?; match self .request(device, &inner, key, false, device.protocol_version) .await @@ -35,7 +39,7 @@ impl GreeClient { Err(first_err) if try_buzzer_suppression => { // Some firmwares reject unknown buzzer properties instead of ignoring them. // Retry the exact state change without buzzer fields and remember the fallback. - let fallback = Self::command_payload(command, false)?; + let fallback = Self::command_payload(command, false, quiet_on_value)?; match self .request(device, &fallback, key, false, device.protocol_version) .await @@ -159,7 +163,11 @@ impl GreeClient { } } - fn command_payload(command: &DeviceCommand, suppress_beep: bool) -> Result { + fn command_payload( + command: &DeviceCommand, + suppress_beep: bool, + quiet_on_value: u8, + ) -> Result { let mut opt = Vec::<&str>::new(); let mut values = Vec::::new(); if let Some(v) = command.power { @@ -191,7 +199,11 @@ impl GreeClient { } if let Some(v) = command.quiet { opt.push("Quiet"); - values.push(json!(if v { 1 } else { 0 })); + values.push(json!(if v { + quiet_on_value.clamp(1, 3) + } else { + 0 + })); } if let Some(v) = command.turbo { opt.push("Tur"); diff --git a/src/protocol/gree/discovery.rs b/src/protocol/gree/discovery.rs index 9c8a2cd..4d90182 100644 --- a/src/protocol/gree/discovery.rs +++ b/src/protocol/gree/discovery.rs @@ -199,6 +199,7 @@ impl GreeClient { swing_vertical: false, swing_horizontal: false, quiet: false, + quiet_wire_value: None, turbo: false, light: true, air: false, diff --git a/src/protocol/gree/network.rs b/src/protocol/gree/network.rs index f68e74e..0b52f0f 100644 --- a/src/protocol/gree/network.rs +++ b/src/protocol/gree/network.rs @@ -108,6 +108,19 @@ fn status_flag(name: &str, value: &Value) -> Result { other => bail!("invalid GREE flag value for {name}: {other}"), } } +/// Optional GREE capabilities are not uniformly encoded as strict 0/1 flags. +/// For example Quiet is known to use 1, 2 and 3 on different units/firmware, +/// and other optional properties can also expose multi-state non-zero values. +/// The public application model is boolean, so keep 0 = off and treat any +/// positive value as active without allowing one optional field to invalidate +/// the complete climate status frame. +fn status_feature_flag(name: &str, value: &Value) -> Result { + let raw = status_i64(name, value)?; + if raw < 0 { + bail!("invalid GREE feature value for {name}: {raw}") + } + Ok(raw != 0) +} fn mode_name_checked(value: i64) -> Option<&'static str> { match value { 0 => Some("auto"), 1 => Some("cool"), 2 => Some("dry"), 3 => Some("fan"), 4 => Some("heat"), _ => None } } fn mode_value(value: &str) -> Result { match value.to_ascii_lowercase().as_str() { diff --git a/src/protocol/gree/polling.rs b/src/protocol/gree/polling.rs index 9c94370..0cd9982 100644 --- a/src/protocol/gree/polling.rs +++ b/src/protocol/gree/polling.rs @@ -196,31 +196,41 @@ impl GreeClient { "SwUpDn" => next.swing_vertical = status_i64(name, value)? != 0, "SwingLfRig" => next.swing_horizontal = status_i64(name, value)? != 0, "Quiet" => { - next.quiet = status_flag(name, value)?; + let raw = status_i64(name, value)?; + if raw < 0 { + bail!("invalid GREE feature value for {name}: {raw}") + } + next.quiet = raw != 0; + // Quiet is a multi-state field on some GREE families. Preserve known + // active encodings so a later boolean "enable Quiet" command does not + // silently change mode (e.g. a unit that reports/uses Quiet=2). + if (1..=3).contains(&raw) { + next.quiet_wire_value = Some(raw as u8); + } next.supports_quiet = Some(true); } "Tur" => { - next.turbo = status_flag(name, value)?; + next.turbo = status_feature_flag(name, value)?; next.supports_turbo = Some(true); } "Lig" => { - next.light = status_flag(name, value)?; + next.light = status_feature_flag(name, value)?; next.supports_light = Some(true); } "Air" => { - next.air = status_flag(name, value)?; + next.air = status_feature_flag(name, value)?; next.supports_air = Some(true); } "Blo" => { - next.xfan = status_flag(name, value)?; + next.xfan = status_feature_flag(name, value)?; next.supports_xfan = Some(true); } "Health" => { - next.health = status_flag(name, value)?; + next.health = status_feature_flag(name, value)?; next.supports_health = Some(true); } "SwhSlp" => { - next.sleep = status_flag(name, value)?; + next.sleep = status_feature_flag(name, value)?; next.supports_sleep = Some(true); } "TemSen" => { diff --git a/src/protocol/gree/tests.rs b/src/protocol/gree/tests.rs index 725ec14..2d6ae40 100644 --- a/src/protocol/gree/tests.rs +++ b/src/protocol/gree/tests.rs @@ -37,6 +37,7 @@ mod tests { ..DeviceCommand::default() }, false, + 1, ) .expect("thermostat command payload"); @@ -49,4 +50,65 @@ mod tests { Some(serde_json::json!([19, 1, 1, 1])) ); } + + #[test] + fn quiet_status_accepts_multistate_value_and_remembers_wire_mode() { + let client = GreeClient::new( + "test-controller".into(), + None, + None, + Arc::new(AtomicBool::new(false)), + ); + let mut device = Device::simulated_default(); + device.quiet = false; + device.quiet_wire_value = None; + + let response = json!({ + "cols": ["Pow", "Quiet"], + "dat": [1, 2] + }); + client.apply_status(&mut device, &response).expect("Quiet=2 status"); + + assert!(device.power); + assert!(device.quiet); + assert_eq!(device.quiet_wire_value, Some(2)); + assert_eq!(device.supports_quiet, Some(true)); + } + + #[test] + fn quiet_command_can_preserve_learned_wire_mode() { + let payload = GreeClient::command_payload( + &DeviceCommand { + quiet: Some(true), + ..DeviceCommand::default() + }, + false, + 2, + ) + .expect("quiet command payload"); + + assert_eq!(payload.get("opt").cloned(), Some(serde_json::json!(["Quiet"]))); + assert_eq!(payload.get("p").cloned(), Some(serde_json::json!([2]))); + } + + #[test] + fn optional_multistate_feature_does_not_reject_whole_status() { + let client = GreeClient::new( + "test-controller".into(), + None, + None, + Arc::new(AtomicBool::new(false)), + ); + let mut device = Device::simulated_default(); + + let response = json!({ + "cols": ["Pow", "Air"], + "dat": [1, 3] + }); + client.apply_status(&mut device, &response).expect("Air=3 status"); + + assert!(device.power); + assert!(device.air); + assert_eq!(device.supports_air, Some(true)); + } }