diff --git a/Cargo.lock b/Cargo.lock index b689952..b3cafd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -719,7 +719,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.16.1" +version = "0.16.3" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index fd03a35..793966a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gree-controller" -version = "0.16.1" +version = "0.16.3" 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 0bc4c26..3c0b3f3 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -1,18 +1,17 @@ de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example 2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore -1ece15d35e56470fbc0e9f0d9b1963f518c81a7b80270f2d2d91e53a4eb463ce ./Cargo.lock -8ad83bc5b719a3c0e058d0d6d28cc5af834f7a6d07d8fdc89c4c8ae804e1eb25 ./Cargo.toml +2f663bd03e41e40d584183074dca077c8963afc8308bd66e7d33ea6aed6ec1e5 ./Cargo.lock +8f4f035398326b1beec96d2c0d4b8fcf4b7f1542e39fe719743cc95ecca740e1 ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -3da89b24afe096e116b5725a279c2e84b493ea2ead4aa24895647e083c02298f ./README.md -9d39a66e1b041fef40a180e918ebfc676b9bf0406af3237327201b2b60ab53e8 ./SOURCE_MANIFEST.sha256 +eaea820d2578f56442792e6fd6403eb1253686e50dbfcd8e75179c98574e1859 ./README.md +2cc1518c6484c340037700ef258e27cdf71a7aa5233aefcb51cfa7c199c3f3a7 ./SOURCE_MANIFEST.sha256 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs -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 +e23bf048051f522364d213b0bd0ddfaff82d311a84542e20568dcf3f6aec7ffc ./docs/API.md +c64564481d1a7bb4657f12b2fe0cbddf86050a4c6a2fc600ad8f02051387aa27 ./docs/FLOW.md +bfb0bba73a02e71aba78c0f5de1fd41d51d70812f6e08eaf2eaceb1295908c9d ./docs/openapi.json +a5ad73f7933031dfaa99aa92e27f10223d53fb84b14ee0a378eb5f10eb80e8c8 ./flows/simple-heating/igor-simple-heat-v5.flow.json +9743f4328a18bc9d89d10a9011d13ca201a4f352db2119335a66a2147a9a486d ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json +2d804831a72577e80dc1ec3cf4f86dc8bcf00b1d8c80929896f11da0230b2b07 ./flows/simple-heating/sypialnia-simple-heat-v5.flow.json 77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example 0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile 0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md @@ -26,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 -8df63d9ae43ac72e47e1004ece2afc397f777c169f5715ad6148e3bfebedfc36 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json +ebe14ca743c50cc0cc02f94621b17119984704f702df04cd94e1e5b86635a014 ./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 @@ -34,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 -eaca083a91d15b31ed3415171f9bf4773312245ebd80ef0e9873d4ea11266986 ./ha-addon/repository/gree-controller/CHANGELOG.md +1b32f617ed09f37d58fd8532107cfcbf2fb92c4434bd173b0fc73464664ce927 ./ha-addon/repository/gree-controller/CHANGELOG.md 50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md -f7038a752048ecb1d2004e9dcccb9b43e5534bae4046331ceb33322316ad83b4 ./ha-addon/repository/gree-controller/README.md -878d3d369c458bc0c2d261b339be1843379e9f77f28141e3db0f19b77cd11105 ./ha-addon/repository/gree-controller/config.yaml +0e92e538aa03b442f8fc2c21562cc7aa19d8ae346f78cecca1eec131288edade ./ha-addon/repository/gree-controller/README.md +e48d172951db6df8afe97b0d463902e744c22bd3e677041bf5e6ee5624646f25 ./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 @@ -47,8 +46,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run 081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md -271aa0ff32672f7888f0cfcb12dc1543ab22640549d5a43c4f3db9d6d45b64ee ./lang/en.json -0f6a09664d90b2a5edf9b7c33d77baf1ea39e94392ae3cc868d68169b4a8ad4f ./lang/pl.json +4bf92ee5627be67b66d76a6106b224159f95071f0aa2a727cd964e113bc3b32a ./lang/en.json +1e056cd915ba2e808db1287c99c06bbbbb3c98e5008481471b895e6ec20ea43a ./lang/pl.json d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json @@ -88,8 +87,8 @@ ae21459a261712bcb8d57594528b1648432e8d03a8a38502234829c0dbfef774 ./presets/week fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/window_available_guard.json 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh -c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md -532ed10e2b6b52ad07685e90858ead075d51124b2cba4da57d19221224bfca19 ./scripts/api_dev_test.py +437ac712fe194ecbd6e2299c834894887ab860a5a563df9a7eea4b0a2f6818c7 ./scripts/README.md +971b899ff544c215b754598f77f8f50b108868a119ec741d2abebfb6fb833ddf ./scripts/api_dev_test.py 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh 1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh @@ -101,17 +100,20 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw 01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh 36396ecfcfd30f50016b17a83cd76cbccd8debaacef45a0d6b4e61946fef098d ./scripts/smoke.sh +b1e7a7a7a1d6e21012808818ff1478f5116a3dc4c7da3650b57a719ca9df7873 ./scripts/test_flow_block_help_ui.js +2942b34d82d06c950fbf167442fd1a25185de193055e02f2ce8dff62c2039add ./scripts/test_simple_heating_ui.js b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh -ea84f5230da97447c60dfb4f8dadd0747c92fd1f7a74d5a65d6fdb6f23cf4b6d ./src/api.rs +14f07768257945ed52c218f3bf03c25cb55053e7a6683c8ff17f395e16d28f2e ./scripts/verify_simple_heating.py +c32caf3ac7b521324d1d27233c271374ccf4efc3d243544ece727372c792e14a ./src/api.rs 0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs 1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs -36ad26dfc488e74359355069e5ea3309527b3d03bf879abf51c5d28497389901 ./src/api/automations.rs +34378c387d34d67483030d1016b874a74115c1ef5f268f854180c25146fdb08b ./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 -a4bd4d09ee1f6ea6039d2eb563e446142aab6b28f7de510ca8a27ba3465abb92 ./src/api/flows.rs +8c5d46ccaa45950f4dbaae63e68ad6efe393ff0904e46420f5e7950f043fb691 ./src/api/flows.rs 82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs @@ -124,7 +126,7 @@ b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/publ fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs 7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs 3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs -95e636288b46cdeb00f9fbf34ac2dbd226184e0eca5a3a111f188a305b0a5258 ./src/api/zones.rs +7c65692070e51af1ebd66d98b087b7f7a241ed31f8ff2f41baf77e46e027688c ./src/api/zones.rs 32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs 87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs @@ -139,11 +141,11 @@ bba6e3fb9f4800ae06d0ca67772b06a8c389d308243306719442d0c1f2623420 ./src/db/flows d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs 28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs -0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs -0bad57d36f6f58c40b0be595c3fe47d981460d61736fb48f389f649ccd4bea3b ./src/engine/automations.rs +7b29709d2a97e201a6f7baab9c3a72ea3792488499c47e426ebb93369fcbb3fc ./src/engine.rs +4412770793e4905d490a56e0f46cc678a5e28044fc0d136912ef1b81da3e3e3f ./src/engine/automations.rs 1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs 357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs -828657b3c13b68792991b5ca2a26ae0b1d5c417191c83c4e5f650f62b6440096 ./src/engine/control_plan.rs +dbf73f1459185836487653d05e73ffd8314fb0244d23878fc84232fd10935b8a ./src/engine/control_plan.rs 7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs 519f67ab805f61afea107215f5df50562fa1669a48334cc015570952e25e773a ./src/engine/energy.rs e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs @@ -153,12 +155,13 @@ e61d7dea4b60e06fd8d08daaceecf53bd71b13ea2b0ad462692049f3cdaa5cef ./src/engine/o 5fafacadbc4a3d7e375a8079f9ae1b80eb013c2fb651b8fcc65f5f17cf023426 ./src/engine/polling.rs c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/runtime.rs 48a496c3935f886a90b90461c38a7aa31ebc40c6482d6feaeb2be47bc2387625 ./src/engine/schedules.rs -b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs +9adfcb3c3dffbaa870b314904bd1995e7b78c3f12febd69e23433179e4bf3fba ./src/engine/simple_heat.rs +2694363e607b7f4407ed07c24019c457f0e0d093cb336ee2dc5cf77487c528f6 ./src/engine/targets.rs d62ac5e97a36bbdb31bc23871ea7de73d8ac64a1e8203153d9faff0b1a075bc9 ./src/engine/temperature.rs 833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs -ca5fbcdf44842766dd0741d2b728656535ff3a00f4a8a6da989d7183ceda51b6 ./src/engine/tests.rs +6f81f1b93f653b8450b9e086ed4728eca90b3d5d7639b7f4fb4fa2f42c514a28 ./src/engine/tests.rs c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs -edee10929eb89d7a774474cd9054890ee53b8c1fdf54e3d6f143265dead8f6b8 ./src/engine/zone_control.rs +2cbfc34ad0868591e60e3110d8ae346dff07a58c894e87598e4b7b5749fb4613 ./src/engine/zone_control.rs bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs @@ -167,7 +170,7 @@ da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb 67e42a92295480d2f1d364958f184654cfb5d4b77e7e1df47f588bdec18868a0 ./src/influxdb/write.rs 62f9da6ef4f5fd92701d0857773f9f6236ef25356f21b9b42236abab6ba7da53 ./src/main.rs 23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs -75bb9db327ae4ab1f29a5aa888e130be3a8e75b714fb5dbad4307bebee583c66 ./src/models/automation.rs +de10221fb1cb7c9eb2f658bc07d710f0b03ed573d994c0df31851cf7c49c4605 ./src/models/automation.rs e4195a40135f408302a78cb200a54e6ef1c0e6031fb069a2431d068e5dbbdd8d ./src/models/control_plan.rs 06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs 698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs @@ -177,7 +180,7 @@ aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/f 17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs 41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs -94ed531788a118f2f3fb5dd1748b2f416480a5945d8c7c7a722c201ce03212dd ./src/models/zone.rs +5229bdb5ce1018601b081654d16cee9ac28afdfda6c67effe1b14d44b14b3257 ./src/models/zone.rs 4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs 7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs 578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs @@ -203,18 +206,18 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/ d8a602ed8906d77593bd140315ce1bd56716038e789f53ff31762e9b3cb7220d ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html -6a30a5ecee99d2cf40381b9f770ed574e9e85b8b50e3425a77e6f64283faeee6 ./web/css/styles.css +282ef3c84f053cfce36fa85828365cac2cb6dfa4375993345ff3bee05c8a884f ./web/css/styles.css 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg -477de38c1cfbf2b925059795ef1b30522813d70908c36d3a471faf16da6935c8 ./web/index.html +e76b1e0bbdad0297d5ae2e79e40e658e0263f038ab70db81d330a342e01a0a8b ./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 008a85cddf6a5357eff35764eabddf1d7baaecf949b481d195124de50431e0d2 ./web/js-dynamic/dashboard.js -c58dddbd1238ed9dfb590fc810e569c9a8408113cd85623e86190874591079e6 ./web/js-dynamic/entities.js +3a6d2edd64fbdc2a8d468b05816366a9e05df284adea752c42a91e2a3a7bd786 ./web/js-dynamic/entities.js bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynamic/events.js -c4e14ebc7ddcec3c7346a037d488ceacd19606f76e84004b4f7c7dbd5fef646f ./web/js-dynamic/flows.js +3e2a5f1aab56e918624e6e7af064eecb9e94e554fd4d185715f8ecf3747a6895 ./web/js-dynamic/flows.js 1d54ae8919edb61414f9ffc46f85394b29c3c355827120264352cccb32f65e68 ./web/js-dynamic/forms.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js diff --git a/README.md b/README.md index e6365ee..2cb512d 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.1** +**Version: 0.16.3** -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. +Version 0.16.3 adds built-in, localized explanations for the two Flow blocks (constant Yes and Simple day/night heating), including live examples calculated from each room's configured temperature and tooltips for every setting. Heating/control behavior from 0.16.2 is 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`. @@ -61,6 +61,9 @@ Useful development commands: ./scripts/dev.sh --release # optimized local build ./scripts/dev.sh --reset # recreate the local SQLite database ./scripts/smoke.sh # API smoke test against an isolated simulated instance +python3 scripts/verify_simple_heating.py # JSON/JS/import/metadata consistency (no Rust required) +node scripts/test_simple_heating_ui.js # Flow vs manual UI status scenarios +cargo test simple_heat_tests # six Rust temperature-regulation scenarios ``` The default panel is available at `http://127.0.0.1:8787` when the process is bound locally. @@ -506,3 +509,7 @@ The editor supports optimistic revisions, so saving an older copy from another b Diagnostics include a non-mutating dry-run endpoint/UI with a selectable simulation time, optional per-block sensor/state overrides, a per-node condition trace and an ownership/block reason. The Flow list also exposes a simpler scenario simulator beside the editor button: it uses live values by default and reduces the result to clear will-run / will-not-run / blocked outcomes with readable condition checks. Shared Home Assistant Flow inputs can also be tested directly in their editor against the current HA entity state. Flow-scoped execution and dry-run events can be viewed from the editor. Individual Flows can be exported/imported as versioned `gree-controller-flow` JSON. A saved Flow can be exported directly from its card in the Flow list without opening the editor, and its description can be edited there through a quick modal; the editor export remains available for saved and unsaved drafts. Generated schedules and automations are intentionally excluded from the portable document and are regenerated from the source graph on import. Incomplete Flows can also be saved as disabled drafts; drafts preserve the editor graph but intentionally generate no schedules or automations until completed and saved normally. Shared inputs show which Flows reference them and link directly to those editors. The preset library includes 37 categorized scenarios for comfort, energy, safety, night, reliability, Home Assistant heat-source coordination and advanced multi-branch logic. + +### Proste grzanie Flow (0.16.2) + +Dla sypialni, Jana i Igora gotowe proste Flow (po 2 bloki) znajdują się w `flows/simple-heating/`. Zastępują złożone grafy v4; po imporcie należy dezaktywować stare v4. Osobne cele nocne: sypialnia 20.5 °C, Jan 20.8 °C, Igor 20.8 °C (można edytować w bloku). Dzienny cel 20.0 °C, próg uruchomienia 19.7 °C, zatrzymanie po 30 min stabilności. Wymaga włączonego harmonogramu **Trybu nocnego**. Działanie i progi: `docs/FLOW.md`. diff --git a/SOURCE_MANIFEST.sha256 b/SOURCE_MANIFEST.sha256 index 1a5651e..0e1fe32 100644 --- a/SOURCE_MANIFEST.sha256 +++ b/SOURCE_MANIFEST.sha256 @@ -1,17 +1,16 @@ de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example 2fd3bf1fb7ef548c7abc9b26d1cc4ef470784b8637af65446b845b27a45b547b ./.gitignore -1ece15d35e56470fbc0e9f0d9b1963f518c81a7b80270f2d2d91e53a4eb463ce ./Cargo.lock -8ad83bc5b719a3c0e058d0d6d28cc5af834f7a6d07d8fdc89c4c8ae804e1eb25 ./Cargo.toml +2f663bd03e41e40d584183074dca077c8963afc8308bd66e7d33ea6aed6ec1e5 ./Cargo.lock +8f4f035398326b1beec96d2c0d4b8fcf4b7f1542e39fe719743cc95ecca740e1 ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -3da89b24afe096e116b5725a279c2e84b493ea2ead4aa24895647e083c02298f ./README.md +eaea820d2578f56442792e6fd6403eb1253686e50dbfcd8e75179c98574e1859 ./README.md 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs -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 +e23bf048051f522364d213b0bd0ddfaff82d311a84542e20568dcf3f6aec7ffc ./docs/API.md +c64564481d1a7bb4657f12b2fe0cbddf86050a4c6a2fc600ad8f02051387aa27 ./docs/FLOW.md +bfb0bba73a02e71aba78c0f5de1fd41d51d70812f6e08eaf2eaceb1295908c9d ./docs/openapi.json +a5ad73f7933031dfaa99aa92e27f10223d53fb84b14ee0a378eb5f10eb80e8c8 ./flows/simple-heating/igor-simple-heat-v5.flow.json +9743f4328a18bc9d89d10a9011d13ca201a4f352db2119335a66a2147a9a486d ./flows/simple-heating/jan-pieczara-simple-heat-v5.flow.json +2d804831a72577e80dc1ec3cf4f86dc8bcf00b1d8c80929896f11da0230b2b07 ./flows/simple-heating/sypialnia-simple-heat-v5.flow.json 77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example 0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile 0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md @@ -25,7 +24,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 -8df63d9ae43ac72e47e1004ece2afc397f777c169f5715ad6148e3bfebedfc36 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json +ebe14ca743c50cc0cc02f94621b17119984704f702df04cd94e1e5b86635a014 ./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 +32,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 -eaca083a91d15b31ed3415171f9bf4773312245ebd80ef0e9873d4ea11266986 ./ha-addon/repository/gree-controller/CHANGELOG.md +1b32f617ed09f37d58fd8532107cfcbf2fb92c4434bd173b0fc73464664ce927 ./ha-addon/repository/gree-controller/CHANGELOG.md 50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md -f7038a752048ecb1d2004e9dcccb9b43e5534bae4046331ceb33322316ad83b4 ./ha-addon/repository/gree-controller/README.md -878d3d369c458bc0c2d261b339be1843379e9f77f28141e3db0f19b77cd11105 ./ha-addon/repository/gree-controller/config.yaml +0e92e538aa03b442f8fc2c21562cc7aa19d8ae346f78cecca1eec131288edade ./ha-addon/repository/gree-controller/README.md +e48d172951db6df8afe97b0d463902e744c22bd3e677041bf5e6ee5624646f25 ./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 +45,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run 081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md -271aa0ff32672f7888f0cfcb12dc1543ab22640549d5a43c4f3db9d6d45b64ee ./lang/en.json -0f6a09664d90b2a5edf9b7c33d77baf1ea39e94392ae3cc868d68169b4a8ad4f ./lang/pl.json +4bf92ee5627be67b66d76a6106b224159f95071f0aa2a727cd964e113bc3b32a ./lang/en.json +1e056cd915ba2e808db1287c99c06bbbbb3c98e5008481471b895e6ec20ea43a ./lang/pl.json d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json @@ -87,8 +86,8 @@ ae21459a261712bcb8d57594528b1648432e8d03a8a38502234829c0dbfef774 ./presets/week fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/window_available_guard.json 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh -c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md -532ed10e2b6b52ad07685e90858ead075d51124b2cba4da57d19221224bfca19 ./scripts/api_dev_test.py +437ac712fe194ecbd6e2299c834894887ab860a5a563df9a7eea4b0a2f6818c7 ./scripts/README.md +971b899ff544c215b754598f77f8f50b108868a119ec741d2abebfb6fb833ddf ./scripts/api_dev_test.py 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh 1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh @@ -100,17 +99,20 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw 01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh 36396ecfcfd30f50016b17a83cd76cbccd8debaacef45a0d6b4e61946fef098d ./scripts/smoke.sh +b1e7a7a7a1d6e21012808818ff1478f5116a3dc4c7da3650b57a719ca9df7873 ./scripts/test_flow_block_help_ui.js +2942b34d82d06c950fbf167442fd1a25185de193055e02f2ce8dff62c2039add ./scripts/test_simple_heating_ui.js b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh -ea84f5230da97447c60dfb4f8dadd0747c92fd1f7a74d5a65d6fdb6f23cf4b6d ./src/api.rs +14f07768257945ed52c218f3bf03c25cb55053e7a6683c8ff17f395e16d28f2e ./scripts/verify_simple_heating.py +c32caf3ac7b521324d1d27233c271374ccf4efc3d243544ece727372c792e14a ./src/api.rs 0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs 1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs -36ad26dfc488e74359355069e5ea3309527b3d03bf879abf51c5d28497389901 ./src/api/automations.rs +34378c387d34d67483030d1016b874a74115c1ef5f268f854180c25146fdb08b ./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 -a4bd4d09ee1f6ea6039d2eb563e446142aab6b28f7de510ca8a27ba3465abb92 ./src/api/flows.rs +8c5d46ccaa45950f4dbaae63e68ad6efe393ff0904e46420f5e7950f043fb691 ./src/api/flows.rs 82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs @@ -123,7 +125,7 @@ b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/publ fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs 7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs 3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs -95e636288b46cdeb00f9fbf34ac2dbd226184e0eca5a3a111f188a305b0a5258 ./src/api/zones.rs +7c65692070e51af1ebd66d98b087b7f7a241ed31f8ff2f41baf77e46e027688c ./src/api/zones.rs 32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs 87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs @@ -138,11 +140,11 @@ bba6e3fb9f4800ae06d0ca67772b06a8c389d308243306719442d0c1f2623420 ./src/db/flows d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs 28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs -0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs -0bad57d36f6f58c40b0be595c3fe47d981460d61736fb48f389f649ccd4bea3b ./src/engine/automations.rs +7b29709d2a97e201a6f7baab9c3a72ea3792488499c47e426ebb93369fcbb3fc ./src/engine.rs +4412770793e4905d490a56e0f46cc678a5e28044fc0d136912ef1b81da3e3e3f ./src/engine/automations.rs 1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs 357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs -828657b3c13b68792991b5ca2a26ae0b1d5c417191c83c4e5f650f62b6440096 ./src/engine/control_plan.rs +dbf73f1459185836487653d05e73ffd8314fb0244d23878fc84232fd10935b8a ./src/engine/control_plan.rs 7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs 519f67ab805f61afea107215f5df50562fa1669a48334cc015570952e25e773a ./src/engine/energy.rs e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs @@ -152,12 +154,13 @@ e61d7dea4b60e06fd8d08daaceecf53bd71b13ea2b0ad462692049f3cdaa5cef ./src/engine/o 5fafacadbc4a3d7e375a8079f9ae1b80eb013c2fb651b8fcc65f5f17cf023426 ./src/engine/polling.rs c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/runtime.rs 48a496c3935f886a90b90461c38a7aa31ebc40c6482d6feaeb2be47bc2387625 ./src/engine/schedules.rs -b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs +9adfcb3c3dffbaa870b314904bd1995e7b78c3f12febd69e23433179e4bf3fba ./src/engine/simple_heat.rs +2694363e607b7f4407ed07c24019c457f0e0d093cb336ee2dc5cf77487c528f6 ./src/engine/targets.rs d62ac5e97a36bbdb31bc23871ea7de73d8ac64a1e8203153d9faff0b1a075bc9 ./src/engine/temperature.rs 833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs -ca5fbcdf44842766dd0741d2b728656535ff3a00f4a8a6da989d7183ceda51b6 ./src/engine/tests.rs +6f81f1b93f653b8450b9e086ed4728eca90b3d5d7639b7f4fb4fa2f42c514a28 ./src/engine/tests.rs c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs -edee10929eb89d7a774474cd9054890ee53b8c1fdf54e3d6f143265dead8f6b8 ./src/engine/zone_control.rs +2cbfc34ad0868591e60e3110d8ae346dff07a58c894e87598e4b7b5749fb4613 ./src/engine/zone_control.rs bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs @@ -166,7 +169,7 @@ da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb 67e42a92295480d2f1d364958f184654cfb5d4b77e7e1df47f588bdec18868a0 ./src/influxdb/write.rs 62f9da6ef4f5fd92701d0857773f9f6236ef25356f21b9b42236abab6ba7da53 ./src/main.rs 23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs -75bb9db327ae4ab1f29a5aa888e130be3a8e75b714fb5dbad4307bebee583c66 ./src/models/automation.rs +de10221fb1cb7c9eb2f658bc07d710f0b03ed573d994c0df31851cf7c49c4605 ./src/models/automation.rs e4195a40135f408302a78cb200a54e6ef1c0e6031fb069a2431d068e5dbbdd8d ./src/models/control_plan.rs 06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs 698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs @@ -176,7 +179,7 @@ aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/f 17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs 41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs -94ed531788a118f2f3fb5dd1748b2f416480a5945d8c7c7a722c201ce03212dd ./src/models/zone.rs +5229bdb5ce1018601b081654d16cee9ac28afdfda6c67effe1b14d44b14b3257 ./src/models/zone.rs 4c1f676eaf0b6da45b4d4b090ae3ac862fb97c1f9e53284a1640767ae9591d8f ./src/notifications.rs 7746bca683809b82529ed152272ae5c0cd3971758a02e076def58f770e7adf10 ./src/protocol/crypto.rs 578b9f39110abbeb98323044b6ae1a941733c82021afeb4d7ff78ad4256b5aa5 ./src/protocol/gree.rs @@ -202,18 +205,18 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/ d8a602ed8906d77593bd140315ce1bd56716038e789f53ff31762e9b3cb7220d ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html -6a30a5ecee99d2cf40381b9f770ed574e9e85b8b50e3425a77e6f64283faeee6 ./web/css/styles.css +282ef3c84f053cfce36fa85828365cac2cb6dfa4375993345ff3bee05c8a884f ./web/css/styles.css 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg -477de38c1cfbf2b925059795ef1b30522813d70908c36d3a471faf16da6935c8 ./web/index.html +e76b1e0bbdad0297d5ae2e79e40e658e0263f038ab70db81d330a342e01a0a8b ./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 008a85cddf6a5357eff35764eabddf1d7baaecf949b481d195124de50431e0d2 ./web/js-dynamic/dashboard.js -c58dddbd1238ed9dfb590fc810e569c9a8408113cd85623e86190874591079e6 ./web/js-dynamic/entities.js +3a6d2edd64fbdc2a8d468b05816366a9e05df284adea752c42a91e2a3a7bd786 ./web/js-dynamic/entities.js bc1e7c4cccda0447fe805c61e655b773375bf503141195b82d4e95cdf093ec80 ./web/js-dynamic/events.js -c4e14ebc7ddcec3c7346a037d488ceacd19606f76e84004b4f7c7dbd5fef646f ./web/js-dynamic/flows.js +3e2a5f1aab56e918624e6e7af064eecb9e94e554fd4d185715f8ecf3747a6895 ./web/js-dynamic/flows.js 1d54ae8919edb61414f9ffc46f85394b29c3c355827120264352cccb32f65e68 ./web/js-dynamic/forms.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js diff --git a/docs/API.md b/docs/API.md index 68a6818..662e2c0 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,8 +1,10 @@ # GREE Controller API reference -HTTP and WebSocket API for GREE Controller **0.16.1**. +HTTP and WebSocket API for GREE Controller **0.16.3**. -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. +Version 0.16.3: the Flow editor adds localized descriptions and contextual examples for the two heating blocks; the API and heating behavior remain compatible with 0.16.2. + +Version 0.16.2: Flow adds the `zone_heat_cycle` action with `zone_id`, `day_target`, `night_target`, `day_min`, `boost_after_minutes` and `day_off_after_minutes`. An always-true Flow condition is sufficient. The Zone runtime stores the active `simple_heat` profile and timers; source `automation.flow` means automatic control, not manual. Nighttime modulation keeps the unit ON and uses 19/21/23 °C around a 20.6 °C target. Daytime power is OFF above the minimum until required, then OFF after 30 minutes at target. Existing `zone_thermostat` coasting options remain supported. [← Main documentation](../README.md) @@ -246,7 +248,7 @@ Response: { "status": "ok", "name": "gree-controller", - "version": "0.16.1", + "version": "0.16.3", "uptime_seconds": 1234, "control_ready": true, "time": "2026-08-30T06:54:00Z" @@ -309,7 +311,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.1", + "version": "0.16.3", "uptime_seconds": 1234, "auth_required": false, "control_ready": true, @@ -1734,6 +1736,14 @@ Runtime settings expose `compressor_protection_enabled` and `compressor_protecti Cancellation suppresses the same pending intent until a new explicit thermostat/group/house command re-arms it, a scheduled Temporary Quick Thermostat session takes ownership, or a different mode/target creates a new intent. Safety OFF commands are not delayed by compressor protection. Direct technical device commands remain immediate manual-control operations. +### Simple two-node heating Flow (0.16.2) + +Create a `constant` condition with `{"value":true}` and connect it to a `zone_heat_cycle` action. The action configuration takes `zone_id` (zone UUID), `day_target`, `night_target`, `day_min`, `boost_after_minutes` (3–30) and `day_off_after_minutes` (10–120). Day/night boundaries come from global Night Mode. The generated Flow automation owns an independent per-zone temperature policy; this is **automatic** ownership (`control_source=automation.flow`), not a manual `custom` preset. The measured temperature comes from that zone's configured GREE/HA/combined sensor. + +GREE modulation at a night room target of 20.6 °C: 21 °C for a moderate dip, 23 °C for a deep dip or no rise after the configured trial, 20 °C at 20.5 °C in the room, 19 °C at/above 20.6 °C. Nighttime physical power remains ON. During day the indoor unit is OFF while above `day_min`; after falling below it the unit heats and powers OFF after the configured continuous time at `day_target`. Compressor-protection settings still apply. Disabling/removing the Flow clears its thermostat policy on the next zone cycle. + +Ready-to-import examples are in `flows/simple-heating/`. Disable older Flow v4 before importing to prevent conflicting rules. + ### Flow portability and diagnostics `GET /api/flows/:id/export` returns a versioned `gree-controller-flow` JSON document containing the source graph plus definitions of every referenced Shared Input. Generated schedules/automations are not exported. The existing envelope may include an optional top-level `shared_inputs` array while leaving the `flow` object unchanged. diff --git a/docs/FLOW.md b/docs/FLOW.md index 940383c..75f72a3 100644 --- a/docs/FLOW.md +++ b/docs/FLOW.md @@ -97,3 +97,29 @@ These need durable state and restart semantics before they should be exposed in ### Display name / alias The Flow name edited in `/flows` is a presentation alias. Renaming it changes labels shown in the UI (including the dashboard) but does not change the stable Flow ID or generated `flow-` schedule/automation names. + +## Proste grzanie dzień/noc (od 0.16.2) + +Do jednego pokoju potrzebne są **tylko dwa bloki**: `Stała: Tak` → `Proste grzanie dzień/noc`. +Blok ustawiasz na odpowiednią strefę oraz: + +- `day_target` – komfort w dzień (np. 20.0 °C), `night_target` – komfort w nocy (osobny dla każdego pokoju), +- `day_min` – próg alarmowy w dzień (np. 19.7 °C); powyżej niego jednostka pozostaje **OFF** dopóki wcześniej nie zaczęła ratunkowego dogrzewania, +- `boost_after_minutes` – czas próby delikatnego grzania (np. 10 min; Jan: 8 min), +- `day_off_after_minutes` – czas ciągłego utrzymywania dziennego celu przed wyłączeniem (np. 30 min). + +Przebieg (dla celu 20.6 °C): noc: **20.6 °C → GREE 19 °C**, od 20.5 °C wyhamowanie **20 °C**, poniżej tego delikatnie **21 °C**; poniżej 19.8 °C lub po 10 minutach bez wzrostu o 0.1 °C grzanie **23 °C**. Przy celu innego pokoju nastawy GREE zmieniają się proporcjonalnie do celu, z zaokrągleniem do całych stopni. Jednostka pozostaje włączona w nocy (niższa nastawa pozwala jej się samoczynnie uspokoić). + +Dzień: przy celu 20.0 °C łagodna faza ustawia GREE na 21 °C, a pełna faza na 22 °C. Nad `day_min` jednostka jest fizycznie wyłączona. Przy spadku poniżej `day_min` podejmuje grzanie; po osiągnięciu `day_target` przez nieprzerwane `day_off_after_minutes` wyłącza jednostkę, z zachowaniem zabezpieczeń sprężarki. Jeśli w tym czasie cel zostaje utracony, odliczanie zaczyna się od nowa. + +**Ważne:** Ustawienia > **Tryb nocny** muszą mieć włączony harmonogram i prawidłowe godziny, ponieważ ten sam kalendarz ustala dzień/noc dla wszystkich stref. Temperatura pokoju pochodzi ze skonfigurowanego czujnika termostatu strefy (w tym czujnika HA/combined), a nie z osobnego warunku Flow. Pole `control_source=automation.flow` oznacza automatyczną kontrolę (nie ręczną), również gdy GREE jest w uśpieniu. Rzeczywisty pilot/ręczne sterowanie nadal ma pierwszeństwo. + +Gotowe importy: `flows/simple-heating/`. Po imporcie **wyłącz lub usuń oryginalne Flow v4**, żeby uniknąć konkurujących poleceń. Wyłączenie nowego Flow usuwa jego profil sterowania w następnym cyklu regulatora. + +### Pomoc w samych blokach (0.16.3) + +Po kliknięciu bloku **Stała testowa** edytor wyjaśnia, że wartość **Tak** jest bezwarunkowym sygnałem wejściowym w każdym cyklu, a **Nie** blokuje akcję. W bibliotece bloków oba nowe opisy są widoczne pod nazwami; w palecie i na schemacie pojawiają się jako podpowiedzi po najechaniu. + +W bloku **Proste grzanie dzień/noc** opis jest widoczny nad ustawieniami, a sekcja **Jak działa?** rozwija kolejne fazy: nocne uśpienie, hamowanie, delikatne i mocne grzanie, dzienny próg uruchomienia oraz warunek wyłączenia po stabilizacji. Temperatury przykładowe są wyliczane na podstawie rzeczywistych wartości ustawionych w edytowanym bloku, więc Jan, Igor i sypialnia mają poprawne progi. Opis obejmuje też dobór źródła pomiaru w strefie, zachowanie przy braku danych HA i pierwszeństwo sterowania ręcznego. + +Ta aktualizacja dotyczy **tylko pomocy UI**. Nie zmienia algorytmu, formatu eksportu Flow ani nastaw trzech wcześniejszych importów; nie trzeba ich ponownie importować. diff --git a/docs/openapi.json b/docs/openapi.json index 2c5ff73..b44f50b 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.1", + "version": "0.16.3", "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.1" + "example": "0.16.3" }, "uptime_seconds": { "type": "integer", 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 3ab5fab..a5097d9 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.1", + "version": "0.16.3", "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 1b3b32c..0e8c50a 100644 --- a/ha-addon/repository/gree-controller/CHANGELOG.md +++ b/ha-addon/repository/gree-controller/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.16.3 + +- Adds expandable, per-room examples for nighttime 19/21/23°C modulation, daytime OFF/boost/30-minute stability, zone temperature-source fallback and Flow vs manual ownership. +- Adds tooltips explaining each day/night heating setting. No changes to runtime heating algorithms, saved Flow JSON schema or existing imported flows. + +## 0.16.2 + +- Adds `zone_heat_cycle`: a single day/night room-temperature controller for one zone. Use a constant true condition plus the new block (two Flow nodes total). +- Night: low demand first (typically GREE 21 °C), boost to target +2 °C after 8–10 minutes without warming or a deep temperature drop, brake 0.1 °C ahead and standby at ~19 °C when reached. Keep the indoor unit powered without repeated ON/OFF. +- Day: physically OFF above the configured room minimum until heating is needed; maintain room temperature, then OFF after 30 continuous minutes at goal with compressor minimum-on protection. +- Keeps individual targets for Bedroom, Jan and Igor; includes three ready-to-import two-block Flow definitions. +- Correctly identifies Flow-owned thermostats as automatic, including day OFF/pending OFF and night standby; respects genuine manual control. +- Adds focused Rust unit cases, UI ownership tests and a static release verification script. Rust tests/build still require a Rust toolchain on the CI/deployment host. + ## 0.16.1 - Adds hysteretic early-heating coasting: lower the GREE target before reaching room comfort temperature without switching the indoor unit OFF. diff --git a/ha-addon/repository/gree-controller/README.md b/ha-addon/repository/gree-controller/README.md index e5de729..80a1425 100644 --- a/ha-addon/repository/gree-controller/README.md +++ b/ha-addon/repository/gree-controller/README.md @@ -2,8 +2,6 @@ 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.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) For VLAN deployments, configure VLAN interfaces on the Home Assistant OS host. The add-on uses `host_network: true`; with multiple directly attached subnets, `gree_interface` may remain empty. Run broadcast discovery separately for each VLAN/subnet. diff --git a/ha-addon/repository/gree-controller/config.yaml b/ha-addon/repository/gree-controller/config.yaml index 327f6f6..2e79fa5 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.1" +version: "0.16.3" 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 6104ed0..770fb96 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1036,7 +1036,38 @@ "flow.node.and": "AND", "flow.node.or": "OR", "flow.node.not": "NOT", - "flow.node.thermostat": "Thermostat", + "flow.node.simpleHeat": "Simple day/night heating", + "flow.simpleHeatDay": "Day target", + "flow.simpleHeatNight": "Night target", + "flow.simpleHeatMinimum": "Day minimum temperature", + "flow.simpleHeatBoostMinutes": "Boost after (min)", + "flow.simpleHeatOffMinutes": "Turn off after day stability (min)", + "flow.simpleHeatHint": "Connect “Test constant: Yes” to this block. Night hours are set in Night mode; detailed rules are below.", + "flow.constantDescription": "An always-true/false condition. Set YES so heating runs automatically on each Flow cycle.", + "flow.constantHelp": "YES: the connected action is evaluated on every automation cycle. NO: the action is blocked. Day/night decisions, temperatures and timers are handled by the heating action, so no other conditions are needed.", + "flow.simpleHeatDescription": "One block controls room temperature by day and night: it selects GREE targets, eases off heating and turns the unit OFF during the day.", + "flow.simpleHeatHowItWorks": "How it works — example with these settings", + "flow.simpleHeatHelpIntro": "The targets are room temperatures, not direct air-conditioner setpoints. The block calculates GREE whole-degree settings for the selected zone.", + "flow.simpleHeatHelpNightTitle": "Night — room target {target}; unit stays ON", + "flow.simpleHeatHelpNightSleep": "At {room} or above: GREE {gree} — standby without powering off.", + "flow.simpleHeatHelpNightBrake": "From {room} until just below target: GREE {gree} — early braking.", + "flow.simpleHeatHelpNightGentle": "Below {room}: GREE {gree} — gentle heating (until boost conditions apply).", + "flow.simpleHeatHelpNightBoost": "At {room} or lower, or after {minutes} min without a 0.1°C rise: GREE {gree} — stronger heating.", + "flow.simpleHeatHelpDayTitle": "Day — target {target}, minimum room temperature {minimum}", + "flow.simpleHeatHelpDayOff": "Keep the unit OFF until room temperature falls below {minimum}.", + "flow.simpleHeatHelpDayHeat": "Below the minimum: GREE {gentle}; at {room} or lower, or after {minutes} min without improvement: GREE {boost}.", + "flow.simpleHeatHelpDayStop": "After staying at {target} or above for {minutes} continuous minutes: OFF. A drop below target resets the timer.", + "flow.simpleHeatHelpSensors": "Sensor: follows the zone setting (HA, GREE or combined). If fresh HA data is missing, the zone may use GREE when its sensor is available. In combined mode, a large disagreement can also select GREE.", + "flow.simpleHeatHelpSchedule": "Day/night times: Settings → Night mode. Each room has its own targets in this block.", + "flow.simpleHeatHelpControl": "Shown as automatic Flow control, not manual. Genuine remote/manual commands take priority; compressor protections still apply.", + "flow.simpleHeatDayTip": "Desired daytime room temperature; this is not the direct GREE setpoint.", + "flow.simpleHeatNightTip": "Desired room temperature during the global night-mode window.", + "flow.simpleHeatMinimumTip": "The daytime unit stays OFF above this threshold; below it, rescue heating begins.", + "flow.simpleHeatBoostTip": "How many minutes to try gentle heating before boosting when the room has not warmed by at least 0.1°C.", + "flow.simpleHeatOffTip": "How many uninterrupted minutes the room must remain at or above its daytime target before turning the unit OFF.", + "zones.flowDayOff": "Flow · day · off", + "zones.flowDayPendingOff": "Flow · day · waiting to switch off", + "flow.node.thermostat": "Thermostat", "flow.node.greeDevice": "GREE device", "flow.node.greeFeature": "Single GREE function", "flow.node.group": "Group", diff --git a/lang/pl.json b/lang/pl.json index 2806e69..be2644d 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -1036,7 +1036,38 @@ "flow.node.and": "AND", "flow.node.or": "OR", "flow.node.not": "NOT", - "flow.node.thermostat": "Termostat", + "flow.node.simpleHeat": "Proste grzanie dzień/noc", + "flow.simpleHeatDay": "Cel dzień", + "flow.simpleHeatNight": "Cel noc", + "flow.simpleHeatMinimum": "Minimalna temperatura dzienna", + "flow.simpleHeatBoostMinutes": "Wzmocnienie po (min)", + "flow.simpleHeatOffMinutes": "Wyłącz w dzień po stabilizacji (min)", + "flow.simpleHeatHint": "Połącz „Stała testowa: Tak” z tym blokiem. Godziny nocy ustawisz w „Trybie nocnym”; szczegóły działania są poniżej.", + "flow.constantDescription": "Stały warunek TAK/NIE. Ustaw TAK, aby grzanie działało automatycznie w każdym cyklu Flow.", + "flow.constantHelp": "TAK: podłączona akcja jest oceniana w każdym cyklu automatyki. NIE: akcja nie uruchamia się. Dzień, noc i temperaturę obsługuje blok grzania, więc nie trzeba dodawać innych warunków.", + "flow.simpleHeatDescription": "Jeden blok steruje temperaturą pokoju w dzień i w nocy. Sam dobiera nastawę GREE, hamuje grzanie i wyłącza urządzenie w dzień.", + "flow.simpleHeatHowItWorks": "Jak działa? Przykład dla tych nastaw", + "flow.simpleHeatHelpIntro": "Cele oznaczają temperaturę pokoju, nie nastawę klimatyzatora. Blok wylicza całkowite stopnie zadane GREE z celu danej strefy.", + "flow.simpleHeatHelpNightTitle": "Noc — cel w pokoju {target}; jednostka pozostaje ON", + "flow.simpleHeatHelpNightSleep": "Od {room} wzwyż: GREE {gree} — uśpienie bez wyłączania jednostki.", + "flow.simpleHeatHelpNightBrake": "Od {room} do poniżej celu: GREE {gree} — wyhamowanie.", + "flow.simpleHeatHelpNightGentle": "Poniżej {room}: GREE {gree} — delikatne dogrzewanie (dopóki nie zajdzie warunek wzmocnienia).", + "flow.simpleHeatHelpNightBoost": "Przy {room} lub mniej albo po {minutes} min bez wzrostu o 0,1°C: GREE {gree} — mocniejsze grzanie.", + "flow.simpleHeatHelpDayTitle": "Dzień — cel {target}, minimalna temperatura {minimum}", + "flow.simpleHeatHelpDayOff": "Jednostka OFF, dopóki temperatura nie spadnie poniżej {minimum}.", + "flow.simpleHeatHelpDayHeat": "Po spadku: GREE {gentle}; przy {room} lub mniej albo po {minutes} min bez poprawy: GREE {boost}.", + "flow.simpleHeatHelpDayStop": "Po utrzymaniu co najmniej {target} przez ciągłe {minutes} min: OFF. Spadek poniżej celu zeruje licznik.", + "flow.simpleHeatHelpSensors": "Czujnik: temperatura z ustawień strefy (HA, GREE lub combined). Przy braku świeżego HA strefa może użyć GREE, o ile jednostka udostępnia odczyt. W combined duża rozbieżność może także wybrać GREE.", + "flow.simpleHeatHelpSchedule": "Godziny dnia/nocy: Ustawienia → Tryb nocny. Każdy pokój ma własne cele w tym bloku.", + "flow.simpleHeatHelpControl": "Sterowanie widoczne jako automatyczne Flow, nie ręczne. Rzeczywiste polecenia ręczne/pilot mają pierwszeństwo; zabezpieczenia sprężarki pozostają aktywne.", + "flow.simpleHeatDayTip": "Temperatura docelowa w pokoju w ciągu dnia; nie jest bezpośrednią nastawą GREE.", + "flow.simpleHeatNightTip": "Temperatura docelowa w pokoju w godzinach trybu nocnego.", + "flow.simpleHeatMinimumTip": "Gdy jest cieplej, dzienna jednostka pozostaje OFF. Poniżej minimum rozpocznie dogrzewanie.", + "flow.simpleHeatBoostTip": "Ile minut próbować delikatnego grzania, zanim przełączy na mocniejsze, jeśli pokój nie ogrzeje się o co najmniej 0,1°C.", + "flow.simpleHeatOffTip": "Ile nieprzerwanych minut temperatura ma być na celu lub wyższa, zanim jednostka wyłączy się w dzień.", + "zones.flowDayOff": "Flow · dzień · wyłączony", + "zones.flowDayPendingOff": "Flow · dzień · oczekiwanie na wyłączenie", + "flow.node.thermostat": "Termostat", "flow.node.greeDevice": "Urządzenie GREE", "flow.node.greeFeature": "Pojedyncza funkcja GREE", "flow.node.group": "Grupa", diff --git a/scripts/README.md b/scripts/README.md index bd52a6b..6fbdf50 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -18,3 +18,7 @@ All operator-facing scripts live in this directory. - `network-debug.sh [broadcast-ip]` — prints interface/routing diagnostics and a tcpdump command. The running controller uses only the committed `web/styles.css` embedded into the Rust binary. There is no frontend dependency installation, CDN, or CSS build step. + +- `verify_simple_heating.py` — static validation for the three simple-heating Flow imports, language files and release versions. +- `test_simple_heating_ui.js` — checks automatic Flow vs manual thermostat statuses. +- `test_flow_block_help_ui.js` — checks the localized two-block descriptions, dynamically calculated day/night heating examples and the Flow inspector help panel. diff --git a/scripts/api_dev_test.py b/scripts/api_dev_test.py index 029d449..fbba7b4 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.1. +"""Developer smoke/integration tests for GREE Controller API 0.16.2. 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.1" +DEFAULT_EXPECTED_VERSION = "0.16.2" 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.1 contract", lambda: test_openapi(client, args.expected_version)) + runner.run("OpenAPI 0.16.2 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/scripts/test_flow_block_help_ui.js b/scripts/test_flow_block_help_ui.js new file mode 100644 index 0000000..36db70a --- /dev/null +++ b/scripts/test_flow_block_help_ui.js @@ -0,0 +1,95 @@ +// Verify contextual help for the constant and room heating Flow blocks. +// Does not need a browser or a running HVAC controller. +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const vm = require('node:vm'); +const path = require('node:path'); +const root = path.resolve(__dirname, '..'); +const translations = {}; +for (const language of ['pl', 'en']) { + translations[language] = JSON.parse(fs.readFileSync(path.join(root, 'lang', `${language}.json`), 'utf8')).translations; +} +function localize(language, key, params = {}) { + const result = translations[language][key]; + assert.ok(result, `missing translation ${language}:${key}`); + return String(result).replace(/\{([a-zA-Z0-9_]+)\}/g, (_, name) => params[name] ?? `{${name}}`); +} +const app = { + zones: [{ id: 'zone1', name: 'Pokój' }], + devices: [], groups: [], flowDraft: null, flowSelectedNodeId: null, +}; +const host = { + innerHTML: '', + expandedNode: null, + classList: { toggle() {}, remove() {} }, + querySelector(selector) { + return selector === '.flow-block-help' && this.expandedNode + ? { open: true, dataset: { flowHelpNode: this.expandedNode } } : null; + }, +}; +const ctx = vm.createContext({ + app, + tr: (key, params) => localize(ctx.language, key, params), + language: 'pl', + esc: value => String(value).replace(/&/g, '&').replace(/ `${Number(value).toFixed(1)}°C`, + uiIcon: () => '', + $: selector => selector === '#flowInspector' ? host : null, + document: { addEventListener() {} }, window: { addEventListener() {} }, +}); +vm.runInContext(fs.readFileSync(path.join(root, 'web/js-dynamic/flows.js'), 'utf8'), ctx); +const meta = vm.runInContext('FLOW_NODE_META', ctx); +assert.equal(meta.constant.descriptionKey, 'flow.constantDescription'); +assert.equal(meta.zone_heat_cycle.descriptionKey, 'flow.simpleHeatDescription'); + +for (const language of ['pl', 'en']) { + ctx.language = language; + for (const file of fs.readdirSync(path.join(root, 'flows/simple-heating'))) { + if (!file.endsWith('.flow.json')) continue; + const json = JSON.parse(fs.readFileSync(path.join(root, 'flows/simple-heating', file), 'utf8')); + const setting = json.flow.nodes.find(node => node.kind === 'zone_heat_cycle').config; + const text = ctx.flowSimpleHeatHelp(setting, 'room', false); + const night = setting.night_target; + assert.ok(text.includes(`${night.toFixed(1)}°C`), `${language} ${file} missing room target`); + assert.ok(text.includes(`${(night - 0.8).toFixed(1)}°C`), `${language} ${file} missing boost threshold`); + assert.ok(text.includes(`${Math.floor(night - 1).toFixed(1)}°C`), `${language} ${file} missing standby`); + assert.ok(text.includes(`${Math.floor(night) + 1}.0°C`), `${language} ${file} missing gentle heat`); + assert.ok(text.includes(`${Math.ceil(night + 2)}.0°C`), `${language} ${file} missing boost`); + assert.ok(text.includes(`${setting.day_min.toFixed(1)}°C`), `${language} ${file} missing daytime minimum`); + assert.ok(text.includes('21.0°C') && text.includes('22.0°C'), `${language} ${file} missing daytime setpoints`); + assert.ok(text.includes('19.2°C'), `${language} ${file} missing daytime immediate boost threshold`); + assert.ok(text.includes(String(setting.day_off_after_minutes)), `${language} ${file} missing off timer`); + assert.ok(text.includes(String(setting.boost_after_minutes)), `${language} ${file} missing boost timer`); + assert.ok(text.includes(localize(language, 'flow.simpleHeatHelpSensors'))); + } + assert.ok(ctx.flowSimpleHeatHelp({ night_target: 22.4, day_target: 21.3, day_min: 20.6, boost_after_minutes: 12, day_off_after_minutes: 45 }).includes('25.0°C'), 'help must recalculate for edited targets'); +} +ctx.language = 'pl'; +function inspect(kind, config) { + app.flowDraft = { nodes: [{ id: 'selected', kind, config }], edges: [] }; + app.flowSelectedNodeId = 'selected'; + ctx.renderFlowInspector(); + return host.innerHTML; +} +const constantHtml = inspect('constant', { value: true }); +assert.ok(constantHtml.includes(localize('pl', 'flow.constantDescription'))); +assert.ok(constantHtml.includes(localize('pl', 'flow.constantHelp'))); +assert.ok(constantHtml.includes('data-flow-config="value"')); +const heat = { zone_id: 'zone1', day_target: 20, night_target: 20.6, day_min: 19.7, boost_after_minutes: 10, day_off_after_minutes: 30 }; +const simpleHtml = inspect('zone_heat_cycle', heat); +assert.ok(simpleHtml.includes(localize('pl', 'flow.simpleHeatDescription'))); +assert.ok(simpleHtml.includes('
'), 'expanded help should stay open when a user edits settings'); +const htmlFile = fs.readFileSync(path.join(root, 'web/index.html'), 'utf8'); +assert.ok(htmlFile.includes('data-i18n-title="flow.constantDescription"')); +assert.ok(htmlFile.includes('data-i18n-title="flow.simpleHeatDescription"')); +console.log('OK: 2 blocks, 3 rooms, PL/EN tips, dynamic temperature examples, inspector and expanded-state behavior'); +if (process.env.FLOW_HELP_PREVIEW_HTML) { + host.expandedNode = 'selected'; + const content = inspect('zone_heat_cycle', heat); + fs.writeFileSync(process.env.FLOW_HELP_PREVIEW_HTML, ``); +} diff --git a/scripts/test_simple_heating_ui.js b/scripts/test_simple_heating_ui.js new file mode 100644 index 0000000..0edc52c --- /dev/null +++ b/scripts/test_simple_heating_ui.js @@ -0,0 +1,29 @@ +// Focused UI behavior checks, with no browser or controller required. +const fs = require('fs'); +const vm = require('vm'); +const ctx = vm.createContext({ + tr: key => key, + app: { zones: [], devices: [], groups: [] }, +}); +vm.runInContext(fs.readFileSync('web/js-dynamic/entities.js', 'utf8'), ctx); +const zone = { + enabled: true, + control_owner: 'automation', + control_source: 'automation.flow', + simple_heat: { day_target: 20, night_target: 20.6 }, + current_temperature: 20.2, + demand: false, +}; +function check(fn, expected, ...args) { + const value = ctx[fn](...args); + if (value !== expected) throw new Error(`${fn}: ${value} != ${expected}`); +} +check('zoneControlOwnerLabel', 'zones.ownerFlow', zone); +check('zoneRuntimeStatusLabel', 'zones.flowDayOff', zone, 'off', { power: false }); +check('zoneRuntimeStatusLabel', 'zones.flowDayPendingOff', zone, 'off', { power: true }); +check('zoneRuntimeStatusLabel', 'zones.flowStandby', zone, 'heat', { power: true, enabled: true, online: true }); +check('zoneRuntimeStatusLabel', 'zones.flowHeating', { ...zone, demand: true }, 'heat', { power: true, enabled: true, online: true, mode: 'heat' }); +check('zoneRuntimeStatusLabel', 'zones.flowCoasting', { ...zone, heat_coasting: true }, 'heat', { power: true, enabled: true, online: true }); +check('zoneRuntimeStatusLabel', 'zones.manualDeviceControl', { ...zone, device_manual_override: true }, 'heat', { power: true }); +check('zoneControlOwnerLabel', 'zones.ownerExternal', { ...zone, control_owner: 'direct_manual', control_source: 'external' }); +console.log('OK: 8 UI Flow/manual status scenarios'); diff --git a/scripts/verify_simple_heating.py b/scripts/verify_simple_heating.py new file mode 100644 index 0000000..2a394c8 --- /dev/null +++ b/scripts/verify_simple_heating.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Static release checks for the two-node Simple day/night heating Flows. + +Does not replace `cargo test` or hardware verification. +""" +from __future__ import annotations + +import json +import subprocess +import sys +import tomllib +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PROFILE_DIR = ROOT / "flows" / "simple-heating" +ROOMS = { + "sypialnia": ("9a0bb37d-0306-4f87-9b14-fabf3b4c6164", 20.5, 10), + "jan-pieczara": ("7feae66a-747e-4205-8cff-91cc0b7924f6", 20.8, 8), + "igor": ("dd451f3e-4fc0-4189-b1cf-0c814b65cefc", 20.8, 10), +} +TRANSLATIONS = [ + "flow.node.simpleHeat", "flow.simpleHeatDay", "flow.simpleHeatNight", + "flow.simpleHeatMinimum", "flow.simpleHeatBoostMinutes", + "flow.simpleHeatOffMinutes", "flow.simpleHeatHint", "zones.flowDayOff", "zones.flowDayPendingOff", + "flow.constantDescription", "flow.constantHelp", "flow.simpleHeatDescription", "flow.simpleHeatHowItWorks", + "flow.simpleHeatHelpIntro", "flow.simpleHeatHelpNightTitle", "flow.simpleHeatHelpNightSleep", + "flow.simpleHeatHelpNightBrake", "flow.simpleHeatHelpNightGentle", "flow.simpleHeatHelpNightBoost", + "flow.simpleHeatHelpDayTitle", "flow.simpleHeatHelpDayOff", "flow.simpleHeatHelpDayHeat", + "flow.simpleHeatHelpDayStop", "flow.simpleHeatHelpSensors", "flow.simpleHeatHelpSchedule", + "flow.simpleHeatHelpControl", "flow.simpleHeatDayTip", "flow.simpleHeatNightTip", + "flow.simpleHeatMinimumTip", "flow.simpleHeatBoostTip", "flow.simpleHeatOffTip", +] + + +def main() -> None: + version = tomllib.loads((ROOT / "Cargo.toml").read_text())["package"]["version"] + for name, (zone_id, target, boost) in ROOMS.items(): + path = PROFILE_DIR / f"{name}-simple-heat-v5.flow.json" + doc = json.loads(path.read_text()) + assert doc["format"] == "gree-controller-flow" and doc["version"] == 1, name + graph = doc["flow"] + assert graph["enabled"] and not graph["draft"], name + assert len(graph["nodes"]) == 2 and len(graph["edges"]) == 1, name + conditions = [node for node in graph["nodes"] if node["kind"] == "constant"] + actions = [node for node in graph["nodes"] if node["kind"] == "zone_heat_cycle"] + assert len(conditions) == len(actions) == 1, name + assert conditions[0]["config"]["value"] is True, name + assert graph["edges"][0]["from"] == conditions[0]["id"], name + assert graph["edges"][0]["to"] == actions[0]["id"], name + conf = actions[0]["config"] + assert conf["zone_id"] == zone_id, name + assert conf["night_target"] == target, name + assert conf["day_target"] == 20 and conf["day_min"] == 19.7, name + assert conf["boost_after_minutes"] == boost, name + assert conf["day_off_after_minutes"] == 30, name + assert "Wymaga wersji aplikacji 0.16.2" in graph["description"], name + + for lang in ("pl", "en"): + translations = json.loads((ROOT / "lang" / f"{lang}.json").read_text())["translations"] + assert all(translations.get(key) for key in TRANSLATIONS), lang + + # Validate every distributable JSON input, not only the three new Flow exports. + json_files = list(ROOT.rglob("*.json")) + for path in json_files: + json.loads(path.read_text()) + assert len(json_files) > 30, len(json_files) + + js_files = list((ROOT / "web").rglob("*.js")) + for path in js_files: + subprocess.run(["node", "--check", str(path)], check=True, capture_output=True, text=True) + + rust = (ROOT / "src" / "api" / "flows.rs").read_text() + engine = (ROOT / "src" / "engine" / "zone_control.rs").read_text() + auto = (ROOT / "src" / "engine" / "automations.rs").read_text() + status = (ROOT / "web" / "js-dynamic" / "entities.js").read_text() + ui = (ROOT / "web" / "js-dynamic" / "flows.js").read_text() + html = (ROOT / "web" / "index.html").read_text() + for value in ("zone_heat_cycle", "boost_after_minutes", "day_off_after_minutes"): + assert value in rust and value in ui, value + assert "simple_heat_step(" in engine and "action_zone_simple_heat" in auto + assert "zone.simple_heat" in engine and "enabled_heat_actions" in engine + assert "automation.flow" in status and "zones.flowDayOff" in status + assert 'data-flow-add="zone_heat_cycle"' in html + assert "day_target_20_starts_at_21_then_boosts_to_22" in (ROOT / "src" / "engine" / "simple_heat.rs").read_text() + + lock = (ROOT / "Cargo.lock").read_text() + assert f'name = "gree-controller"\nversion = "{version}"' in lock + for manifest in [ + ROOT / "ha-addon" / "repository" / "gree-controller" / "config.yaml", + ROOT / "ha-addon" / "home-assistant" / "custom_components" / "gree_controller" / "manifest.json", + ]: + assert version in manifest.read_text(), manifest + assert json.loads((ROOT / "docs" / "openapi.json").read_text())["info"]["version"] == version + + print(f"OK: {len(ROOMS)} Flow imports, {len(json_files)} JSON files, {len(js_files)} JS scripts, translations, linked controller source, version {version}") + + +if __name__ == "__main__": + try: + main() + except (AssertionError, subprocess.CalledProcessError) as exc: + print(f"FAIL: {exc}", file=sys.stderr) + raise diff --git a/src/api.rs b/src/api.rs index dae44d8..a474ed8 100644 --- a/src/api.rs +++ b/src/api.rs @@ -12,7 +12,7 @@ use crate::{ InfluxDbSettingsUpdate, InfluxDbSettingsView, LocalDiscoveryCandidate, ManualDeviceRequest, NetworkReading, NightModeSettings, NotificationSettings, NotificationSettingsUpdate, NotificationSettingsView, Reading, RuntimeSettings, Schedule, SettingsSnapshot, - TemporaryQuickThermostat, TemporaryQuickThermostatRequest, Zone, ZoneControlPatch, + SimpleHeatRuntime, TemporaryQuickThermostat, TemporaryQuickThermostatRequest, Zone, ZoneControlPatch, ZoneReading, }, notifications, diff --git a/src/api/automations.rs b/src/api/automations.rs index b3eeaea..97f0c50 100644 --- a/src/api/automations.rs +++ b/src/api/automations.rs @@ -164,6 +164,7 @@ impl AutomationInput { last_trigger_trace: vec![], action_zone_id: None, action_zone_preset: None, + action_zone_simple_heat: None, action_zone_heat_anticipation_c: None, action_zone_standby_offset_c: None, action_ha_domain: None, diff --git a/src/api/flows.rs b/src/api/flows.rs index b835d4b..4525667 100644 --- a/src/api/flows.rs +++ b/src/api/flows.rs @@ -269,6 +269,7 @@ fn flow_action_kind(kind: &str) -> bool { matches!( kind, "zone_thermostat" + | "zone_heat_cycle" | "device_action" | "device_feature_action" | "group_action" @@ -1563,6 +1564,7 @@ fn compile_flow( last_trigger_trace, action_zone_id: None, action_zone_preset: None, + action_zone_simple_heat: None, action_zone_heat_anticipation_c: None, action_zone_standby_offset_c: None, action_ha_domain: None, @@ -1586,6 +1588,38 @@ fn compile_flow( item.cooldown_seconds = cooldown.max(30); } match action_node.kind.as_str() { + "zone_heat_cycle" => { + if value_binding.is_some() { + return Err(AppError::BadRequest("simple heat does not accept a dynamic value connection".into())); + } + let zone_id = flow_string(&action_node.config, "zone_id") + .ok_or_else(|| AppError::BadRequest("simple heat needs a zone".into()))?; + if !zones.iter().any(|zone| zone.id == zone_id) { + return Err(AppError::BadRequest("simple heat references a missing zone".into())); + } + let day_target = flow_f64(&action_node.config, "day_target") + .ok_or_else(|| AppError::BadRequest("simple heat requires a day target".into()))?; + let night_target = flow_f64(&action_node.config, "night_target") + .ok_or_else(|| AppError::BadRequest("simple heat requires a night target".into()))?; + let day_min = flow_f64(&action_node.config, "day_min") + .ok_or_else(|| AppError::BadRequest("simple heat requires a daytime minimum".into()))?; + let boost_after_minutes = action_node.config.get("boost_after_minutes") + .and_then(Value::as_u64).unwrap_or(10); + let day_off_after_minutes = action_node.config.get("day_off_after_minutes") + .and_then(Value::as_u64).unwrap_or(30); + if ![day_target, night_target, day_min].iter().all(|v| v.is_finite() && (17.0..=28.0).contains(v)) + || day_min >= day_target || !(3..=30).contains(&boost_after_minutes) + || !(10..=120).contains(&day_off_after_minutes) { + return Err(AppError::BadRequest("invalid simple heat targets or timing (day minimum must be below day target)".into())); + } + item.action_zone_id = Some(zone_id); + item.action_zone_simple_heat = Some(crate::models::SimpleHeatSettings { + flow_id: flow.id.clone(), flow_node_id: action_node.id.clone(), + day_target, night_target, day_min, boost_after_minutes, + day_off_after_minutes, + }); + item.action.mode = Some("heat".into()); + } "zone_thermostat" => { let zone_id = flow_string(&action_node.config, "zone_id") .ok_or_else(|| AppError::BadRequest("thermostat block needs a zone".into()))?; @@ -2130,7 +2164,7 @@ fn dry_run_block_reason( value_binding: Option<&(String, String)>, ) -> Result, AppError> { match action.kind.as_str() { - "zone_thermostat" => { + "zone_thermostat" | "zone_heat_cycle" => { let Some(zone_id) = flow_string(&action.config, "zone_id") else { return Ok(Some("missing_zone".into())); }; diff --git a/src/api/zones.rs b/src/api/zones.rs index 46d93cd..2a7ffc3 100644 --- a/src/api/zones.rs +++ b/src/api/zones.rs @@ -214,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, + simple_heat: None, + simple_heat_runtime: SimpleHeatRuntime::default(), heat_anticipation_c: self.heat_anticipation_c, heat_coasting: false, standby_offset_c: self.standby_offset_c, @@ -382,6 +384,8 @@ async fn update_zone( zone.current_temperature = existing.current_temperature; zone.control_temperature_source = existing.control_temperature_source; zone.active_preset = existing.active_preset; + zone.simple_heat = existing.simple_heat.clone(); + zone.simple_heat_runtime = existing.simple_heat_runtime.clone(); zone.manual_preset = existing.manual_preset; zone.manual_setpoint = existing.manual_setpoint; zone.manual_override_until = existing.manual_override_until; diff --git a/src/engine.rs b/src/engine.rs index c1a34cc..a4d62df 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -6,7 +6,7 @@ use crate::{ ControlPlan, ControlPlanEvent, Device, DeviceCommand, DeviceGroup, EnergyReading, HORIZONTAL_SWING_MAX, VERTICAL_SWING_MAX, EnergySourcePreference, GroupControlPatch, HaReading, NetworkReading, NightModeSettings, - Reading, RuntimeSettings, Schedule, TemporaryQuickThermostat, Zone, ZoneControlPlan, + Reading, RuntimeSettings, Schedule, SimpleHeatRuntime, SimpleHeatSettings, TemporaryQuickThermostat, Zone, ZoneControlPlan, ZoneReading, }, state::{AppState, ControlPlanSnapshot, PendingControllerCommand}, @@ -31,6 +31,7 @@ include!("engine/deadlines.rs"); include!("engine/ownership.rs"); include!("engine/groups.rs"); include!("engine/zone_actions.rs"); +include!("engine/simple_heat.rs"); include!("engine/zone_control.rs"); include!("engine/history.rs"); include!("engine/connectivity.rs"); diff --git a/src/engine/automations.rs b/src/engine/automations.rs index cbbbc3a..0ec6ab4 100644 --- a/src/engine/automations.rs +++ b/src/engine/automations.rs @@ -33,7 +33,12 @@ fn flow_zone_action_already_satisfied( action: &DeviceCommand, heat_anticipation_c: Option, standby_offset_c: Option, + simple_heat: Option<&SimpleHeatSettings>, ) -> bool { + if let Some(profile) = simple_heat { + return zone.simple_heat.as_ref() == Some(profile) + && zone.control_source == "automation.flow"; + } 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) { @@ -202,6 +207,7 @@ async fn run_automations(state: &AppState) -> Result<()> { &effective_action, item.action_zone_heat_anticipation_c, item.action_zone_standby_offset_c, + item.action_zone_simple_heat.as_ref(), ) { let before_runtime = item.flow_runtime.clone(); flow_acknowledge_change_gates(&mut item); @@ -289,6 +295,7 @@ async fn run_automations(state: &AppState) -> Result<()> { 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, + item.action_zone_simple_heat.as_ref(), ).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() }); @@ -1120,6 +1127,7 @@ fn flow_condition_kind_runtime(kind: &str) -> bool { async fn apply_flow_zone_action( state: &AppState, zone_id: &str, preset: Option<&str>, action: &DeviceCommand, heat_anticipation_c: Option, standby_offset_c: Option, + simple_heat: Option<&SimpleHeatSettings>, ) -> Result { let _schedule_guard = state.lock_schedule_operation().await; // Match the canonical thermostat ordering used by Web/HA zone control. This prevents @@ -1131,13 +1139,29 @@ async fn apply_flow_zone_action( // A disabled Zone is an administrative gate for every Flow thermostat action. // Flow power commands control only the assigned unit runtime; they never change zone.enabled. if !zone.enabled { return Ok(false); } + // Explicit manual target wins until Auto is selected again. A previous Flow v4 + // may have left a custom preset behind: that is *automation*, not manual, + // and must be replaceable by the simplified Flow without user intervention. + if simple_heat.is_some() && zone.control_source != "automation.flow" + && (zone.manual_preset.is_some() || zone.manual_setpoint.is_some()) { + return Ok(false); + } let device_id = zone.device_id.clone(); + if let Some(profile) = simple_heat { + if zone.simple_heat.as_ref() != Some(profile) { + zone.simple_heat_runtime = SimpleHeatRuntime::default(); + } + zone.simple_heat = Some(profile.clone()); + } else { + zone.simple_heat = None; + zone.simple_heat_runtime = SimpleHeatRuntime::default(); + } 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())) } } - match preset.unwrap_or("auto") { + match if simple_heat.is_some() { "auto" } else { preset.unwrap_or("auto") } { "auto" => { zone.manual_preset = None; zone.manual_setpoint = None; zone.manual_override_until = None; } "custom" => { let target = action.target_temperature.ok_or_else(|| AppError::BadRequest("Flow custom thermostat action has no target".into()))?; @@ -1163,7 +1187,17 @@ async fn apply_flow_zone_action( zone.control_source = "automation.flow".into(); zone.control_reason = "Visual Flow automation".into(); let schedules = state.db.list_schedules()?; - let house_mode = state.settings.read().await.house_mode.clone(); + let runtime_settings = state.settings.read().await; + let house_mode = runtime_settings.house_mode.clone(); + if simple_heat.is_some() { + // Show the correct bedroom/child-room night target immediately after + // Flow activation, not the old Comfort preset until the next zone tick. + zone.simple_heat_runtime.night = Some(night_mode_active( + &runtime_settings.night_mode, + Local::now().time(), + )); + } + drop(runtime_settings); refresh_control_ownership(&mut zone); if zone.control_owner == "automation" { zone.control_source = "automation.flow".into(); diff --git a/src/engine/control_plan.rs b/src/engine/control_plan.rs index edd4aa1..20908a0 100644 --- a/src/engine/control_plan.rs +++ b/src/engine/control_plan.rs @@ -151,16 +151,25 @@ pub async fn build_control_plan(state: &AppState) -> Result Result, +) -> SimpleHeatDecision { + if runtime.night != Some(night) { + runtime.night = Some(night); + runtime.day_heating = false; + runtime.day_stable_since = None; + runtime.gentle_since = None; + runtime.gentle_start_temp = None; + } + let target = if night { profile.night_target } else { profile.day_target }; + let off = SimpleHeatDecision { + power: false, + device_target: 0.0, + demand: false, + coasting: false, + }; + + // Day: stay physically OFF until the permitted minimum is breached. After a + // daytime rescue, require 30 consecutive minutes at target before switching OFF. + if !night { + if !runtime.day_heating { + if room >= profile.day_min { + return off; + } + runtime.day_heating = true; + } + if room >= target { + let since = runtime.day_stable_since.get_or_insert(now); + if now.signed_duration_since(*since).num_seconds() + >= (profile.day_off_after_minutes as i64) * 60 + { + runtime.day_heating = false; + runtime.day_stable_since = None; + runtime.gentle_since = None; + runtime.gentle_start_temp = None; + return off; + } + } else { + runtime.day_stable_since = None; + } + } + + // Full comfort: 19 C at 20.5/20.6 C room target. Early braking begins + // 0.1 C before the room target; no repeated GREE power cycling at night. + if room >= target { + runtime.gentle_since = None; + runtime.gentle_start_temp = None; + return SimpleHeatDecision { + power: true, + device_target: (target - 1.0).floor().clamp(16.0, 30.0), + demand: false, + coasting: false, + }; + } + if room + 0.00001 >= target - 0.1 { + runtime.gentle_since = None; + runtime.gentle_start_temp = None; + return SimpleHeatDecision { + power: true, + device_target: (target - 0.5).floor().clamp(16.0, 30.0), + demand: false, + coasting: true, + }; + } + + // At the hard lower point (target - 0.8 C), boost immediately by 2 C. + // Above that, start mildly at floor(target)+1, typically 21 C; escalate to + // ceil(target+2), typically 23 C, if the room does not rise by 0.1 C + // during the configured trial period. + let severe = room <= target - 0.8; + let boost = if severe { + runtime.gentle_since = None; + runtime.gentle_start_temp = None; + true + } else { + if runtime.gentle_start_temp.is_none() + || runtime + .gentle_start_temp + .is_some_and(|previous| room >= previous + 0.1 - 0.00001) + { + runtime.gentle_since = Some(now); + runtime.gentle_start_temp = Some(room); + } + runtime.gentle_since.is_some_and(|since| { + now.signed_duration_since(since).num_seconds() + >= (profile.boost_after_minutes as i64) * 60 + }) + }; + SimpleHeatDecision { + power: true, + device_target: (if boost { (target + 2.0).ceil() } else { target.floor() + 1.0 }) + .clamp(16.0, 30.0), + demand: true, + coasting: false, + } +} + +#[cfg(test)] +mod simple_heat_tests { + use super::*; + fn profile() -> SimpleHeatSettings { + SimpleHeatSettings { + flow_id: "flow".into(), flow_node_id: "action".into(), + day_target: 20.0, night_target: 20.6, day_min: 19.7, + boost_after_minutes: 10, day_off_after_minutes: 30, + } + } + + #[test] + fn night_soft_brake_and_sleep() { + let mut runtime = SimpleHeatRuntime::default(); + let now = Utc::now(); + let p = profile(); + assert_eq!(simple_heat_step(&p, &mut runtime, 19.7, true, now).device_target, 23.0); + assert_eq!(simple_heat_step(&p, &mut runtime, 20.2, true, now).device_target, 21.0); + assert_eq!(simple_heat_step(&p, &mut runtime, 20.5, true, now).device_target, 20.0); + let standby = simple_heat_step(&p, &mut runtime, 20.6, true, now); + assert_eq!(standby.device_target, 19.0); + assert!(standby.power); + assert!(!standby.demand); + } + + #[test] + fn boost_after_10_minutes_without_room_rise() { + let mut runtime = SimpleHeatRuntime::default(); + let now = Utc::now(); + let p = profile(); + assert_eq!(simple_heat_step(&p, &mut runtime, 20.2, true, now).device_target, 21.0); + assert_eq!(simple_heat_step(&p, &mut runtime, 20.2, true, now + chrono::Duration::minutes(9)).device_target, 21.0); + assert_eq!(simple_heat_step(&p, &mut runtime, 20.2, true, now + chrono::Duration::minutes(10)).device_target, 23.0); + assert_eq!(simple_heat_step(&p, &mut runtime, 20.3, true, now + chrono::Duration::minutes(11)).device_target, 21.0); + } + + #[test] + fn day_off_until_limit_and_after_30_stable_minutes() { + let mut runtime = SimpleHeatRuntime::default(); + let now = Utc::now(); + let p = profile(); + assert!(!simple_heat_step(&p, &mut runtime, 19.8, false, now).power); + assert!(simple_heat_step(&p, &mut runtime, 19.6, false, now).power); + assert!(simple_heat_step(&p, &mut runtime, 20.0, false, now + chrono::Duration::minutes(2)).power); + assert!(simple_heat_step(&p, &mut runtime, 20.0, false, now + chrono::Duration::minutes(31)).power); + assert!(!simple_heat_step(&p, &mut runtime, 20.0, false, now + chrono::Duration::minutes(32)).power); + assert!(!simple_heat_step(&p, &mut runtime, 19.8, false, now + chrono::Duration::minutes(33)).power); + assert!(simple_heat_step(&p, &mut runtime, 19.6, false, now + chrono::Duration::minutes(34)).power); + } + + #[test] + fn falling_below_target_resets_day_stability() { + let mut runtime = SimpleHeatRuntime::default(); + let now = Utc::now(); + let p = profile(); + simple_heat_step(&p, &mut runtime, 19.6, false, now); + simple_heat_step(&p, &mut runtime, 20.0, false, now + chrono::Duration::minutes(1)); + simple_heat_step(&p, &mut runtime, 19.9, false, now + chrono::Duration::minutes(29)); + assert!(simple_heat_step(&p, &mut runtime, 20.0, false, now + chrono::Duration::minutes(30)).power); + assert!(simple_heat_step(&p, &mut runtime, 20.0, false, now + chrono::Duration::minutes(59)).power); + assert!(!simple_heat_step(&p, &mut runtime, 20.0, false, now + chrono::Duration::minutes(60)).power); + } + + #[test] + fn day_target_20_starts_at_21_then_boosts_to_22() { + let mut runtime = SimpleHeatRuntime::default(); + let now = Utc::now(); + let p = profile(); + assert_eq!(simple_heat_step(&p, &mut runtime, 19.6, false, now).device_target, 21.0); + assert_eq!(simple_heat_step(&p, &mut runtime, 19.6, false, now + chrono::Duration::minutes(10)).device_target, 22.0); + } + + #[test] + fn switching_night_to_day_resets_day_heat() { + let mut runtime = SimpleHeatRuntime::default(); + let now = Utc::now(); + let p = profile(); + assert!(simple_heat_step(&p, &mut runtime, 20.6, true, now).power); + assert!(!simple_heat_step(&p, &mut runtime, 20.0, false, now).power); + assert!(simple_heat_step(&p, &mut runtime, 19.6, false, now).power); + assert!(simple_heat_step(&p, &mut runtime, 19.6, true, now).power); + } +} diff --git a/src/engine/targets.rs b/src/engine/targets.rs index 13c5708..7f596bc 100644 --- a/src/engine/targets.rs +++ b/src/engine/targets.rs @@ -52,6 +52,7 @@ fn zone_has_thermostat_intent_source( ) -> bool { zone.local_thermostat_power == Some(true) || temporary_quick_thermostat_is_active(zone, now) + || (zone.simple_heat.is_some() && zone.control_source == "automation.flow") || zone.manual_preset.is_some() || zone.manual_setpoint.is_some() || zone.control_source.starts_with("group:") @@ -109,6 +110,23 @@ pub fn refresh_zone_runtime_target(zone: &mut Zone, schedules: &[Schedule], hous return; } + // The simplified Flow profile is an automatic thermostat target, never a + // manual/custom preset. Refresh it immediately for the UI, even before + // the next polling cycle; night/day is re-evaluated by the zone controller. + if let Some(profile) = zone.simple_heat.as_ref().filter(|_| { + zone.control_source == "automation.flow" + && zone.manual_preset.is_none() && zone.manual_setpoint.is_none() + }) { + zone.effective_mode = configured_mode; + zone.active_preset = "auto".into(); + zone.effective_setpoint = Some(if zone.simple_heat_runtime.night.unwrap_or(false) { + profile.night_target + } else { + profile.day_target + }); + return; + } + // Resolve the target while `target_mode` may still borrow `configured_mode`. // After this call the borrow ends, so `configured_mode` can be moved into the runtime state. let (preset, target) = resolve_zone_target(zone, schedule, target_mode); diff --git a/src/engine/tests.rs b/src/engine/tests.rs index cd2a25d..4eff40a 100644 --- a/src/engine/tests.rs +++ b/src/engine/tests.rs @@ -566,6 +566,7 @@ mod tests { last_trigger_trace: vec![], action_zone_id: None, action_zone_preset: None, + action_zone_simple_heat: None, action_zone_heat_anticipation_c: None, action_zone_standby_offset_c: None, action_ha_domain: None, @@ -610,6 +611,8 @@ mod tests { min_on_seconds: 180, min_off_seconds: 180, min_adjust_seconds: 120, + simple_heat: None, + simple_heat_runtime: SimpleHeatRuntime::default(), heat_anticipation_c: 0.0, heat_coasting: false, standby_offset_c: 2.0, diff --git a/src/engine/zone_control.rs b/src/engine/zone_control.rs index ea59c78..06fc622 100644 --- a/src/engine/zone_control.rs +++ b/src/engine/zone_control.rs @@ -541,6 +541,17 @@ async fn control_zones(state: &AppState) -> Result<()> { } let schedules = state.db.list_schedules()?; let mut zone_snapshot = state.db.list_zones()?; + // Only an enabled, compiled Flow action is allowed to retain simple heating + // ownership. Disabling, deleting or recompiling a Flow immediately removes + // stale heating profiles in the next control cycle. + let enabled_heat_actions: HashSet<(String, String, String)> = state.db.list_automations()? + .into_iter() + .filter(|item| item.enabled) + .filter_map(|item| { + let profile = item.action_zone_simple_heat?; + Some((profile.flow_id, profile.flow_node_id, item.action_zone_id?)) + }) + .collect(); // Local/temporary thermostat ownership has its own deadlines. Expire and activate sessions independently. expire_local_thermostat_overrides(state, &mut zone_snapshot, &schedules, &settings.house_mode) .await?; @@ -576,6 +587,16 @@ async fn control_zones(state: &AppState) -> Result<()> { continue; }; let cycle_started_at = zone.updated_at.clone(); + if zone.simple_heat.as_ref().is_some_and(|profile| { + !enabled_heat_actions.contains(&(profile.flow_id.clone(), profile.flow_node_id.clone(), zone.id.clone())) + }) { + zone.simple_heat = None; + zone.simple_heat_runtime = SimpleHeatRuntime::default(); + if zone.control_source == "automation.flow" { + zone.control_source = "automation".into(); + zone.control_reason = "Simple heating Flow disabled; thermostat idle".into(); + } + } let outdoor_temperature = zone_outdoor_sensor_results .remove(&zone.id) .flatten() @@ -784,7 +805,15 @@ async fn control_zones(state: &AppState) -> Result<()> { continue; } - let (preset, target) = resolve_zone_target(&zone, active_schedule, effective_mode); + let simple_profile = zone.simple_heat.clone().filter(|_| { + zone.control_owner == "automation" && zone.control_source == "automation.flow" + && zone.manual_preset.is_none() && zone.manual_setpoint.is_none() + }); + let (preset, target) = if let Some(profile) = simple_profile.as_ref() { + ("auto".into(), if night_active { profile.night_target } else { profile.day_target }) + } else { + resolve_zone_target(&zone, active_schedule, effective_mode) + }; zone.active_preset = preset; zone.effective_setpoint = Some(target); @@ -802,17 +831,53 @@ async fn control_zones(state: &AppState) -> Result<()> { continue; }; + let simple_decision = simple_profile.as_ref().map(|profile| { + simple_heat_step(profile, &mut zone.simple_heat_runtime, temp, night_active, Utc::now()) + }); + if simple_decision.is_some_and(|decision| !decision.power) { + zone.heat_coasting = false; + zone.demand = false; + zone.demand_since = None; + zone.effective_mode = "off".into(); + zone.device_setpoint = if device.power { Some(device.target_temperature) } else { None }; + clear_compressor_pending(&mut zone, true); + if device.power && device.online && device.communication_failures == 0 { + let protection = chrono::Duration::seconds(zone.min_on_seconds.max(settings.compressor_protection_seconds).max(30) as i64); + let safe_to_switch_off = !settings.compressor_protection_enabled + || zone.last_power_change_at.is_none_or(|at| Utc::now().signed_duration_since(at) >= protection); + if safe_to_switch_off { + match send_zone_command_if_owned(state, &zone.id, &zone.device_id, DeviceCommand { + power: Some(false), mode: Some("heat".into()), ..Default::default() + }).await { + Ok(Some(updated_device)) => { + if !updated_device.power { + zone.last_power_change_at = Some(Utc::now()); + zone.last_action_at = Some(Utc::now()); + zone.device_setpoint = None; + } + } + Ok(None) => {} + Err(err) => state.log("warn", "zone.simple_heat_day_off_error", &err.to_string(), + json!({"zone_id": zone.id})), + } + } + } + record_zone_history(state, &zone, outdoor_temperature, settings.poll_interval_seconds); + publish_persisted_zone_cycle(state, persist_zone_cycle(state, &zone, cycle_started_at)?)?; + continue; + } + 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 { + zone.heat_coasting = if let Some(decision) = simple_decision { + decision.coasting + } else { + heat_coasting_next_state(zone.heat_coasting, effective_mode, temp, target, zone.heat_anticipation_c) + }; + zone.demand = if let Some(decision) = simple_decision { + decision.demand + } else { match effective_mode { "heat" => { if temp <= target - half { true @@ -831,7 +896,7 @@ async fn control_zones(state: &AppState) -> Result<()> { zone.demand } } - }; + }}; if zone.demand && !previous_demand { zone.demand_since = Some(Utc::now()); zone.target_alerted_at = None; @@ -874,15 +939,10 @@ async fn control_zones(state: &AppState) -> Result<()> { "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, - delivering_heat, - if delivering_heat { - active_target - } else { - standby_target - }, - ); + let desired_device_target = simple_decision.map(|decision| decision.device_target).unwrap_or_else(|| { + round_device_setpoint(effective_mode, delivering_heat, + if delivering_heat { active_target } else { standby_target }) + }); // Report only the last confirmed device state here. The desired target belongs to // effective_setpoint/command planning until a device command succeeds. zone.device_setpoint = if device.power { diff --git a/src/models/automation.rs b/src/models/automation.rs index 989cf0c..c46d0ef 100644 --- a/src/models/automation.rs +++ b/src/models/automation.rs @@ -83,6 +83,9 @@ pub struct Automation { pub action_zone_id: Option, #[serde(default)] pub action_zone_preset: Option, + /// Optional single-block Flow heating policy. + #[serde(default)] + pub action_zone_simple_heat: Option, /// Optional soft-coast parameters set by a Flow thermostat action. #[serde(default)] pub action_zone_heat_anticipation_c: Option, diff --git a/src/models/zone.rs b/src/models/zone.rs index b5b5bf5..b2b5b0b 100644 --- a/src/models/zone.rs +++ b/src/models/zone.rs @@ -1,3 +1,25 @@ +/// One simple Flow action owns a room's daytime/night-time heating policy. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct SimpleHeatSettings { + pub flow_id: String, + pub flow_node_id: String, + pub day_target: f64, + pub night_target: f64, + pub day_min: f64, + pub boost_after_minutes: u64, + pub day_off_after_minutes: u64, +} + +/// Persisted hysteresis/timers survive application restarts and sensor polling gaps. +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct SimpleHeatRuntime { + pub night: Option, + pub day_heating: bool, + pub day_stable_since: Option>, + pub gentle_since: Option>, + pub gentle_start_temp: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Zone { pub id: String, @@ -44,6 +66,11 @@ pub struct Zone { /// Minimum interval between automatic setpoint/fan adjustments. #[serde(default = "default_min_adjust")] pub min_adjust_seconds: u64, + /// Active simple Day/Night heating Flow (not a manual custom preset). + #[serde(default)] + pub simple_heat: Option, + #[serde(default)] + pub simple_heat_runtime: SimpleHeatRuntime, /// Degrees before the comfort target at which GREE enters low-heat coasting. /// Zero disables predictive soft standby (the pre-upgrade behavior). #[serde(default)] diff --git a/web/css/styles.css b/web/css/styles.css index 57c7fa4..a2e2b9f 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -636,7 +636,7 @@ h3 { min-width: 0; } -.louver-control > span { +.louver-control>span { color: var(--muted); font-size: 11px; line-height: 1.15; @@ -1549,20 +1549,20 @@ legend { gap: 10px; } -.local-discovery-head > input[type="checkbox"] { +.local-discovery-head>input[type="checkbox"] { width: 18px; height: 18px; margin: 0; } -.local-discovery-head > label { +.local-discovery-head>label { display: grid; gap: 2px; min-width: 0; cursor: pointer; } -.local-discovery-head > .badge { +.local-discovery-head>.badge { justify-self: end; } @@ -6608,6 +6608,59 @@ textarea[aria-invalid="true"] { margin: 2px 0 0; } +.flow-inspector-description { + margin: -7px 0 15px; + color: var(--muted); + font-size: .82rem; + line-height: 1.5; +} + +.flow-block-help { + margin: 16px 0 18px; + padding: 10px 12px; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--surface-2); + color: var(--text); + font-size: .81rem; + line-height: 1.5; +} + +.flow-block-help summary { + cursor: pointer; + font-weight: 700; +} + +.flow-block-help[open] summary { + margin-bottom: 10px; +} + +.flow-block-help p { + margin: 8px 0 10px; +} + +.flow-block-help strong { + display: block; + margin: 12px 0 5px; +} + +.flow-block-help ul { + margin: 0 0 10px; + padding-left: 19px; +} + +.flow-block-help li { + margin-bottom: 6px; +} + +.flow-block-library-description { + display: block; + color: var(--muted); + font-weight: 400; + line-height: 1.45; + margin-top: 4px; +} + .flow-inspector label { display: grid; gap: 6px; @@ -10632,6 +10685,7 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr min-height: 140px; } } + /* Keep Flow interpretation compact on every viewport. Details are opt-in. */ .flow-natural-preview { display: block; @@ -10708,7 +10762,7 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr margin-top: 14px; } -.flow-shortcut-grid > div { +.flow-shortcut-grid>div { display: grid; grid-template-columns: minmax(0, auto) 1fr; align-items: center; @@ -10725,7 +10779,7 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr gap: 4px; } -.flow-shortcut-grid > div > span:last-child { +.flow-shortcut-grid>div>span:last-child { grid-column: 2; } @@ -10765,14 +10819,17 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr transition: opacity .14s ease, transform .14s ease; pointer-events: none; } + .flow-editor-notice.is-visible { opacity: 1; transform: translateY(0); } + .flow-editor-notice.is-error { color: var(--danger); background: var(--danger-soft); } + .flow-context-menu { position: fixed; z-index: 1200; @@ -10784,7 +10841,11 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr background: var(--surface); box-shadow: 0 16px 38px rgba(0, 0, 0, .28); } -.flow-context-menu[hidden] { display: none; } + +.flow-context-menu[hidden] { + display: none; +} + .flow-context-menu button { width: 100%; min-height: 34px; @@ -10797,22 +10858,28 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr font: inherit; font-size: .82rem; } + .flow-context-menu button:hover:not(:disabled), .flow-context-menu button:focus-visible:not(:disabled) { background: var(--accent-soft); outline: none; } + .flow-context-menu button:disabled { opacity: .45; cursor: default; } + .flow-context-separator { height: 1px; margin: 5px 4px; background: var(--line); } + @media (max-width: 760px) { - .flow-editor-notice { max-width: 42vw; } + .flow-editor-notice { + max-width: 42vw; + } } /* Flow quick actions: description editing and simple simulator */ @@ -10920,12 +10987,12 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr background: var(--surface-2); } -.flow-simple-intro > div { +.flow-simple-intro>div { display: grid; gap: 3px; } -.flow-simple-intro > div > span { +.flow-simple-intro>div>span { color: var(--muted); font-size: .88rem; } @@ -10971,7 +11038,7 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr background: var(--surface-2); } -.flow-simple-scenario-field > span { +.flow-simple-scenario-field>span { display: grid; gap: 2px; } @@ -11012,19 +11079,19 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr gap: 12px; } -.flow-simple-result-head > div { +.flow-simple-result-head>div { display: grid; gap: 3px; } -.flow-simple-result-head > div > small { +.flow-simple-result-head>div>small { color: var(--muted); font-weight: 700; text-transform: uppercase; letter-spacing: .05em; } -.flow-simple-result > p { +.flow-simple-result>p { margin: 0; color: var(--muted); } @@ -11050,7 +11117,7 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr border-top: 1px solid var(--line); } -.flow-simple-condition > span:nth-child(2) { +.flow-simple-condition>span:nth-child(2) { display: grid; gap: 2px; min-width: 0; @@ -11079,7 +11146,7 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr align-items: flex-start; } - .flow-card .card-footer > small { + .flow-card .card-footer>small { max-width: 100%; white-space: normal; } @@ -11121,4 +11188,4 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr grid-column: 2; text-align: left; } -} +} \ No newline at end of file diff --git a/web/index.html b/web/index.html index f4584d1..7e9f079 100644 --- a/web/index.html +++ b/web/index.html @@ -86,7 +86,8 @@ data-action="house-power" data-value="true" data-i18n="house.powerOnAll">Turn all on + class="secondary house-emergency-stop" data-action="house-emergency-stop" data-value="true" + hidden>Emergency stop
@@ -960,7 +961,8 @@
Sensory / wartości + data-flow-add="constant" data-i18n="flow.node.constant" data-i18n-title="flow.constantDescription">Stała + testowa
Logika
Akcje + + data-i18n="flow.node.greeDevice">Urządzenie GREE
Home Assistant
+ data-i18n="flow.shortcuts">Skróty
Skróty klawiaturowe
-

