diff --git a/Cargo.lock b/Cargo.lock index 684e9f3..b689952 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -719,7 +719,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.16.0" +version = "0.16.1" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index 3522684..fd03a35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gree-controller" -version = "0.16.0" +version = "0.16.1" 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 4bdaa02..0bc4c26 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -1,17 +1,18 @@ de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example -a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -3c92ae20c070a1db66390a8ec95ed87bc0fed9ec82188e5348fc862ddcbff06e ./Cargo.lock -5a7d380c42118a8ab57939bfc99fe15dcf4ad4c1f4bfd5fd808164012ca2c0df ./Cargo.toml +2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore +1ece15d35e56470fbc0e9f0d9b1963f518c81a7b80270f2d2d91e53a4eb463ce ./Cargo.lock +8ad83bc5b719a3c0e058d0d6d28cc5af834f7a6d07d8fdc89c4c8ae804e1eb25 ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -8f2573aeec2240fa85c090b616575a9f5f79acc06ccfe8a98bf216ab13d3176a ./README.md -43cc9dbec1deb5c9f2fdddf03c4f19e745014a0f5ef44de556ca75b312b3acd8 ./SOURCE_MANIFEST.sha256 +3da89b24afe096e116b5725a279c2e84b493ea2ead4aa24895647e083c02298f ./README.md +9d39a66e1b041fef40a180e918ebfc676b9bf0406af3237327201b2b60ab53e8 ./SOURCE_MANIFEST.sha256 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs -dcbfdc5a281adb94c3e4186f88248d48d6e995b45591cdba7183f28e658698e5 ./docs/API.md -4c2a0ea22061c39ae4ff534134392edfb31264c9ccaa453e8448b60b881c3847 ./docs/FLOW.md -1a675292d181edb313850b26fa264b6a9daa3ccc4a632fb86828a04b3803ea36 ./docs/openapi.json -26130f86bc8bd020f0a1668d11bb0220f577779cbe2b008fb194d62dbb385ddd ./flows/stability/igor---heating-ha-stable.flow.json -63553ab25775d332b8aae0bf7715c37a3d2a19b12ebabbbf09335534f369c29a ./flows/stability/jan---heating-ha-stable.flow.json -8ffec14bd7ad358134e25cb901906494186a8a73788111fd4ced450f87cf3b6f ./flows/stability/sypialnia---heating-ha-stable.flow.json +a1f77b4c93063376a8568667713fc708d8e59541260834f9bb353ca44f1670cc ./docs/API.md +dd90e743bb9d3743ea677300a2ce0276f87f02fec2db0a3ffdcf5cc1bdf6cc60 ./docs/FLOW.md +7f8afa5a07a021f3f0fd49b1c65b14724e34229620ff58565b73dfe1fe7722a5 ./docs/openapi.json +716523299bfdf791276cc7c590caa3ea54530207c5e05cebb45e2ba6ef9da915 ./flows/multisplit/v4/INSTRUKCJA.txt +0a2f74baec4202b45708aecd7456d7339e4dbbe6c65fa497fb8b89c9d0e1d7c9 ./flows/multisplit/v4/igor-multisplit-soft-coast-v4.flow.json +1e91ca707015988ea01b944570b88fb27fa9a7b8aca48a8493885969ce69b452 ./flows/multisplit/v4/jan-multisplit-soft-coast-v4.flow.json +ef649edd25d41c2cf348149896003d21143bcadd599665c21322f6ee0d52ed36 ./flows/multisplit/v4/sypialnia-multisplit-soft-coast-v4.flow.json 77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example 0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile 0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md @@ -25,7 +26,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom 6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./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 -002170e041762ba17d2a082722a5c23fe13d126281e04aa5438a835ce98542a0 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json +8df63d9ae43ac72e47e1004ece2afc397f777c169f5715ad6148e3bfebedfc36 ./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 @@ -33,10 +34,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json 50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json -bfce426a0151c5616122446ded457bdefb20d691afa64003fd57e8b67b26ff85 ./ha-addon/repository/gree-controller/CHANGELOG.md +eaca083a91d15b31ed3415171f9bf4773312245ebd80ef0e9873d4ea11266986 ./ha-addon/repository/gree-controller/CHANGELOG.md 50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md -40036e3de45a68322e26d970eca50d3f17b57572f00fb13b0d4939d4a0fb36e0 ./ha-addon/repository/gree-controller/README.md -94cd29892fe53337bf5cf20ddd5317da514f311be14e056652ecfc3c45c7d45a ./ha-addon/repository/gree-controller/config.yaml +f7038a752048ecb1d2004e9dcccb9b43e5534bae4046331ceb33322316ad83b4 ./ha-addon/repository/gree-controller/README.md +878d3d369c458bc0c2d261b339be1843379e9f77f28141e3db0f19b77cd11105 ./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 2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml @@ -46,8 +47,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run 081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md -ea015719d066cab86aff9daab33c3a83a29def711195eca416652be45a033ef8 ./lang/en.json -fefb2710c34195a2d50c62aa107e2f284875990fa63f7da42c8019b0398c3387 ./lang/pl.json +271aa0ff32672f7888f0cfcb12dc1543ab22640549d5a43c4f3db9d6d45b64ee ./lang/en.json +0f6a09664d90b2a5edf9b7c33d77baf1ea39e94392ae3cc868d68169b4a8ad4f ./lang/pl.json d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json @@ -88,7 +89,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md -0636d1f517e866ea5d92960dd16a37eb848715e0f5b6c5e4e5a2f7fd550e2e4e ./scripts/api_dev_test.py +532ed10e2b6b52ad07685e90858ead075d51124b2cba4da57d19221224bfca19 ./scripts/api_dev_test.py 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh 1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh @@ -104,13 +105,13 @@ b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/upda ea84f5230da97447c60dfb4f8dadd0747c92fd1f7a74d5a65d6fdb6f23cf4b6d ./src/api.rs 0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs 1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs -1eb8c3cd86beb4f23aef21a23d31c144a9b31ec5bbaeaf24536723da5e78b768 ./src/api/automations.rs +36ad26dfc488e74359355069e5ea3309527b3d03bf879abf51c5d28497389901 ./src/api/automations.rs fffad7874465739cd6b508e9d7791387b864fa88420ce0f7e2fc933d129a2751 ./src/api/configuration.rs 19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs 8f92909dc575ea6238f4a003f8bd4056c2752fa3247b9ab226e47f875c80b916 ./src/api/devices.rs 4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs -1c5bc76fca312e6cd5fce988b3bfee785068322337b609405c12e18c1448665a ./src/api/flows.rs +a4bd4d09ee1f6ea6039d2eb563e446142aab6b28f7de510ca8a27ba3465abb92 ./src/api/flows.rs 82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs @@ -123,7 +124,7 @@ b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/publ fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs 7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs 3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs -ddd1bfc8625b66f6e5f3d24b3122ac86c25f89b0af527e3dfce9102551a8f142 ./src/api/zones.rs +95e636288b46cdeb00f9fbf34ac2dbd226184e0eca5a3a111f188a305b0a5258 ./src/api/zones.rs 32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs 87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs @@ -139,7 +140,7 @@ d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/sched 28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs 0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs -fbd0c387f48133cf7b1119bdd2830c3e3bb9238d94bcb3b76133b98ab657b601 ./src/engine/automations.rs +0bad57d36f6f58c40b0be595c3fe47d981460d61736fb48f389f649ccd4bea3b ./src/engine/automations.rs 1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs 357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs 828657b3c13b68792991b5ca2a26ae0b1d5c417191c83c4e5f650f62b6440096 ./src/engine/control_plan.rs @@ -149,15 +150,15 @@ e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/g 4ec11426ef860811348f2bf1b458bc5713fe8a7794fb4c8045dd637652e8e696 ./src/engine/history.rs 41b7045877771b7c352328998cb45ce2433e7f9631fad3eb1a882ab4897dc31f ./src/engine/local_thermostat.rs e61d7dea4b60e06fd8d08daaceecf53bd71b13ea2b0ad462692049f3cdaa5cef ./src/engine/ownership.rs -76c1594f31af64c9a34e2555ac015e8719d42273c687b4c85b9c8f306b3c69a2 ./src/engine/polling.rs +5fafacadbc4a3d7e375a8079f9ae1b80eb013c2fb651b8fcc65f5f17cf023426 ./src/engine/polling.rs c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/runtime.rs 48a496c3935f886a90b90461c38a7aa31ebc40c6482d6feaeb2be47bc2387625 ./src/engine/schedules.rs b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs -975298f6dec43bd962de67a1fdd2d6d9466bb0b16cc5dc46d04c2b23229e7a27 ./src/engine/temperature.rs +d62ac5e97a36bbdb31bc23871ea7de73d8ac64a1e8203153d9faff0b1a075bc9 ./src/engine/temperature.rs 833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs -bb7758192868f6e7589e70a3d2962fe7d05357821609f4e1eb8237cfb56af597 ./src/engine/tests.rs +ca5fbcdf44842766dd0741d2b728656535ff3a00f4a8a6da989d7183ceda51b6 ./src/engine/tests.rs c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs -08cb170ab73152720dbcf137bf8f3190e18ea2d498a49df128790fa59c2f3416 ./src/engine/zone_control.rs +edee10929eb89d7a774474cd9054890ee53b8c1fdf54e3d6f143265dead8f6b8 ./src/engine/zone_control.rs bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs @@ -166,7 +167,7 @@ da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb 67e42a92295480d2f1d364958f184654cfb5d4b77e7e1df47f588bdec18868a0 ./src/influxdb/write.rs 62f9da6ef4f5fd92701d0857773f9f6236ef25356f21b9b42236abab6ba7da53 ./src/main.rs 23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs -615fba5dc5f3ad75497d637b02cc83b7aacfe7baa6372c034dd5609bfff398a0 ./src/models/automation.rs +75bb9db327ae4ab1f29a5aa888e130be3a8e75b714fb5dbad4307bebee583c66 ./src/models/automation.rs e4195a40135f408302a78cb200a54e6ef1c0e6031fb069a2431d068e5dbbdd8d ./src/models/control_plan.rs 06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs 698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs @@ -176,7 +177,7 @@ aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/f 17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs 41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs -bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs +94ed531788a118f2f3fb5dd1748b2f416480a5945d8c7c7a722c201ce03212dd ./src/models/zone.rs 4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs 7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs 578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs @@ -205,19 +206,19 @@ b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree 6a30a5ecee99d2cf40381b9f770ed574e9e85b8b50e3425a77e6f64283faeee6 ./web/css/styles.css 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg -78996e460243aa3043bd889891a99b9417d930beb7c9ea81b4a7f496e3d58844 ./web/index.html +477de38c1cfbf2b925059795ef1b30522813d70908c36d3a471faf16da6935c8 ./web/index.html 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md e7318cd92b76e8a0109dc01adc2431474be17d8bb1488cdf6ffb39dc5450909d ./web/js-dynamic/bootstrap.js 5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js 07629b331bd498e8bea769d577321bbd12d9b076264242706d4ef16165fb22fa ./web/js-dynamic/core.js -80aaf5c56375345c67fd30c4fc1622319e4749b2859ec10613f77ac117f0441e ./web/js-dynamic/dashboard.js -930e67432f55e8476513e808edfa9a0809a8da7479307284fcc136082a4129a4 ./web/js-dynamic/entities.js -4612c958d7b9ccf89d6d45a11c3bc7e864bbd2ceaccc16fd7d76811ba4c74196 ./web/js-dynamic/events.js -3e52db4f0ab5d721a047bf5a8f690c2d634c1cb24433d50d95bcbaa617115bcc ./web/js-dynamic/flows.js -3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js +008a85cddf6a5357eff35764eabddf1d7baaecf949b481d195124de50431e0d2 ./web/js-dynamic/dashboard.js +c58dddbd1238ed9dfb590fc810e569c9a8408113cd85623e86190874591079e6 ./web/js-dynamic/entities.js +bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynamic/events.js +c4e14ebc7ddcec3c7346a037d488ceacd19606f76e84004b4f7c7dbd5fef646f ./web/js-dynamic/flows.js +1d54ae8919edb61414f9ffc46f85394b29c3c355827120264352cccb32f65e68 ./web/js-dynamic/forms.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js -30cbaebb817d5d416e1a372595b647aae42c14b9cc406c4751d0ced563243b69 ./web/js-dynamic/navigation.js +d9596cef1b86c0d3f73ccfc8d3a73c2b5293d81293fdbdcefd77346cbd3fe983 ./web/js-dynamic/navigation.js 3ff9c027beb8fb033330bf3cfb3496e06fb38c42041f1ffe522c6c7af18e9e30 ./web/js-dynamic/realtime.js 1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js diff --git a/README.md b/README.md index 9d3ec8c..e6365ee 100644 --- a/README.md +++ b/README.md @@ -2,9 +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. -**Version: 0.16.0** +**Version: 0.16.1** -Version 0.16.0 improves heating stability with Home Assistant room-temperature sensor control, reliable Flow ownership, and safer shutdown behavior. +Version 0.16.1 adds configurable early heat coasting without unit ON/OFF cycling, per-Flow settings for coasting and deep standby, and separate Flow/manual status in the Web UI. Existing compressor protection and shared multisplit OFF logic are unchanged. > [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples. > Built-in API: `/api/*` and `/ws`; interactive Swagger docs: `/api-docs`; OpenAPI JSON: `/api-docs/openapi.json`. diff --git a/SOURCE_MANIFEST.sha256 b/SOURCE_MANIFEST.sha256 index ea2ddc9..1a5651e 100644 --- a/SOURCE_MANIFEST.sha256 +++ b/SOURCE_MANIFEST.sha256 @@ -1,16 +1,17 @@ de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example -a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -3c92ae20c070a1db66390a8ec95ed87bc0fed9ec82188e5348fc862ddcbff06e ./Cargo.lock -5a7d380c42118a8ab57939bfc99fe15dcf4ad4c1f4bfd5fd808164012ca2c0df ./Cargo.toml +2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore +1ece15d35e56470fbc0e9f0d9b1963f518c81a7b80270f2d2d91e53a4eb463ce ./Cargo.lock +8ad83bc5b719a3c0e058d0d6d28cc5af834f7a6d07d8fdc89c4c8ae804e1eb25 ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -8f2573aeec2240fa85c090b616575a9f5f79acc06ccfe8a98bf216ab13d3176a ./README.md +3da89b24afe096e116b5725a279c2e84b493ea2ead4aa24895647e083c02298f ./README.md 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs -dcbfdc5a281adb94c3e4186f88248d48d6e995b45591cdba7183f28e658698e5 ./docs/API.md -4c2a0ea22061c39ae4ff534134392edfb31264c9ccaa453e8448b60b881c3847 ./docs/FLOW.md -1a675292d181edb313850b26fa264b6a9daa3ccc4a632fb86828a04b3803ea36 ./docs/openapi.json -26130f86bc8bd020f0a1668d11bb0220f577779cbe2b008fb194d62dbb385ddd ./flows/stability/igor---heating-ha-stable.flow.json -63553ab25775d332b8aae0bf7715c37a3d2a19b12ebabbbf09335534f369c29a ./flows/stability/jan---heating-ha-stable.flow.json -8ffec14bd7ad358134e25cb901906494186a8a73788111fd4ced450f87cf3b6f ./flows/stability/sypialnia---heating-ha-stable.flow.json +a1f77b4c93063376a8568667713fc708d8e59541260834f9bb353ca44f1670cc ./docs/API.md +dd90e743bb9d3743ea677300a2ce0276f87f02fec2db0a3ffdcf5cc1bdf6cc60 ./docs/FLOW.md +7f8afa5a07a021f3f0fd49b1c65b14724e34229620ff58565b73dfe1fe7722a5 ./docs/openapi.json +716523299bfdf791276cc7c590caa3ea54530207c5e05cebb45e2ba6ef9da915 ./flows/multisplit/v4/INSTRUKCJA.txt +0a2f74baec4202b45708aecd7456d7339e4dbbe6c65fa497fb8b89c9d0e1d7c9 ./flows/multisplit/v4/igor-multisplit-soft-coast-v4.flow.json +1e91ca707015988ea01b944570b88fb27fa9a7b8aca48a8493885969ce69b452 ./flows/multisplit/v4/jan-multisplit-soft-coast-v4.flow.json +ef649edd25d41c2cf348149896003d21143bcadd599665c21322f6ee0d52ed36 ./flows/multisplit/v4/sypialnia-multisplit-soft-coast-v4.flow.json 77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example 0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile 0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md @@ -24,7 +25,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom 6509286c7b7de638dcc5d40ce53181e65d1eddc5dc36a13306dc6d87220fedbb ./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 -002170e041762ba17d2a082722a5c23fe13d126281e04aa5438a835ce98542a0 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json +8df63d9ae43ac72e47e1004ece2afc397f777c169f5715ad6148e3bfebedfc36 ./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 @@ -32,10 +33,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom a745cc458c235158d5d70b6a33d8000fd8b40ff93bfe2f2796af0f4b3a9eef9b ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json 50b7d6fe6329ad4ea6bee9612ab141c2f73f8382fcc2af0bb076a8e8d4dd4465 ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json -bfce426a0151c5616122446ded457bdefb20d691afa64003fd57e8b67b26ff85 ./ha-addon/repository/gree-controller/CHANGELOG.md +eaca083a91d15b31ed3415171f9bf4773312245ebd80ef0e9873d4ea11266986 ./ha-addon/repository/gree-controller/CHANGELOG.md 50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md -40036e3de45a68322e26d970eca50d3f17b57572f00fb13b0d4939d4a0fb36e0 ./ha-addon/repository/gree-controller/README.md -94cd29892fe53337bf5cf20ddd5317da514f311be14e056652ecfc3c45c7d45a ./ha-addon/repository/gree-controller/config.yaml +f7038a752048ecb1d2004e9dcccb9b43e5534bae4046331ceb33322316ad83b4 ./ha-addon/repository/gree-controller/README.md +878d3d369c458bc0c2d261b339be1843379e9f77f28141e3db0f19b77cd11105 ./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 2272740b185e9520e948f74b8f45cf98b8b5fd28c93bdee760da626f91fe5c2a ./ha-addon/repository/gree-controller/translations/en.yaml @@ -45,8 +46,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run 081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md -ea015719d066cab86aff9daab33c3a83a29def711195eca416652be45a033ef8 ./lang/en.json -fefb2710c34195a2d50c62aa107e2f284875990fa63f7da42c8019b0398c3387 ./lang/pl.json +271aa0ff32672f7888f0cfcb12dc1543ab22640549d5a43c4f3db9d6d45b64ee ./lang/en.json +0f6a09664d90b2a5edf9b7c33d77baf1ea39e94392ae3cc868d68169b4a8ad4f ./lang/pl.json d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json @@ -87,7 +88,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md -0636d1f517e866ea5d92960dd16a37eb848715e0f5b6c5e4e5a2f7fd550e2e4e ./scripts/api_dev_test.py +532ed10e2b6b52ad07685e90858ead075d51124b2cba4da57d19221224bfca19 ./scripts/api_dev_test.py 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh 1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh @@ -103,13 +104,13 @@ b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/upda ea84f5230da97447c60dfb4f8dadd0747c92fd1f7a74d5a65d6fdb6f23cf4b6d ./src/api.rs 0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs 1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs -1eb8c3cd86beb4f23aef21a23d31c144a9b31ec5bbaeaf24536723da5e78b768 ./src/api/automations.rs +36ad26dfc488e74359355069e5ea3309527b3d03bf879abf51c5d28497389901 ./src/api/automations.rs fffad7874465739cd6b508e9d7791387b864fa88420ce0f7e2fc933d129a2751 ./src/api/configuration.rs 19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs 8f92909dc575ea6238f4a003f8bd4056c2752fa3247b9ab226e47f875c80b916 ./src/api/devices.rs 4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs -1c5bc76fca312e6cd5fce988b3bfee785068322337b609405c12e18c1448665a ./src/api/flows.rs +a4bd4d09ee1f6ea6039d2eb563e446142aab6b28f7de510ca8a27ba3465abb92 ./src/api/flows.rs 82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs @@ -122,7 +123,7 @@ b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/publ fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs 7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs 3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs -ddd1bfc8625b66f6e5f3d24b3122ac86c25f89b0af527e3dfce9102551a8f142 ./src/api/zones.rs +95e636288b46cdeb00f9fbf34ac2dbd226184e0eca5a3a111f188a305b0a5258 ./src/api/zones.rs 32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs 87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs @@ -138,7 +139,7 @@ d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/sched 28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs 0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs -fbd0c387f48133cf7b1119bdd2830c3e3bb9238d94bcb3b76133b98ab657b601 ./src/engine/automations.rs +0bad57d36f6f58c40b0be595c3fe47d981460d61736fb48f389f649ccd4bea3b ./src/engine/automations.rs 1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs 357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs 828657b3c13b68792991b5ca2a26ae0b1d5c417191c83c4e5f650f62b6440096 ./src/engine/control_plan.rs @@ -148,15 +149,15 @@ e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/g 4ec11426ef860811348f2bf1b458bc5713fe8a7794fb4c8045dd637652e8e696 ./src/engine/history.rs 41b7045877771b7c352328998cb45ce2433e7f9631fad3eb1a882ab4897dc31f ./src/engine/local_thermostat.rs e61d7dea4b60e06fd8d08daaceecf53bd71b13ea2b0ad462692049f3cdaa5cef ./src/engine/ownership.rs -76c1594f31af64c9a34e2555ac015e8719d42273c687b4c85b9c8f306b3c69a2 ./src/engine/polling.rs +5fafacadbc4a3d7e375a8079f9ae1b80eb013c2fb651b8fcc65f5f17cf023426 ./src/engine/polling.rs c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/runtime.rs 48a496c3935f886a90b90461c38a7aa31ebc40c6482d6feaeb2be47bc2387625 ./src/engine/schedules.rs b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs -975298f6dec43bd962de67a1fdd2d6d9466bb0b16cc5dc46d04c2b23229e7a27 ./src/engine/temperature.rs +d62ac5e97a36bbdb31bc23871ea7de73d8ac64a1e8203153d9faff0b1a075bc9 ./src/engine/temperature.rs 833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs -bb7758192868f6e7589e70a3d2962fe7d05357821609f4e1eb8237cfb56af597 ./src/engine/tests.rs +ca5fbcdf44842766dd0741d2b728656535ff3a00f4a8a6da989d7183ceda51b6 ./src/engine/tests.rs c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs -08cb170ab73152720dbcf137bf8f3190e18ea2d498a49df128790fa59c2f3416 ./src/engine/zone_control.rs +edee10929eb89d7a774474cd9054890ee53b8c1fdf54e3d6f143265dead8f6b8 ./src/engine/zone_control.rs bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs @@ -165,7 +166,7 @@ da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb 67e42a92295480d2f1d364958f184654cfb5d4b77e7e1df47f588bdec18868a0 ./src/influxdb/write.rs 62f9da6ef4f5fd92701d0857773f9f6236ef25356f21b9b42236abab6ba7da53 ./src/main.rs 23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs -615fba5dc5f3ad75497d637b02cc83b7aacfe7baa6372c034dd5609bfff398a0 ./src/models/automation.rs +75bb9db327ae4ab1f29a5aa888e130be3a8e75b714fb5dbad4307bebee583c66 ./src/models/automation.rs e4195a40135f408302a78cb200a54e6ef1c0e6031fb069a2431d068e5dbbdd8d ./src/models/control_plan.rs 06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs 698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs @@ -175,7 +176,7 @@ aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/f 17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs 41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs -bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs +94ed531788a118f2f3fb5dd1748b2f416480a5945d8c7c7a722c201ce03212dd ./src/models/zone.rs 4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs 7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs 578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs @@ -204,19 +205,19 @@ b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree 6a30a5ecee99d2cf40381b9f770ed574e9e85b8b50e3425a77e6f64283faeee6 ./web/css/styles.css 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg -78996e460243aa3043bd889891a99b9417d930beb7c9ea81b4a7f496e3d58844 ./web/index.html +477de38c1cfbf2b925059795ef1b30522813d70908c36d3a471faf16da6935c8 ./web/index.html 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md e7318cd92b76e8a0109dc01adc2431474be17d8bb1488cdf6ffb39dc5450909d ./web/js-dynamic/bootstrap.js 5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js 07629b331bd498e8bea769d577321bbd12d9b076264242706d4ef16165fb22fa ./web/js-dynamic/core.js -80aaf5c56375345c67fd30c4fc1622319e4749b2859ec10613f77ac117f0441e ./web/js-dynamic/dashboard.js -930e67432f55e8476513e808edfa9a0809a8da7479307284fcc136082a4129a4 ./web/js-dynamic/entities.js -4612c958d7b9ccf89d6d45a11c3bc7e864bbd2ceaccc16fd7d76811ba4c74196 ./web/js-dynamic/events.js -3e52db4f0ab5d721a047bf5a8f690c2d634c1cb24433d50d95bcbaa617115bcc ./web/js-dynamic/flows.js -3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js +008a85cddf6a5357eff35764eabddf1d7baaecf949b481d195124de50431e0d2 ./web/js-dynamic/dashboard.js +c58dddbd1238ed9dfb590fc810e569c9a8408113cd85623e86190874591079e6 ./web/js-dynamic/entities.js +bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynamic/events.js +c4e14ebc7ddcec3c7346a037d488ceacd19606f76e84004b4f7c7dbd5fef646f ./web/js-dynamic/flows.js +1d54ae8919edb61414f9ffc46f85394b29c3c355827120264352cccb32f65e68 ./web/js-dynamic/forms.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js -30cbaebb817d5d416e1a372595b647aae42c14b9cc406c4751d0ced563243b69 ./web/js-dynamic/navigation.js +d9596cef1b86c0d3f73ccfc8d3a73c2b5293d81293fdbdcefd77346cbd3fe983 ./web/js-dynamic/navigation.js 3ff9c027beb8fb033330bf3cfb3496e06fb38c42041f1ffe522c6c7af18e9e30 ./web/js-dynamic/realtime.js 1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js diff --git a/docs/API.md b/docs/API.md index db48135..68a6818 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,9 @@ # GREE Controller API reference -HTTP and WebSocket API for GREE Controller **0.16.0**. +HTTP and WebSocket API for GREE Controller **0.16.1**. + +Version 0.16.1: Zone configuration exposes `heat_anticipation_c` (0..2.5°C, default 0), runtime Zone exposes `heat_coasting`, and optional Flow `zone_thermostat` node configuration supports `heat_anticipation_c` and `standby_offset_c`. These settings alter the GREE temperature target, not physical power. + [← Main documentation](../README.md) @@ -243,7 +246,7 @@ Response: { "status": "ok", "name": "gree-controller", - "version": "0.16.0", + "version": "0.16.1", "uptime_seconds": 1234, "control_ready": true, "time": "2026-08-30T06:54:00Z" @@ -306,7 +309,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.16.0", + "version": "0.16.1", "uptime_seconds": 1234, "auth_required": false, "control_ready": true, diff --git a/docs/FLOW.md b/docs/FLOW.md index 8bc920d..940383c 100644 --- a/docs/FLOW.md +++ b/docs/FLOW.md @@ -14,6 +14,7 @@ Flow is an authoring/orchestration layer. It does not replace the thermostat, sc ## Existing control domains remain authoritative +- Optional `zone_thermostat` fields `heat_anticipation_c` (0–2.5 °C) and `standby_offset_c` (0.5–8 °C) persist into the target Zone when the Flow action executes. The regulator enters a hysteretic low-GREE-setpoint *coasting* state when room temperature reaches `target - heat_anticipation_c`, and resumes full setpoint heating once it falls another 0.4 °C. Coasting changes the requested GREE temperature and smart fan/Quiet behavior, **never the indoor unit power**; pre-existing compressor protection and multisplit shared-OFF flow gates remain unchanged. For older Flows the fields are absent and the previous regulator behavior remains. - `zone_thermostat` writes the existing persistent Zone intent and wakes the normal thermostat cycle. `power=false` sends an explicit physical OFF to the assigned unit but does **not** change the persistent Zone `enabled` flag; Zone enable/disable remains an administrative control gate. A concrete Flow mode (`heat`/`cool`) is also written with the OFF frame so a stale device `dry`/`fan` mode is not retained; Flow `auto` remains an ownership policy and is never mapped to physical GREE Auto on OFF. While a zone is owned by `automation.flow`, the global outdoor-assist bias is suppressed so the Flow's own room-temperature thresholds remain authoritative. It can also issue one-shot granular vertical/horizontal louver commands to the assigned unit; these auxiliary fields do not take over thermostat ownership. - `group_action` calls the existing `control_group` path, including custom group targets. - `device_action` uses the existing `DeviceCommand` / automatic-device path. diff --git a/docs/openapi.json b/docs/openapi.json index bbae1ca..2c5ff73 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "GREE Controller API", - "version": "0.16.0", + "version": "0.16.1", "summary": "Local HTTP/WebSocket API for GREE Controller", "description": "Local API used by the GREE Controller Web UI and Home Assistant integration.", "license": { @@ -6079,7 +6079,7 @@ }, "version": { "type": "string", - "example": "0.16.0" + "example": "0.16.1" }, "uptime_seconds": { "type": "integer", @@ -7173,6 +7173,13 @@ "description": "Optimistic concurrency revision when updating", "minimum": 1, "example": 12 + }, + "heat_anticipation_c": { + "type": "number", + "minimum": 0.0, + "maximum": 2.5, + "default": 0.0, + "description": "Temperature margin before target for early coasting without turning the unit OFF." } }, "required": [ @@ -7362,6 +7369,17 @@ "updated_at": { "type": "string", "format": "date-time" + }, + "heat_anticipation_c": { + "type": "number", + "minimum": 0.0, + "maximum": 2.5, + "default": 0.0, + "description": "Temperature margin before target for early coasting without turning the unit OFF." + }, + "heat_coasting": { + "type": "boolean", + "description": "True when the controller requested low setpoint while keeping unit powered ON." } }, "required": [ diff --git a/ha-addon/home-assistant/custom_components/gree_controller/manifest.json b/ha-addon/home-assistant/custom_components/gree_controller/manifest.json index 3eaaf45..3ab5fab 100644 --- a/ha-addon/home-assistant/custom_components/gree_controller/manifest.json +++ b/ha-addon/home-assistant/custom_components/gree_controller/manifest.json @@ -1,7 +1,7 @@ { "domain": "gree_controller", "name": "GREE Controller", - "version": "0.16.0", + "version": "0.16.1", "config_flow": true, "integration_type": "hub", "iot_class": "local_polling", diff --git a/ha-addon/repository/gree-controller/CHANGELOG.md b/ha-addon/repository/gree-controller/CHANGELOG.md index 152551d..1b3b32c 100644 --- a/ha-addon/repository/gree-controller/CHANGELOG.md +++ b/ha-addon/repository/gree-controller/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.16.1 + +- Adds hysteretic early-heating coasting: lower the GREE target before reaching room comfort temperature without switching the indoor unit OFF. +- Supports `heat_anticipation_c` and `standby_offset_c` settings in Visual Flow thermostat actions, including import/export and zone editing. +- Shows Flow heating/coasting/standby distinctly from real direct-manual device takeover. +- Preserves existing compressor protection and backward compatibility with existing Flow definitions. + ## 0.16.0 - Improves heating stability when Visual Flow automations use Home Assistant room-temperature sensors. diff --git a/ha-addon/repository/gree-controller/README.md b/ha-addon/repository/gree-controller/README.md index 9a24f83..e5de729 100644 --- a/ha-addon/repository/gree-controller/README.md +++ b/ha-addon/repository/gree-controller/README.md @@ -2,7 +2,7 @@ Local GREE HVAC controller packaged as a Home Assistant add-on, with Web UI, HTTP/WebSocket API, built-in Swagger API documentation, UDP discovery/control, `amd64` and `aarch64` support, ingress, and database backup from `/data/gree-controller.db`. -Version 0.16.0 improves heating stability with Home Assistant room-temperature sensor control, reliable Flow ownership, and safer shutdown behavior. +Version 0.16.1 adds early heating coasting with GREE setpoint modulation, configurable from Visual Flow, plus distinct Flow/manual status. Compressor protection and shared multisplit shutdown logic remain unchanged. ![Home Assistant OS + GREE + VLAN topology](https://git.linuxiarz.pl/gru/gree-controller-ha-addon/raw/branch/master/gree-controller/topology-ha-vlan-gree-en.png) diff --git a/ha-addon/repository/gree-controller/config.yaml b/ha-addon/repository/gree-controller/config.yaml index 74835d3..327f6f6 100644 --- a/ha-addon/repository/gree-controller/config.yaml +++ b/ha-addon/repository/gree-controller/config.yaml @@ -1,5 +1,5 @@ name: "GREE Controller" -version: "0.16.0" +version: "0.16.1" slug: "gree_controller" description: "Local GREE HVAC controller with Web UI, API, and Home Assistant integration" url: "https://git.linuxiarz.pl/gru/gree-controller-ha-addon/" diff --git a/lang/en.json b/lang/en.json index 38c154a..6104ed0 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1548,6 +1548,13 @@ "flow.contextDeleteConnection": "Delete connection", "flow.rightClickKey": "Right click", "flow.shortcutContextMenu": "Flow context menu", - "flow.heatingCurveDryRunValue": "Value passed from curve" + "flow.heatingCurveDryRunValue": "Value passed from curve", + "zones.ownerFlow": "Flow automation", + "zones.flowHeating": "Flow active - heating", + "zones.flowCoasting": "Flow active - coasting (unit ON, low target)", + "zones.flowStandby": "Flow active - soft standby", + "zones.heatAnticipation": "Early heat coasting (°C ahead)", + "zones.heatAnticipationHint": "Lowers the GREE target before the comfort target without powering the unit off. 0 disables coasting.", + "simulation.reasonCoasting": "Early heating coasting before {target}°C: GREE stays ON with a {standby}°C setpoint." } } diff --git a/lang/pl.json b/lang/pl.json index bcd868f..2806e69 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -1548,6 +1548,13 @@ "flow.contextDeleteConnection": "Usuń połączenie", "flow.rightClickKey": "Prawy przycisk", "flow.shortcutContextMenu": "Menu kontekstowe Flow", - "flow.heatingCurveDryRunValue": "Wartość przekazana z krzywej" + "flow.heatingCurveDryRunValue": "Wartość przekazana z krzywej", + "zones.ownerFlow": "Flow (automatyka)", + "zones.flowHeating": "Flow aktywny - grzanie", + "zones.flowCoasting": "Flow aktywny - wyhamowanie (ON, niska nastawa)", + "zones.flowStandby": "Flow aktywny - soft standby", + "zones.heatAnticipation": "Wyprzedzenie wyhamowania grzania (°C)", + "zones.heatAnticipationHint": "Obniża nastawę GREE przed osiągnięciem celu, bez OFF jednostki. 0 wyłącza funkcję.", + "simulation.reasonCoasting": "Wyhamowanie grzania przed celem {target}°C: GREE utrzymuje jednostkę ON z nastawą {standby}°C." } } diff --git a/scripts/api_dev_test.py b/scripts/api_dev_test.py index a5ed04e..029d449 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.16.0. +"""Developer smoke/integration tests for GREE Controller API 0.16.1. 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.16.0" +DEFAULT_EXPECTED_VERSION = "0.16.1" SETTINGS_PATHS: dict[str, set[str]] = { "/api/settings/application": {"simulator_enabled"}, @@ -599,7 +599,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.16.0 contract", lambda: test_openapi(client, args.expected_version)) + runner.run("OpenAPI 0.16.1 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)) diff --git a/src/api/automations.rs b/src/api/automations.rs index 66cc610..b3eeaea 100644 --- a/src/api/automations.rs +++ b/src/api/automations.rs @@ -164,6 +164,8 @@ impl AutomationInput { last_trigger_trace: vec![], action_zone_id: None, action_zone_preset: None, + action_zone_heat_anticipation_c: None, + action_zone_standby_offset_c: None, action_ha_domain: None, action_ha_service: None, action_ha_entity_id: None, diff --git a/src/api/flows.rs b/src/api/flows.rs index 02c4a77..b835d4b 100644 --- a/src/api/flows.rs +++ b/src/api/flows.rs @@ -1563,6 +1563,8 @@ fn compile_flow( last_trigger_trace, action_zone_id: None, action_zone_preset: None, + action_zone_heat_anticipation_c: None, + action_zone_standby_offset_c: None, action_ha_domain: None, action_ha_service: None, action_ha_entity_id: None, @@ -1606,6 +1608,32 @@ fn compile_flow( return Err(AppError::BadRequest("unsupported thermostat preset".into())); } item.action_zone_preset = Some(preset.clone()); + if let Some(value) = action_node.config.get("heat_anticipation_c") { + if !value.is_null() { + let offset = value.as_f64().ok_or_else(|| AppError::BadRequest( + "Flow heat anticipation must be numeric".into(), + ))?; + if !offset.is_finite() || !(0.0..=2.5).contains(&offset) { + return Err(AppError::BadRequest( + "Flow heat anticipation must be between 0 and 2.5 C".into(), + )); + } + item.action_zone_heat_anticipation_c = Some(offset); + } + } + if let Some(value) = action_node.config.get("standby_offset_c") { + if !value.is_null() { + let offset = value.as_f64().ok_or_else(|| AppError::BadRequest( + "Flow standby offset must be numeric".into(), + ))?; + if !offset.is_finite() || !(0.5..=8.0).contains(&offset) { + return Err(AppError::BadRequest( + "Flow standby offset must be between 0.5 and 8 C".into(), + )); + } + item.action_zone_standby_offset_c = Some(offset); + } + } if preset == "custom" && value_binding.is_none() { let target = flow_f64(&action_node.config, "setpoint").ok_or_else(|| { AppError::BadRequest("custom thermostat block needs a target".into()) diff --git a/src/api/zones.rs b/src/api/zones.rs index b9260b5..46d93cd 100644 --- a/src/api/zones.rs +++ b/src/api/zones.rs @@ -36,6 +36,8 @@ struct ZoneInput { min_off_seconds: u64, #[serde(default = "min_adjust")] min_adjust_seconds: u64, + #[serde(default)] + heat_anticipation_c: f64, #[serde(default = "standby_offset")] standby_offset_c: f64, #[serde(default = "yes")] @@ -142,6 +144,13 @@ impl ZoneInput { "heating hysteresis must be between 0.1 and 5 C".into(), )); } + if !self.heat_anticipation_c.is_finite() + || !(0.0..=2.5).contains(&self.heat_anticipation_c) + { + return Err(AppError::BadRequest( + "heat anticipation must be between 0 and 2.5 C".into(), + )); + } if !(0.5..=8.0).contains(&self.standby_offset_c) { return Err(AppError::BadRequest( "standby offset must be between 0.5 and 8 C".into(), @@ -205,6 +214,8 @@ impl ZoneInput { min_on_seconds: self.min_on_seconds, min_off_seconds: self.min_off_seconds, min_adjust_seconds: self.min_adjust_seconds, + heat_anticipation_c: self.heat_anticipation_c, + heat_coasting: false, standby_offset_c: self.standby_offset_c, smart_fan: self.smart_fan, sensor_source: self.sensor_source, @@ -400,6 +411,7 @@ async fn update_zone( zone.effective_mode = existing.effective_mode; zone.effective_setpoint = existing.effective_setpoint; zone.device_setpoint = existing.device_setpoint; + zone.heat_coasting = existing.heat_coasting; zone.demand = existing.demand; zone.demand_since = existing.demand_since; zone.target_alerted_at = existing.target_alerted_at; diff --git a/src/engine/automations.rs b/src/engine/automations.rs index 6813e2a..cbbbc3a 100644 --- a/src/engine/automations.rs +++ b/src/engine/automations.rs @@ -31,7 +31,14 @@ fn flow_zone_action_already_satisfied( device_swing_horizontal: u8, preset: Option<&str>, action: &DeviceCommand, + heat_anticipation_c: Option, + standby_offset_c: Option, ) -> bool { + if heat_anticipation_c.is_some_and(|value| (zone.heat_anticipation_c - value).abs() > 0.001) + || standby_offset_c.is_some_and(|value| (zone.standby_offset_c - value).abs() > 0.001) + { + return false; + } let mode_satisfied = match action.mode.as_deref() { Some("auto") => zone.inherit_house_mode, Some(mode) if matches!(mode, "heat" | "cool") => { @@ -193,6 +200,8 @@ async fn run_automations(state: &AppState) -> Result<()> { device.swing_horizontal, item.action_zone_preset.as_deref(), &effective_action, + item.action_zone_heat_anticipation_c, + item.action_zone_standby_offset_c, ) { let before_runtime = item.flow_runtime.clone(); flow_acknowledge_change_gates(&mut item); @@ -277,7 +286,10 @@ async fn run_automations(state: &AppState) -> Result<()> { Err(err) => Err(AppError::BadRequest(format!("Home Assistant service action failed: {err}"))), } } else if let Some(zone_id) = item.action_zone_id.as_deref() { - apply_flow_zone_action(state, zone_id, item.action_zone_preset.as_deref(), &effective_action).await + apply_flow_zone_action( + state, zone_id, item.action_zone_preset.as_deref(), &effective_action, + item.action_zone_heat_anticipation_c, item.action_zone_standby_offset_c, + ).await } else if let Some(group_id) = item.action_group_id.as_deref() { let group_mode = effective_action.mode.as_deref().map(|mode| if mode == "auto" { "house".to_string() } else { mode.to_string() }); control_group(state, group_id, GroupControlPatch { @@ -1105,7 +1117,10 @@ fn flow_condition_kind_runtime(kind: &str) -> bool { ) } -async fn apply_flow_zone_action(state: &AppState, zone_id: &str, preset: Option<&str>, action: &DeviceCommand) -> Result { +async fn apply_flow_zone_action( + state: &AppState, zone_id: &str, preset: Option<&str>, action: &DeviceCommand, + heat_anticipation_c: Option, standby_offset_c: Option, +) -> Result { let _schedule_guard = state.lock_schedule_operation().await; // Match the canonical thermostat ordering used by Web/HA zone control. This prevents // a Flow action from racing a thermostat arbitration cycle that already holds a stale snapshot. @@ -1117,6 +1132,8 @@ async fn apply_flow_zone_action(state: &AppState, zone_id: &str, preset: Option< // Flow power commands control only the assigned unit runtime; they never change zone.enabled. if !zone.enabled { return Ok(false); } let device_id = zone.device_id.clone(); + if let Some(value) = heat_anticipation_c { zone.heat_anticipation_c = value; } + if let Some(value) = standby_offset_c { zone.standby_offset_c = value; } if let Some(mode) = action.mode.as_deref() { match mode { "auto" => zone.inherit_house_mode = true, "heat" | "cool" => { zone.mode = mode.to_string(); zone.inherit_house_mode = false; }, _ => return Err(AppError::BadRequest("unsupported Flow thermostat mode".into())) } } diff --git a/src/engine/polling.rs b/src/engine/polling.rs index 2eb1127..6ccd03d 100644 --- a/src/engine/polling.rs +++ b/src/engine/polling.rs @@ -638,7 +638,7 @@ fn externally_changed_control_fields(before: &Device, after: &Device, zone: &Zon // again without user interaction. Treat that one known normalization as firmware drift, // not as a remote-control takeover. Other fan changes remain meaningful manual input. let standby_low_to_auto = - zone.smart_fan && !zone.demand && before.fan_speed == 1 && after.fan_speed == 0; + zone.smart_fan && (!zone.demand || zone.heat_coasting) && before.fan_speed == 1 && after.fan_speed == 0; if before.fan_speed != after.fan_speed && !standby_low_to_auto { fields.push("fan_speed".to_string()); } diff --git a/src/engine/temperature.rs b/src/engine/temperature.rs index 91dbe16..8d189cc 100644 --- a/src/engine/temperature.rs +++ b/src/engine/temperature.rs @@ -1,3 +1,24 @@ +/// GREE stays powered during coasting: only its temperature target is lowered. +/// Separate enter/exit thresholds prevent target chatter on a noisy HA sensor. +fn heat_coasting_next_state( + was_coasting: bool, + mode: &str, + temperature: f64, + comfort_target: f64, + anticipation_c: f64, +) -> bool { + if mode != "heat" || anticipation_c <= 0.0 { + return false; + } + let enter_at = comfort_target - anticipation_c; + let resume_heat_at = enter_at - 0.4; + if was_coasting { + temperature > resume_heat_at + 1e-6 + } else { + temperature + 1e-6 >= enter_at + } +} + fn select_zone_temperature( zone: &Zone, device_temperature: Option, diff --git a/src/engine/tests.rs b/src/engine/tests.rs index 726e849..cd2a25d 100644 --- a/src/engine/tests.rs +++ b/src/engine/tests.rs @@ -3,6 +3,17 @@ mod tests { use super::*; use chrono::TimeZone; + #[test] + fn heat_coasting_modulates_setpoint_without_power_cycle() { + assert!(!heat_coasting_next_state(false, "heat", 19.8, 20.8, 0.9)); + assert!(heat_coasting_next_state(false, "heat", 19.9, 20.8, 0.9)); + assert!(heat_coasting_next_state(true, "heat", 19.6, 20.8, 0.9)); + assert!(!heat_coasting_next_state(true, "heat", 19.5, 20.8, 0.9)); + assert!(!heat_coasting_next_state(true, "heat", 21.0, 20.8, 0.0)); + assert!(!heat_coasting_next_state(true, "cool", 21.0, 20.8, 0.9)); + assert_eq!(round_device_setpoint("heat", false, 20.8 - 5.0), 16.0); + } + #[test] fn thermostat_target_normalization_preserves_tenths() { assert!((normalize_thermostat_target(24.2) - 24.2).abs() < 1e-9); @@ -242,7 +253,15 @@ mod tests { ..Default::default() }; assert!(flow_zone_action_already_satisfied( - &zone, false, 0, 0, Some("custom"), &on + &zone, false, 0, 0, Some("custom"), &on, None, None + )); + assert!(!flow_zone_action_already_satisfied( + &zone, false, 0, 0, Some("custom"), &on, Some(1.1), Some(5.0) + )); + zone.heat_anticipation_c = 1.1; + zone.standby_offset_c = 5.0; + assert!(flow_zone_action_already_satisfied( + &zone, false, 0, 0, Some("custom"), &on, Some(1.1), Some(5.0) )); zone.manual_preset = None; @@ -257,10 +276,10 @@ mod tests { ..Default::default() }; assert!(flow_zone_action_already_satisfied( - &zone, false, 0, 0, Some("auto"), &off + &zone, false, 0, 0, Some("auto"), &off, None, None )); assert!(!flow_zone_action_already_satisfied( - &zone, true, 0, 0, Some("auto"), &off + &zone, true, 0, 0, Some("auto"), &off, None, None )); } @@ -279,10 +298,10 @@ mod tests { ..Default::default() }; assert!(!flow_zone_action_already_satisfied( - &zone, true, 0, 0, Some("custom"), &action + &zone, true, 0, 0, Some("custom"), &action, None, None )); assert!(flow_zone_action_already_satisfied( - &zone, true, 1, 0, Some("custom"), &action + &zone, true, 1, 0, Some("custom"), &action, None, None )); } @@ -547,6 +566,8 @@ mod tests { last_trigger_trace: vec![], action_zone_id: None, action_zone_preset: None, + action_zone_heat_anticipation_c: None, + action_zone_standby_offset_c: None, action_ha_domain: None, action_ha_service: None, action_ha_entity_id: None, @@ -589,6 +610,8 @@ mod tests { min_on_seconds: 180, min_off_seconds: 180, min_adjust_seconds: 120, + heat_anticipation_c: 0.0, + heat_coasting: false, standby_offset_c: 2.0, smart_fan: true, sensor_source: source.into(), diff --git a/src/engine/zone_control.rs b/src/engine/zone_control.rs index e5c3ef8..ea59c78 100644 --- a/src/engine/zone_control.rs +++ b/src/engine/zone_control.rs @@ -683,6 +683,7 @@ async fn control_zones(state: &AppState) -> Result<()> { // publishing the zone, but never overwrite manual device state while it follows // the house mode. Explicit per-zone heat/cool bypasses this branch above. if effective_mode == "off" { + zone.heat_coasting = false; zone.effective_setpoint = None; zone.device_setpoint = None; zone.demand = false; @@ -709,6 +710,7 @@ async fn control_zones(state: &AppState) -> Result<()> { // controller stays physically OFF. This also makes gaps between schedule windows true // OFF periods instead of silently falling back to Comfort and recreating demand. if !zone_has_active_thermostat_intent(&zone, active_schedule, Utc::now()) { + zone.heat_coasting = false; clear_compressor_pending(&mut zone, true); zone.effective_mode = "off".into(); zone.effective_setpoint = None; @@ -802,6 +804,14 @@ async fn control_zones(state: &AppState) -> Result<()> { let half = zone.hysteresis_for_mode(effective_mode) / 2.0; let previous_demand = zone.demand; + let previous_heat_coasting = zone.heat_coasting; + zone.heat_coasting = heat_coasting_next_state( + zone.heat_coasting, + effective_mode, + temp, + target, + zone.heat_anticipation_c, + ); zone.demand = match effective_mode { "heat" => { if temp <= target - half { @@ -857,14 +867,17 @@ async fn control_zones(state: &AppState) -> Result<()> { "heat" => target + outdoor_assist, _ => target - demand_assist, }; + // Early heat coasting changes only GREE's requested temperature. It does NOT + // toggle unit power or touch compressor protection/whole-multisplit OFF policy. + let delivering_heat = zone.demand && !(effective_mode == "heat" && zone.heat_coasting); let standby_target = match effective_mode { "heat" => target - zone.standby_offset_c.max(0.5), _ => target + zone.standby_offset_c.max(0.5), }; let desired_device_target = round_device_setpoint( effective_mode, - zone.demand, - if zone.demand { + delivering_heat, + if delivering_heat { active_target } else { standby_target @@ -878,7 +891,10 @@ async fn control_zones(state: &AppState) -> Result<()> { None }; - let demand_changed = previous_demand != zone.demand; + let demand_changed = previous_demand != zone.demand + || (effective_mode == "heat" && previous_heat_coasting != zone.heat_coasting); + let previous_delivering_heat = previous_demand + && !(effective_mode == "heat" && previous_heat_coasting); let desired_fan = if night_active { let max_fan = settings.night_mode.max_fan_speed.clamp(1, 5); if zone.smart_fan { @@ -888,7 +904,7 @@ async fn control_zones(state: &AppState) -> Result<()> { temp, target, outdoor_assist_temperature, - zone.demand, + delivering_heat, ), max_fan, )) @@ -903,7 +919,7 @@ async fn control_zones(state: &AppState) -> Result<()> { temp, target, outdoor_assist_temperature, - zone.demand, + delivering_heat, )) } else { None @@ -934,8 +950,8 @@ async fn control_zones(state: &AppState) -> Result<()> { let desired_quiet = smart_quiet_command( zone.smart_fan, quiet_supported, - previous_demand, - zone.demand, + previous_delivering_heat, + delivering_heat, device.quiet, settings.night_mode.enabled, night_active, @@ -1113,7 +1129,7 @@ async fn control_zones(state: &AppState) -> Result<()> { let fan_needs_command = desired_fan .map(|fan| fan != device.fan_speed) .unwrap_or(false) - && (zone.demand || demand_changed || core_needs_command || night_active); + && (delivering_heat || demand_changed || core_needs_command || (night_active && !zone.smart_fan)); let needs_command = core_needs_command || fan_needs_command || desired_quiet @@ -1150,10 +1166,12 @@ async fn control_zones(state: &AppState) -> Result<()> { }; zone.last_action_at = Some(transition_at); clear_compressor_pending(&mut zone, true); - state.log("info", "zone.setpoint_modulation", &format!("Zone {} -> {:.1} C ({})", zone.name, desired_device_target, if zone.demand { "demand" } else { "standby" }), json!({ + state.log("info", "zone.setpoint_modulation", &format!("Zone {} -> {:.1} C ({})", zone.name, desired_device_target, if zone.heat_coasting { "coasting" } else if zone.demand { "demand" } else { "standby" }), json!({ "zone_id": zone.id, "room_temperature": temp, "comfort_target": target, + "heat_coasting": zone.heat_coasting, + "heat_anticipation_c": zone.heat_anticipation_c, "device_target": desired_device_target, "mode": effective_mode, "preset": zone.active_preset, diff --git a/src/models/automation.rs b/src/models/automation.rs index 0407ed9..989cf0c 100644 --- a/src/models/automation.rs +++ b/src/models/automation.rs @@ -83,6 +83,11 @@ pub struct Automation { pub action_zone_id: Option, #[serde(default)] pub action_zone_preset: Option, + /// Optional soft-coast parameters set by a Flow thermostat action. + #[serde(default)] + pub action_zone_heat_anticipation_c: Option, + #[serde(default)] + pub action_zone_standby_offset_c: Option, #[serde(default)] pub action_ha_domain: Option, #[serde(default)] diff --git a/src/models/zone.rs b/src/models/zone.rs index c012fbc..b5b5bf5 100644 --- a/src/models/zone.rs +++ b/src/models/zone.rs @@ -44,6 +44,13 @@ pub struct Zone { /// Minimum interval between automatic setpoint/fan adjustments. #[serde(default = "default_min_adjust")] pub min_adjust_seconds: u64, + /// Degrees before the comfort target at which GREE enters low-heat coasting. + /// Zero disables predictive soft standby (the pre-upgrade behavior). + #[serde(default)] + pub heat_anticipation_c: f64, + /// Hysteretic coasting state; never changes the physical unit power state. + #[serde(default)] + pub heat_coasting: bool, /// Difference applied to the AC setpoint while the room is satisfied. #[serde(default = "default_standby_offset")] pub standby_offset_c: f64, diff --git a/web/index.html b/web/index.html index bd093c0..f4584d1 100644 --- a/web/index.html +++ b/web/index.html @@ -1598,6 +1598,9 @@ type="number" min="15" name="min_adjust_seconds" value="120"> + +

A positive value lowers the GREE setpoint early without powering the indoor unit off. 0 disables early coasting.

0 && current != null && coastEnter != null + && (zone?.heat_coasting ? current > coastEnter - 0.4 + 1e-6 : current + 1e-6 >= coastEnter); + const deliveringHeat = demand && !heatCoasting; + const assist = current == null || target == null || zone?.control_source === 'automation.flow' ? 0 : simulationOutdoorAssist(mode, outdoor, current, target); const activeTarget = current == null || target == null ? null : (mode === 'heat' ? target + assist : target - assist); const standbyTarget = target == null ? null : (mode === 'heat' ? target - standbyOffset : target + standbyOffset); - const desiredDeviceTarget = current == null || target == null ? null : simulationRoundDeviceSetpoint(mode, demand, demand ? activeTarget : standbyTarget); + const desiredDeviceTarget = current == null || target == null ? null : simulationRoundDeviceSetpoint(mode, deliveringHeat, deliveringHeat ? activeTarget : standbyTarget); const nightActive = !!app.controlPlan?.night_mode_active; const nightMaxFan = clamp(Number(app.controlPlan?.night_mode_max_fan_speed || 1), 1, 5); let fanSpeed = simulationNumeric(device?.fan_speed); if (mode !== 'off') { - fanSpeed = zone?.smart_fan && current != null && target != null ? simulationSmartFanSpeed(mode, current, target, outdoor, demand) : fanSpeed; + fanSpeed = zone?.smart_fan && current != null && target != null ? simulationSmartFanSpeed(mode, current, target, outdoor, deliveringHeat) : fanSpeed; if (nightActive) { if (zone?.smart_fan) fanSpeed = fanSpeed === 0 ? 1 : Math.min(fanSpeed ?? nightMaxFan, nightMaxFan); else if (fanSpeed === 0 || fanSpeed == null || fanSpeed > nightMaxFan) fanSpeed = nightMaxFan; @@ -423,7 +428,7 @@ function buildZoneSimulation(zone, planZone) { ? tr('simulation.manual') : nightActive && app.settings?.night_mode?.force_quiet ? tr('simulation.quietIfSupported') - : (zone?.smart_fan ? (!demand ? tr('simulation.quietIfSupported') : tr('common.off')) : tr('simulation.manual')); + : (zone?.smart_fan ? (!deliveringHeat ? tr('simulation.quietIfSupported') : tr('common.off')) : tr('simulation.manual')); const nativeSleep = mode !== 'off' && nightActive && app.settings?.night_mode?.use_native_sleep && device?.supports_sleep === true; const reasoning = !zone?.enabled || planZoneEffectiveEnabled(planZone) === false ? tr('simulation.reasonDisabled') @@ -431,10 +436,12 @@ function buildZoneSimulation(zone, planZone) { ? tr('simulation.reasonHouseOff') : current == null || target == null ? tr('simulation.reasonWaiting') - : demand + : heatCoasting + ? tr('simulation.reasonCoasting', { target: target.toFixed(1), standby: standbyTarget.toFixed(1) }) + : demand ? tr('simulation.reasonDemand', { mode: modeLabel(mode), target: target.toFixed(1), hysteresis: hysteresis.toFixed(1) }) : tr('simulation.reasonSatisfied', { target: target.toFixed(1), standby: standbyTarget.toFixed(1) }); - return { device, mode, current, target, demand, status, outdoor, hysteresis, assist, activeTarget, standbyTarget, desiredDeviceTarget, fanSpeed, quiet, nativeSleep, reasoning }; + return { device, mode, current, target, demand, heatCoasting, status, outdoor, hysteresis, assist, activeTarget, standbyTarget, desiredDeviceTarget, fanSpeed, quiet, nativeSleep, reasoning }; } function simulationRuleAction(rule) { diff --git a/web/js-dynamic/entities.js b/web/js-dynamic/entities.js index c34547f..5597dd9 100644 --- a/web/js-dynamic/entities.js +++ b/web/js-dynamic/entities.js @@ -453,6 +453,7 @@ function zoneControlOwnerLabel(zone) { } if (owner === 'local_thermostat') return tr('zones.ownerLocalThermostat'); if (source.startsWith('group:')) return tr('zones.ownerGroup'); + if (source === 'automation.flow') return tr('zones.ownerFlow'); return tr('zones.ownerAutomation'); } @@ -478,6 +479,10 @@ function zoneRuntimeStatusLabel(zone, effectiveMode, device = null) { if (zone.compressor_cancelled_action && device && (!device.power || (['heat', 'cool'].includes(effectiveMode) && device.mode !== effectiveMode))) return tr('zones.queueCancelledStatus'); if (zone.demand && device && !device.power) return tr('zones.waitingStart'); if (zone.demand && device?.power && ['heat', 'cool'].includes(effectiveMode) && device.mode !== effectiveMode) return tr('zones.waitingMode'); + if (zone.control_owner === 'automation' && zone.control_source === 'automation.flow') { + if (zone.demand && zone.heat_coasting && device?.power) return tr('zones.flowCoasting'); + return zone.demand ? tr('zones.flowHeating') : tr('zones.flowStandby'); + } return zone.demand ? tr('zones.runningDemand') : tr('zones.satisfied'); } @@ -555,6 +560,7 @@ function zoneCard(zone, detailed = true) {
${esc(tr('zones.source'))}${esc(zoneStrategyLabel(zone))}
${esc(tr('zones.hysteresis'))}${zone.separate_hysteresis ? `${esc(tr('mode.cool'))} ${Number(zone.cool_hysteresis ?? zone.hysteresis ?? 0.6).toFixed(1)}°C · ${esc(tr('mode.heat'))} ${Number(zone.heat_hysteresis ?? zone.hysteresis ?? 0.6).toFixed(1)}°C` : `${Number(zone.hysteresis ?? 0.6).toFixed(1)}°C`}
${esc(tr('zones.smartFan'))}${esc(tr(zone.smart_fan === false ? 'common.off' : 'common.on'))}
+
${esc(tr('zones.heatAnticipation'))}${Number(zone.heat_anticipation_c ?? 0).toFixed(1)}°C
${esc(sensorDetails)}
${compressorQueuePanel(zone)} diff --git a/web/js-dynamic/events.js b/web/js-dynamic/events.js index 96cc171..7daaa1a 100644 --- a/web/js-dynamic/events.js +++ b/web/js-dynamic/events.js @@ -340,7 +340,7 @@ $('#copyZoneSettings')?.addEventListener('click', () => { ha_outdoor_entity_id: f.elements.ha_outdoor_entity_id.value, enabled: f.elements.enabled.checked, }; - const copyFields = ['setpoint', 'cool_comfort_setpoint', 'cool_sleep_setpoint', 'cool_away_setpoint', 'heat_comfort_setpoint', 'heat_sleep_setpoint', 'heat_away_setpoint', 'hysteresis', 'cool_hysteresis', 'heat_hysteresis', 'min_on_seconds', 'min_off_seconds', 'min_adjust_seconds', 'standby_offset_c', 'external_sensor_weight_percent', 'max_sensor_difference', 'sensor_stale_after_seconds']; + const copyFields = ['setpoint', 'cool_comfort_setpoint', 'cool_sleep_setpoint', 'cool_away_setpoint', 'heat_comfort_setpoint', 'heat_sleep_setpoint', 'heat_away_setpoint', 'hysteresis', 'cool_hysteresis', 'heat_hysteresis', 'min_on_seconds', 'min_off_seconds', 'min_adjust_seconds', 'standby_offset_c', 'heat_anticipation_c', 'external_sensor_weight_percent', 'max_sensor_difference', 'sensor_stale_after_seconds']; copyFields.forEach(name => { const input = f.elements[name]; if (!input) return; @@ -583,7 +583,8 @@ $('#zoneForm').addEventListener('submit', async event => { cool_comfort_setpoint: parseDecimal(raw.cool_comfort_setpoint), cool_sleep_setpoint: parseDecimal(raw.cool_sleep_setpoint), cool_away_setpoint: parseDecimal(raw.cool_away_setpoint), heat_comfort_setpoint: parseDecimal(raw.heat_comfort_setpoint), heat_sleep_setpoint: parseDecimal(raw.heat_sleep_setpoint), heat_away_setpoint: parseDecimal(raw.heat_away_setpoint), hysteresis: parseDecimal(raw.hysteresis), separate_hysteresis: form.separate_hysteresis.checked, cool_hysteresis: parseDecimal(raw.cool_hysteresis), heat_hysteresis: parseDecimal(raw.heat_hysteresis), min_on_seconds: Number(raw.min_on_seconds), min_off_seconds: Number(raw.min_off_seconds), - min_adjust_seconds: Number(raw.min_adjust_seconds), standby_offset_c: parseDecimal(raw.standby_offset_c), smart_fan: form.smart_fan.checked, + min_adjust_seconds: Number(raw.min_adjust_seconds), standby_offset_c: parseDecimal(raw.standby_offset_c), + heat_anticipation_c: parseDecimal(raw.heat_anticipation_c), smart_fan: form.smart_fan.checked, sensor_source: raw.sensor_source, ha_entity_id: raw.ha_entity_id || null, ha_outdoor_entity_id: raw.ha_outdoor_entity_id || null, external_sensor_weight: Number(raw.external_sensor_weight_percent) / 100, max_sensor_difference: parseDecimal(raw.max_sensor_difference), sensor_stale_after_seconds: Number(raw.sensor_stale_after_seconds || 300), revision: raw.revision ? Number(raw.revision) : null }; await runFormTask(form, async () => { diff --git a/web/js-dynamic/flows.js b/web/js-dynamic/flows.js index fd29d42..ad85748 100644 --- a/web/js-dynamic/flows.js +++ b/web/js-dynamic/flows.js @@ -98,7 +98,7 @@ function flowDefaultConfig(kind) { if (kind === 'night_mode') return {}; if (kind === 'constant') return { value: true }; if (kind === 'shared_input') return sharedFlowReferenceDefaultConfig(app.flowSharedInputs?.[0]); - if (kind === 'zone_thermostat') return { zone_id: app.zones[0]?.id || '', preset: 'comfort', setpoint: 21, mode: 'auto', power: null, swing_vertical: null, swing_horizontal: null, cooldown_seconds: 60 }; + if (kind === 'zone_thermostat') return { zone_id: app.zones[0]?.id || '', preset: 'comfort', setpoint: 21, mode: 'auto', power: null, heat_anticipation_c: null, standby_offset_c: null, swing_vertical: null, swing_horizontal: null, cooldown_seconds: 60 }; if (kind === 'device_action') return { device_id: app.devices[0]?.id || '', power: true, mode: '', target_temperature: null, fan_speed: null, swing_vertical: null, swing_horizontal: null, quiet: null, turbo: null, light: null, air: null, xfan: null, health: null, sleep: null, cooldown_seconds: 60 }; if (kind === 'device_feature_action') return { device_id: app.devices[0]?.id || '', feature: 'light', value: true, cooldown_seconds: 60 }; if (kind === 'group_action') return { group_id: app.groups[0]?.id || '', power: true, mode: '', preset: '', setpoint: 21, cooldown_seconds: 60 }; @@ -813,7 +813,7 @@ function renderFlowInspector() { else if (node.kind === 'logic_and') fields = `

${esc(tr('flow.andHint'))}

`; else if (node.kind === 'logic_or') fields = `

${esc(tr('flow.orHint'))}

`; else if (node.kind === 'logic_not') fields = `

${esc(tr('flow.notHint'))}

`; - else if (node.kind === 'zone_thermostat') fields = `
${esc(tr('flow.thermostatDeviceOptions'))}
${flowOptionalLouverField(c, 'swing_vertical', 'vertical')}${flowOptionalLouverField(c, 'swing_horizontal', 'horizontal')}

${esc(tr('flow.thermostatDeviceOptionsHint'))}

`; + else if (node.kind === 'zone_thermostat') fields = `
${esc(tr('flow.thermostatDeviceOptions'))}
${flowOptionalLouverField(c, 'swing_vertical', 'vertical')}${flowOptionalLouverField(c, 'swing_horizontal', 'horizontal')}

${esc(tr('flow.thermostatDeviceOptionsHint'))}

`; else if (node.kind === 'device_action') fields = `${flowActionFields(c, false)}`; else if (node.kind === 'device_feature_action') fields = `${flowDeviceFeatureValueField(c)}

${esc(tr('flow.deviceFeatureHint'))}

`; else if (node.kind === 'group_action') fields = `${flowActionFields(c, true)}`; @@ -1736,6 +1736,8 @@ function updateFlowConfig(input) { else if (wasLouver) node.config.value = ''; } else if (node.kind === 'device_state' && key === 'value' && ['swing_vertical', 'swing_horizontal'].includes(node.config.field)) node.config.value = Number(input.value); + else if (node.kind === 'zone_thermostat' && ['heat_anticipation_c', 'standby_offset_c'].includes(key)) + node.config[key] = input.value === '' ? null : Number(input.value); else if (['swing_vertical', 'swing_horizontal'].includes(key)) node.config[key] = input.value === '' ? null : Number(input.value); else if (['power', 'quiet', 'turbo', 'light', 'air', 'xfan', 'health', 'sleep'].includes(key)) node.config[key] = input.value === '' ? null : input.value === 'true'; else if (key === 'value' && node.kind === 'constant') node.config[key] = input.value === 'true'; diff --git a/web/js-dynamic/forms.js b/web/js-dynamic/forms.js index c38627a..895c5c8 100644 --- a/web/js-dynamic/forms.js +++ b/web/js-dynamic/forms.js @@ -150,7 +150,7 @@ function validateForm(form) { const zoneDecimalFields = [ ['cool_comfort_setpoint', 8, 30], ['cool_sleep_setpoint', 8, 30], ['cool_away_setpoint', 8, 30], ['heat_comfort_setpoint', 8, 30], ['heat_sleep_setpoint', 8, 30], ['heat_away_setpoint', 8, 30], - ['max_sensor_difference', 0.1, 20], ['standby_offset_c', 0.5, 8], + ['max_sensor_difference', 0.1, 20], ['standby_offset_c', 0.5, 8], ['heat_anticipation_c', 0, 2.5], ...(form.elements.separate_hysteresis?.checked ? [['cool_hysteresis', 0.1, 5], ['heat_hysteresis', 0.1, 5]] : [['hysteresis', 0.1, 5]]) diff --git a/web/js-dynamic/navigation.js b/web/js-dynamic/navigation.js index 2cd759f..463634a 100644 --- a/web/js-dynamic/navigation.js +++ b/web/js-dynamic/navigation.js @@ -598,6 +598,7 @@ function populateZone(id) { form.max_sensor_difference.value = Number(item.max_sensor_difference ?? 3); form.min_adjust_seconds.value = Number(item.min_adjust_seconds ?? 120); form.standby_offset_c.value = Number(item.standby_offset_c ?? 2); + form.heat_anticipation_c.value = Number(item.heat_anticipation_c ?? 0); form.smart_fan.checked = item.smart_fan !== false; form.separate_hysteresis.checked = item.separate_hysteresis === true; const commonHysteresis = Number(item.hysteresis ?? 0.6);