Skróty działają, gdy kursor nie znajduje się w polu formularza.

+

Skróty działają, gdy kursor nie znajduje się w polu formularza. +

-
AInsertCtrl/Cmd+KDodaj / wyszukaj blok
-
EnterDodaj pierwszy wynik wyszukiwania
-
Ctrl/Cmd+SZapisz Flow
-
Ctrl/Cmd+ZCofnij ostatnią zmianę
-
Ctrl/Cmd+AZaznacz wszystkie bloki
-
Ctrl/Cmd+CKopiuj zaznaczone bloki
-
Ctrl/Cmd+XWytnij zaznaczone bloki
-
Ctrl/Cmd+VWklej bloki
-
Ctrl/Cmd+DDuplikuj zaznaczenie
-
DeleteBackspaceUsuń zaznaczone bloki
-
← ↑ ↓ →Przesuń zaznaczenie o 10 px
-
Shift+← ↑ ↓ →Przesuń precyzyjnie o 1 px
-
EscWyczyść zaznaczenie / anuluj łączenie
-
Ctrl/Cmd+0Dopasuj cały Flow do widoku
-
Ctrl/Cmd++ / -Powiększ / pomniejsz
-
?Pokaż tę listę skrótów
-
Prawy przyciskMenu kontekstowe Flow
+
AInsertCtrl/Cmd+KDodaj / wyszukaj blok
+
EnterDodaj + pierwszy wynik wyszukiwania
+
Ctrl/Cmd+SZapisz + Flow
+
Ctrl/Cmd+ZCofnij + ostatnią zmianę
+
Ctrl/Cmd+AZaznacz + wszystkie bloki
+
Ctrl/Cmd+CKopiuj + zaznaczone bloki
+
Ctrl/Cmd+XWytnij + zaznaczone bloki
+
Ctrl/Cmd+VWklej + bloki
+
Ctrl/Cmd+DDuplikuj zaznaczenie
+
DeleteBackspaceUsuń zaznaczone bloki
+
← ↑ ↓ →Przesuń + zaznaczenie o 10 px
+
Shift+← ↑ ↓ →Przesuń precyzyjnie o 1 px
+
EscWyczyść + zaznaczenie / anuluj łączenie
+
Ctrl/Cmd+0Dopasuj cały + Flow do widoku
+
Ctrl/Cmd++ / -Powiększ + / pomniejsz
+
?Pokaż tę listę + skrótów
+
Prawy przyciskMenu kontekstowe Flow
@@ -1173,8 +1196,10 @@

-

Only the description is changed. You do not need to open the Flow editor.

+ data-i18n-placeholder="flow.descriptionPlaceholder" + placeholder="Describe what this Flow does…"> +

Only the description is changed. You do not need to + open the Flow editor.

@@ -1189,12 +1214,17 @@
-
Set a situation and see whether each Flow action would run.
+
Set a situation + and see whether each Flow action would run.
Preview only — nothing is changed
-
Situation to check

Leave values unchanged to use the current application and Home Assistant state.

+
+
Situation to check +

Leave values unchanged to use the current + application and Home Assistant state.

+
+
@@ -1345,7 +1375,10 @@

Discover GREE devices

-

Auto accepts both AES-ECB and AES-GCM devices, uses discovery metadata only as a protocol hint and verifies the actual protocol during binding. Selecting V1 or V2 keeps discovery and binding locked to that protocol. Multiple passes improve discovery when several Wi-Fi modules answer the same broadcast.

+

Auto accepts both AES-ECB and AES-GCM devices, uses discovery + metadata only as a protocol hint and verifies the actual protocol during binding. Selecting V1 or V2 keeps + discovery and binding locked to that protocol. Multiple passes improve discovery when several Wi-Fi modules + answer the same broadcast.

- -

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

+ +

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

Medium-high - - - - - - - + + + + + + + -

Leave every other field as No change to modify only the selected unit function.

+

Leave every other field as No change to + modify only the selected unit function.

diff --git a/web/js-dynamic/entities.js b/web/js-dynamic/entities.js index 5597dd9..161f922 100644 --- a/web/js-dynamic/entities.js +++ b/web/js-dynamic/entities.js @@ -471,7 +471,10 @@ function zoneRuntimeStatusLabel(zone, effectiveMode, device = null) { if (zone.device_manual_override === true) return tr('zones.manualDeviceControl'); if (!zone.enabled) return tr('common.disabled'); if (zone.local_thermostat_power === false) return tr('zones.localThermostatOff'); - if ((effectiveMode || 'off') === 'off') return tr('zones.waiting'); + if ((effectiveMode || 'off') === 'off') { + if (zone.control_source === 'automation.flow' && zone.simple_heat && zone.control_owner === 'automation') return tr(device?.power ? 'zones.flowDayPendingOff' : 'zones.flowDayOff'); + return tr('zones.waiting'); + } if (device?.enabled === false) return tr('zones.waitingDeviceDisabled'); if (device && (!device.online || Number(device.communication_failures || 0) > 0)) return tr('zones.waitingOffline'); if (zone.current_temperature == null) return tr('zones.noMeasurement'); @@ -480,7 +483,7 @@ function zoneRuntimeStatusLabel(zone, effectiveMode, device = null) { 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'); + if (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'); @@ -501,7 +504,8 @@ function zoneCard(zone, detailed = true) { const displayPreset = idleWithoutTarget ? 'auto' : (zone.manual_preset || zone.active_preset || 'comfort'); const globalModeAvailable = ['heat', 'cool'].includes(app.settings?.house_mode); const globalModeDisabled = globalModeAvailable ? '' : ` disabled aria-disabled="true" title="${esc(tr('zones.globalModeUnavailable'))}"`; - const hasManualOverride = zone.manual_preset != null || zone.manual_setpoint != null; + const flowOwned = zone.control_owner === 'automation' && zone.control_source === 'automation.flow'; + const hasManualOverride = !flowOwned && (zone.manual_preset != null || zone.manual_setpoint != null); const deviceManualOverride = zone.device_manual_override === true; const controlGroup = groupControlForZone(zone); const deviceUnavailable = !!device && (device.enabled === false || !device.online || Number(device.communication_failures || 0) > 0); @@ -527,9 +531,9 @@ function zoneCard(zone, detailed = true) { ? tr('zones.temporaryOverride', { time: temporaryStatus?.countdown || '—' }) : localThermostatOverride ? tr(localThermostatPower ? 'zones.localThermostatOn' : 'zones.localThermostatOff') - : (zone.manual_override_until + : (flowOwned ? tr('zones.ownerFlow') : (zone.manual_override_until ? `${tr('zones.overrideUntil')} ${new Date(zone.manual_override_until).toLocaleTimeString(locale(), { hour: '2-digit', minute: '2-digit' })}` - : (hasManualOverride ? tr('zones.manualNoBoundary') : tr('zones.scheduleControl'))); + : (hasManualOverride ? tr('zones.manualNoBoundary') : tr('zones.scheduleControl')))); const manualTakeover = deviceManualOverride ? `
${esc(tr('zones.manualDeviceControl'))}

${esc(tr('zones.manualDeviceDescription'))}

` : localThermostatOverride && !temporarySession diff --git a/web/js-dynamic/flows.js b/web/js-dynamic/flows.js index ad85748..508a0fb 100644 --- a/web/js-dynamic/flows.js +++ b/web/js-dynamic/flows.js @@ -23,11 +23,12 @@ const FLOW_NODE_META = Object.freeze({ zone_state: { titleKey: 'flow.node.zoneState', category: 'sensor' }, group_state: { titleKey: 'flow.node.groupState', category: 'sensor' }, night_mode: { titleKey: 'flow.node.nightMode', category: 'time' }, - constant: { titleKey: 'flow.node.constant', category: 'sensor' }, + constant: { titleKey: 'flow.node.constant', category: 'sensor', descriptionKey: 'flow.constantDescription' }, shared_input: { titleKey: 'flow.node.sharedInput', category: 'sensor' }, logic_and: { titleKey: 'flow.node.and', category: 'logic' }, logic_or: { titleKey: 'flow.node.or', category: 'logic' }, logic_not: { titleKey: 'flow.node.not', category: 'logic' }, + zone_heat_cycle: { titleKey: 'flow.node.simpleHeat', category: 'action', descriptionKey: 'flow.simpleHeatDescription' }, zone_thermostat: { titleKey: 'flow.node.thermostat', category: 'action' }, device_action: { titleKey: 'flow.node.greeDevice', category: 'action' }, device_feature_action: { titleKey: 'flow.node.greeFeature', category: 'action' }, @@ -98,6 +99,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_heat_cycle') return { zone_id: app.zones[0]?.id || '', day_target: 20.0, night_target: 20.6, day_min: 19.7, boost_after_minutes: 10, day_off_after_minutes: 30, 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 }; @@ -424,6 +426,10 @@ function flowNodeSummary(node) { if (node.kind === 'logic_and') return tr('flow.allConditions'); if (node.kind === 'logic_or') return tr('flow.anyCondition'); if (node.kind === 'logic_not') return tr('flow.invertCondition'); + if (node.kind === 'zone_heat_cycle') { + const zone = app.zones.find(z => z.id === c.zone_id); + return `${zone?.name || tr('common.noZone')} · ${tr('flow.simpleHeatDay')} ${fmtTemp(c.day_target)} · ${tr('flow.simpleHeatNight')} ${fmtTemp(c.night_target)}`; + } if (node.kind === 'zone_thermostat') { const zone = app.zones.find(z => z.id === c.zone_id); const power = c.power == null ? null : (c.power ? tr('common.on') : tr('common.off')); @@ -627,10 +633,18 @@ function renderFlowBlockLibrary(filter = '') { const groups = $$('.flow-palette .flow-palette-group'); host.innerHTML = groups.map(group => { const title = group.querySelector(':scope > span')?.textContent?.trim() || ''; - const buttons = $$('[data-flow-add]', group).filter(button => !query || `${title} ${button.textContent}`.toLocaleLowerCase(locale()).includes(query)); + const buttons = $$('[data-flow-add]', group).filter(button => { + const meta = FLOW_NODE_META[button.dataset.flowAdd]; + const description = meta?.descriptionKey ? tr(meta.descriptionKey) : ''; + return !query || `${title} ${button.textContent} ${description}`.toLocaleLowerCase(locale()).includes(query); + }); if (!buttons.length) return ''; const categoryClass = [...group.classList].find(name => name.startsWith('block-category-')) || ''; - return `

${esc(title)}

${buttons.map(button => ``).join('')}
`; + return `

${esc(title)}

${buttons.map(button => { + const meta = FLOW_NODE_META[button.dataset.flowAdd]; + const description = meta?.descriptionKey ? tr(meta.descriptionKey) : ''; + return ``; + }).join('')}
`; }).join('') || `
${esc(tr('flow.noBlocksFound'))}${esc(tr('flow.noBlocksFoundHint'))}
`; } @@ -647,7 +661,7 @@ function renderFlowEditor() { const nodesHost = $('#flowNodes'); nodesHost.innerHTML = draft.nodes.map(node => { const meta = FLOW_NODE_META[node.kind] || { title: node.kind, category: 'logic' }; - return `
+ return `
${esc(flowNodeTitle(meta))}
${esc(flowNodeSummary(node))}${node.kind === 'shared_input' && node.config?.input_id ? `
${esc(tr('flow.sharedInputTestCurrent'))}—
` : ''}
@@ -773,11 +787,50 @@ function flowIncomingHeatingCurve(nodeId) { return edge ? flowNodeById(edge.from) : null; } +// The inspector derives all examples from this block's room targets; GREE +// accepts whole-degree setpoints but the room targets may have tenths. +function flowSimpleHeatHelp(config, nodeId = '', expanded = false) { + const day = Number.isFinite(Number(config.day_target)) ? Number(config.day_target) : 20; + const night = Number.isFinite(Number(config.night_target)) ? Number(config.night_target) : 20.6; + const minimum = Number.isFinite(Number(config.day_min)) ? Number(config.day_min) : 19.7; + const boostMinutes = Number.isFinite(Number(config.boost_after_minutes)) ? Number(config.boost_after_minutes) : 10; + const offMinutes = Number.isFinite(Number(config.day_off_after_minutes)) ? Number(config.day_off_after_minutes) : 30; + const nightGentle = Math.min(30, Math.max(16, Math.floor(night) + 1)); + const nightBoost = Math.min(30, Math.max(16, Math.ceil(night + 2))); + const nightBrake = Math.min(30, Math.max(16, Math.floor(night - 0.5))); + const nightStandby = Math.min(30, Math.max(16, Math.floor(night - 1))); + const dayGentle = Math.min(30, Math.max(16, Math.floor(day) + 1)); + const dayBoost = Math.min(30, Math.max(16, Math.ceil(day + 2))); + const line = (key, params) => `
  • ${esc(tr(key, params))}
  • `; + return `
    + ${esc(tr('flow.simpleHeatHowItWorks'))} +

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

    + ${esc(tr('flow.simpleHeatHelpNightTitle', { target: fmtTemp(night) }))} +
      + ${line('flow.simpleHeatHelpNightSleep', { room: fmtTemp(night), gree: fmtTemp(nightStandby) })} + ${line('flow.simpleHeatHelpNightBrake', { room: fmtTemp(night - 0.1), gree: fmtTemp(nightBrake) })} + ${line('flow.simpleHeatHelpNightGentle', { room: fmtTemp(night - 0.1), gree: fmtTemp(nightGentle) })} + ${line('flow.simpleHeatHelpNightBoost', { room: fmtTemp(night - 0.8), minutes: boostMinutes, gree: fmtTemp(nightBoost) })} +
    + ${esc(tr('flow.simpleHeatHelpDayTitle', { target: fmtTemp(day), minimum: fmtTemp(minimum) }))} +
      + ${line('flow.simpleHeatHelpDayOff', { minimum: fmtTemp(minimum) })} + ${line('flow.simpleHeatHelpDayHeat', { gentle: fmtTemp(dayGentle), boost: fmtTemp(dayBoost), room: fmtTemp(day - 0.8), minutes: boostMinutes })} + ${line('flow.simpleHeatHelpDayStop', { target: fmtTemp(day), minutes: offMinutes })} +
    +

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

    +

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

    +

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

    +
    `; +} + function renderFlowInspector() { const host = $('#flowInspector'), node = flowNodeById(app.flowSelectedNodeId); if (!host) return; host.classList.toggle('has-selection', Boolean(node)); if (!node) { host.classList.remove('is-expanded'); host.innerHTML = `
    ${esc(tr('flow.selectBlock'))}${esc(tr('flow.selectBlockHint'))}
    `; return; } const c = node.config || {}, meta = FLOW_NODE_META[node.kind] || { title: node.kind }; + const currentHelp = host.querySelector('.flow-block-help'); + const helpOpen = Boolean(currentHelp?.open && currentHelp.dataset.flowHelpNode === node.id); let fields = ''; if (node.kind === 'weekday') fields = `
    ${[1, 2, 3, 4, 5, 6, 7].map(day => ``).join('')}
    `; else if (node.kind === 'time_range') fields = `
    `; @@ -803,7 +856,7 @@ function renderFlowInspector() { else if (node.kind === 'zone_state') fields = `${flowTextComparisonFields(c)}`; else if (node.kind === 'group_state') fields = `${flowTextComparisonFields(c)}`; else if (node.kind === 'night_mode') fields = `

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

    `; - else if (node.kind === 'constant') fields = ``; + else if (node.kind === 'constant') fields = `

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

    `; else if (node.kind === 'shared_input') { const item = (app.flowSharedInputs || []).find(value => value.id === c.input_id) || app.flowSharedInputs?.[0]; fields = (app.flowSharedInputs || []).length @@ -813,6 +866,19 @@ 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_heat_cycle') fields = ` + +
    + + +
    + +
    + + +
    +

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

    + ${flowSimpleHeatHelp(c, node.id, helpOpen)}`; 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'))}

    `; @@ -826,7 +892,8 @@ function renderFlowInspector() { } else if (incomingCurve && node.kind === 'ha_service_action') { fields += `

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

    `; } - host.innerHTML = `
    ${esc(tr('flow.blockSettings'))}

    ${esc(flowNodeTitle(meta))}

    ${fields}
    ID${esc(node.id)}
    `; + const description = meta.descriptionKey ? `

    ${esc(tr(meta.descriptionKey))}

    ` : ''; + host.innerHTML = `
    ${esc(tr('flow.blockSettings'))}

    ${esc(flowNodeTitle(meta))}

    ${description}${fields}
    ID${esc(node.id)}
    `; } function flowTextComparisonFields(c, numeric = false) { const options = numeric ? flowOperatorOptions(c.operator || 'eq') : [['eq', '='], ['neq', '≠']].map(([v, l]) => ``).join(''); return `
    `; } @@ -1736,6 +1803,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_heat_cycle' && ['day_target', 'night_target', 'day_min', 'boost_after_minutes', 'day_off_after_minutes'].includes(key)) + node.config[key] = 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);