From d80005de33fc2814d3b041f7f3a8a8de7b4022df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 21 Sep 2026 22:58:30 +0200 Subject: [PATCH] v0.15.15 --- Cargo.lock | 2 +- Cargo.toml | 2 +- FILE_MANIFEST.sha256 | 64 ++++----- README.md | 4 +- SOURCE_MANIFEST.sha256 | 62 ++++----- docs/API.md | 26 +++- docs/openapi.json | 129 +++++++++++++++++- .../gree_controller/manifest.json | 2 +- .../repository/gree-controller/CHANGELOG.md | 7 + ha-addon/repository/gree-controller/README.md | 2 +- .../repository/gree-controller/config.yaml | 2 +- lang/en.json | 34 +++++ lang/pl.json | 34 +++++ scripts/api_dev_test.py | 6 +- src/api.rs | 5 + src/api/configuration.rs | 12 ++ src/api/house.rs | 122 +++++++++++++++++ src/api/system.rs | 17 ++- src/config.rs | 2 + src/engine/automations.rs | 8 ++ src/engine/control_plan.rs | 21 ++- src/engine/tests.rs | 5 + src/engine/zone_control.rs | 5 +- src/models/control_plan.rs | 2 + src/models/runtime.rs | 6 + src/state.rs | 6 +- web/css/styles.css | 113 +++++++++++++++ web/index.html | 3 +- web/js-dynamic/bootstrap.js | 1 + web/js-dynamic/core.js | 2 +- web/js-dynamic/dashboard.js | 84 +++++++++++- web/js-dynamic/events.js | 17 +++ web/js-dynamic/realtime.js | 9 +- 33 files changed, 722 insertions(+), 94 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dd05078..71d8656 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -719,7 +719,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.15.14" +version = "0.15.15" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index 7c1c8c5..361d239 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gree-controller" -version = "0.15.14" +version = "0.15.15" 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 8dfe778..87b5f32 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -1,14 +1,14 @@ de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -11082066dd96ca1b6c606593cbb1264ad19d30e92db64aa0ca14b0203f880e81 ./Cargo.lock -909fd22e0fb139f17f3996be8088c42150761706e8b6c495d9f016ba26191491 ./Cargo.toml +bf494c24be8032cab82e54d9aaebdda6e7544eb5ebef4b2a3e700808636073e7 ./Cargo.lock +34a15c5dbb3a739480b32f40ba51d19ea51db19fd39474df560e35ea9f0ba0e3 ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -3bb541af2907e9f55ab3ab683822966834b8f6d81881ab0ce812d48cadac344b ./README.md -dab9da1afb385bc96ba4888c9eb0c8fcef60f5983bd8fdbb808632f269d8f02d ./SOURCE_MANIFEST.sha256 +41bb456a4b4e71298a1859994562e0f582d056b2cfdbb54c59f5b4dfaab6d2f0 ./README.md +bbce332922c081118cf6cace226fb5ae1573139604a8bb9bc27b7ef1c8c04541 ./SOURCE_MANIFEST.sha256 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs -69c7604b2bdeef946dd95675a07b77bd798a0c4ab17025670df09cd339d38278 ./docs/API.md +323abb42b1c612d23826e5c01ab89aa1d757490d32e4423430626acb94ae68c9 ./docs/API.md 43af7941ec4d427f9db7ab3cc6baf3d182a72dba7a6791f14094fee138324d8d ./docs/FLOW.md -01186f7403248378373edebb990cb016e9ff9f48df5874ab95eec4b1fa6e4273 ./docs/openapi.json +7abd620b69af9fe82dcf7e1c91a1c27170e00003a5ca26e3089bffca3ed38c96 ./docs/openapi.json 77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example 0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile 0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md @@ -22,7 +22,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 -f84b8abfd72a2f4eb69cff85aa4e9ec46b06a44f038e6a75afdd6b5accf570e8 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json +dc29a75dcc3ce0f3f941f651f77271acdc5e49540baec5050e6c8e294f95da74 ./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 @@ -30,10 +30,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 -b1aa529d58658ca1538904e86a42d0b5d23fc39c35a4bae9c785f3288d75af55 ./ha-addon/repository/gree-controller/CHANGELOG.md +e4ce58fc6b0f5f1a1fba802097badc8dad6c357ca2bd5a25ab9b698995ebc5c1 ./ha-addon/repository/gree-controller/CHANGELOG.md 50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md -f583a7f66d024da96f55c4aa45ea403294a433265da2312a0d81502a2f7079c3 ./ha-addon/repository/gree-controller/README.md -2b4470debd1ad26fbddb3c1c70ba6ad994f5509c2c604a3dcc0ba62b4779572e ./ha-addon/repository/gree-controller/config.yaml +fc3151e76c08d176bdf695dd5b2537a79d83a768633282e4c72a3c86bb841659 ./ha-addon/repository/gree-controller/README.md +9622d558c6fa9c855cc3bc672980bb5ff8726ac81d622f0ede371580156305c6 ./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 @@ -43,8 +43,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run 081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md -df4b0545d6f522a7f1bdff3b47b8a6fe0f711c0ad2013c0d35cfad7b61d73a92 ./lang/en.json -cfc4ef4a6f6dd26063ed8673d6f74173f193d531f2255cc89e53e9137cf6a09e ./lang/pl.json +f5251fbd8355f560df97bc2042e05e045f79bffc6b6f18bf84999c16fc4c64df ./lang/en.json +c1c98e668c6b2e615e882c9079b6f198e0360c188264d0083c21d7b332b4cbc9 ./lang/pl.json d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json @@ -85,7 +85,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md -17a8e34882ad7c3f4cd53d3ccdff74a9d533963bd14b7434fc7d4ca2d9e56093 ./scripts/api_dev_test.py +a453bb9c8529c0c37d7004765aef8a47ccc7a5e92080802435b7b1db3a695464 ./scripts/api_dev_test.py 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh 1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh @@ -98,11 +98,11 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh bd703ef841f9763f13367aaa5764381b9c59ee87210e806c24617f3cc0739a9b ./scripts/smoke.sh b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh -b53e1afab342634fedc218ea4517446f19a5ba7985dee599e191b37d9cc8058a ./src/api.rs +ea84f5230da97447c60dfb4f8dadd0747c92fd1f7a74d5a65d6fdb6f23cf4b6d ./src/api.rs 0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs 1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs 9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs -27a788efe385ad7180cb7fc7ec66c34ebd5e6062d9cdf35b32fb461e52a42863 ./src/api/configuration.rs +fffad7874465739cd6b508e9d7791387b864fa88420ce0f7e2fc933d129a2751 ./src/api/configuration.rs 19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs 8f92909dc575ea6238f4a003f8bd4056c2752fa3247b9ab226e47f875c80b916 ./src/api/devices.rs @@ -111,17 +111,17 @@ ca5bc21e051222dcaf0c507e84cb3e60f52a0b612a6056847fa0aee3a4d1a672 ./src/api/flow 82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs -e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/house.rs +655bc8135fbe0b8fa491666942e80817a0eabc7dea694c68c2e5ebb30c8ecf75 ./src/api/house.rs 4ed7835ed622806124a805f54ffde7c39854f2447de0141d756d96f22446fb90 ./src/api/integrations.rs 7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs 9d996d326f2ec192c79fb108a8d2c06efaa1c9634d9e9e54fa65a65524f92a75 ./src/api/openapi.rs b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs 5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs -bb57f4b28df75ba350c9706719650fff3a689afee0cb6979c1fcae79396ac0cf ./src/api/system.rs +7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs 3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs ddd1bfc8625b66f6e5f3d24b3122ac86c25f89b0af527e3dfce9102551a8f142 ./src/api/zones.rs -0b7d2ace2e8cb1dd0cb59aa6ca3e80cec14078e93530fd7b58081798585cd833 ./src/config.rs +32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs 87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs a8fe3636ac79df191e2f2c9d8c1eec1e5181e048ef8c9036cfa18f84aef1a462 ./src/db/configuration.rs @@ -136,10 +136,10 @@ d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/sched 28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs 0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs -a0d60ad81d024488dfec586b0d9e9f447e9a3485985604415465cacba59d645f ./src/engine/automations.rs +f3cc7d2cbf44180372d987d7502409547eadb1c725bcce6eeb5a26cc4277b83f ./src/engine/automations.rs 1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs 357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs -ad53cd7be24deb759e03cbc6472522b69ebd36d8fac35398a9b19c899d3b17f2 ./src/engine/control_plan.rs +315116a0dbeeba1af2f72513ea5fcc238da1b5ce9501b039c5188e44976978dc ./src/engine/control_plan.rs 7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs 519f67ab805f61afea107215f5df50562fa1669a48334cc015570952e25e773a ./src/engine/energy.rs e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs @@ -152,9 +152,9 @@ c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/r b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs 682c8b3956c657b655746cbd19ee97ee2f4cf953bc9de67d6d70a1b14f3dd746 ./src/engine/temperature.rs 833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs -0404e040c5d6f388682edb3d103a658e0b963a402e4e847fc92fd0aea1c0e773 ./src/engine/tests.rs +bf1a3840135a71ce64148559cce0fbd570ff99d31c8972cdea7447a49f6868f7 ./src/engine/tests.rs c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs -fb169bcc20f95abe9892367df6dd3a683584bdbc721792d6ffb9311b81ca30d9 ./src/engine/zone_control.rs +87ab87d434a6462a2c3f25249b42c59ecb2bca6ed700ec73e95e4613349cf16f ./src/engine/zone_control.rs bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs @@ -164,13 +164,13 @@ da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb 62f9da6ef4f5fd92701d0857773f9f6236ef25356f21b9b42236abab6ba7da53 ./src/main.rs 23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs e7b6e49d369aaf665a8aea2464950fd795957de0ad7528a002e11c55c7983f35 ./src/models/automation.rs -fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/control_plan.rs +a7a35e60318a620d411ec16ab8faecd34410ccba3310f612e89c06904a16539b ./src/models/control_plan.rs 06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs 698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs 6e44df6bde2f73c282095c80ac17287e5bef77d9fc1d344e440d863dae5f5031 ./src/models/history.rs 1fee89f939018bf1f3686c15f25434f01ab96b3239164108db7f348a1f1f8d82 ./src/models/integrations.rs -e557ee4a443de586a7ea7dd04318694fb30d6ec053076429503e18f1bcd65c9e ./src/models/runtime.rs +17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs 41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs @@ -196,26 +196,26 @@ a677ce61d3d0f4cf358055ddbc343e73b452c68daed80798499147370819615e ./src/provider 7e12e8704faa7ac15a312bcc1d714ee1c6f27c1dce0972d4ccd6e5a40c69c884 ./src/queries/maintenance.rs 7da8b73a1c8c30e2a12071062d36f5393938fde0b5ffebbe4af1891da40f3233 ./src/queries/schema.rs f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/zone_history.rs -8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs +d8a602ed8906d77593bd140315ce1bd56716038e789f53ff31762e9b3cb7220d ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html -2d227963624338f958033ec9b211b6bfa2c869ba3d7dc4f8f8125a29d343f7d9 ./web/css/styles.css +fd56aa620ddd63bf7f8d03f00da3b2bd53993240bb37cebdac1bdffec22d867c ./web/css/styles.css 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg -92847143a358e6869ed4f21b156b10bda3f8abfcb9a12fc86256221c93be3b09 ./web/index.html +43f33fc21bb11df0ea149fe7fbca722a57f93890e4d9bb542ced703c1311207e ./web/index.html 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md -e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js +e7318cd92b76e8a0109dc01adc2431474be17d8bb1488cdf6ffb39dc5450909d ./web/js-dynamic/bootstrap.js 5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js -81012bd88282034cdc4e876af5eb5057289f4db8505847568bc3c3701a348d4b ./web/js-dynamic/core.js -752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js +07629b331bd498e8bea769d577321bbd12d9b076264242706d4ef16165fb22fa ./web/js-dynamic/core.js +1b444068bf91741c54bd51741a95128e335219275c415fe9ce4531c80593b50f ./web/js-dynamic/dashboard.js 930e67432f55e8476513e808edfa9a0809a8da7479307284fcc136082a4129a4 ./web/js-dynamic/entities.js -b4204241eecf387256364f4230b23ed50d947a362f4ceb1e528d69494e93d7cf ./web/js-dynamic/events.js +4612c958d7b9ccf89d6d45a11c3bc7e864bbd2ceaccc16fd7d76811ba4c74196 ./web/js-dynamic/events.js 43c78a3871cfdb0382081ec30b8d48650450c71b44d006414b161349ac1fa659 ./web/js-dynamic/flows.js 3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js 30cbaebb817d5d416e1a372595b647aae42c14b9cc406c4751d0ced563243b69 ./web/js-dynamic/navigation.js -0e398bad2cde85d4a62d8733c06e73f2b6a5b07db791d4570feba38c7ff8741f ./web/js-dynamic/realtime.js +0c93119b3d5985d3440e1525d908efecd1567ab2caa0e6cc3babeecd87bfde5c ./web/js-dynamic/realtime.js 1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js bc42b4a0dd0d20239ce22ea1dff09b3f94e8c0e1395e01251a4585a051968a8f ./web/js-dynamic/settings-ui.js diff --git a/README.md b/README.md index 7f58885..363b15e 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.15.14** +**Version: 0.15.15** -Version 0.15.14 fixes Home Assistant Supervisor authentication detection for background energy sampling and centralizes runtime HA readiness checks, so add-on installations use the same Supervisor-aware connection logic as entity tests, temperatures and Flow integrations. +Version 0.15.15 adds a persistent emergency STOP in Global power and extends Control plan with per-zone control reasons plus recent automation trigger context, making it clear why the controller is acting or paused. > [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples. > Built-in API: `/api/*` and `/ws`; interactive Swagger docs: `/api-docs`; OpenAPI JSON: `/api-docs/openapi.json`. diff --git a/SOURCE_MANIFEST.sha256 b/SOURCE_MANIFEST.sha256 index 38bdc39..3762227 100644 --- a/SOURCE_MANIFEST.sha256 +++ b/SOURCE_MANIFEST.sha256 @@ -1,13 +1,13 @@ de0793949cf01d27d903653226ecc9b5f72523d8711f20bcab022cf42c4c4ed0 ./.env.example a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -11082066dd96ca1b6c606593cbb1264ad19d30e92db64aa0ca14b0203f880e81 ./Cargo.lock -909fd22e0fb139f17f3996be8088c42150761706e8b6c495d9f016ba26191491 ./Cargo.toml +bf494c24be8032cab82e54d9aaebdda6e7544eb5ebef4b2a3e700808636073e7 ./Cargo.lock +34a15c5dbb3a739480b32f40ba51d19ea51db19fd39474df560e35ea9f0ba0e3 ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -3bb541af2907e9f55ab3ab683822966834b8f6d81881ab0ce812d48cadac344b ./README.md +41bb456a4b4e71298a1859994562e0f582d056b2cfdbb54c59f5b4dfaab6d2f0 ./README.md 29e9821e5ed216ea3565dd6d8bc0b174ed3cf9fa72c4ccebbfa99e98db3ad8f9 ./build.rs -69c7604b2bdeef946dd95675a07b77bd798a0c4ab17025670df09cd339d38278 ./docs/API.md +323abb42b1c612d23826e5c01ab89aa1d757490d32e4423430626acb94ae68c9 ./docs/API.md 43af7941ec4d427f9db7ab3cc6baf3d182a72dba7a6791f14094fee138324d8d ./docs/FLOW.md -01186f7403248378373edebb990cb016e9ff9f48df5874ab95eec4b1fa6e4273 ./docs/openapi.json +7abd620b69af9fe82dcf7e1c91a1c27170e00003a5ca26e3089bffca3ed38c96 ./docs/openapi.json 77b8becf7a2c16a384ea85721d878b76e2d520549cb4455d9aa806e57e5a2ecd ./ha-addon/.env.example 0ca6e58170c2b8925194431ebbdb55f0c8b377fec5cd422cd2bd257e2d9328e3 ./ha-addon/Dockerfile 0951696a2c2e66d4d969f8e7f8641184e3012f261583b31074bc38a7a1c0cc1a ./ha-addon/README.md @@ -21,7 +21,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 -f84b8abfd72a2f4eb69cff85aa4e9ec46b06a44f038e6a75afdd6b5accf570e8 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json +dc29a75dcc3ce0f3f941f651f77271acdc5e49540baec5050e6c8e294f95da74 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py 39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py @@ -29,10 +29,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom 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 -b1aa529d58658ca1538904e86a42d0b5d23fc39c35a4bae9c785f3288d75af55 ./ha-addon/repository/gree-controller/CHANGELOG.md +e4ce58fc6b0f5f1a1fba802097badc8dad6c357ca2bd5a25ab9b698995ebc5c1 ./ha-addon/repository/gree-controller/CHANGELOG.md 50b8ba050c6b4919e4d057f7b38c1e23d3913b5b7c87552626452e9695d37251 ./ha-addon/repository/gree-controller/DOCS.md -f583a7f66d024da96f55c4aa45ea403294a433265da2312a0d81502a2f7079c3 ./ha-addon/repository/gree-controller/README.md -2b4470debd1ad26fbddb3c1c70ba6ad994f5509c2c604a3dcc0ba62b4779572e ./ha-addon/repository/gree-controller/config.yaml +fc3151e76c08d176bdf695dd5b2537a79d83a768633282e4c72a3c86bb841659 ./ha-addon/repository/gree-controller/README.md +9622d558c6fa9c855cc3bc672980bb5ff8726ac81d622f0ede371580156305c6 ./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 @@ -42,8 +42,8 @@ cea12bc74c066a588a5ef0ef7bad9d4906bdbcea99f2d275429b0eefc94b4b5f ./ha-addon/run 081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md -df4b0545d6f522a7f1bdff3b47b8a6fe0f711c0ad2013c0d35cfad7b61d73a92 ./lang/en.json -cfc4ef4a6f6dd26063ed8673d6f74173f193d531f2255cc89e53e9137cf6a09e ./lang/pl.json +f5251fbd8355f560df97bc2042e05e045f79bffc6b6f18bf84999c16fc4c64df ./lang/en.json +c1c98e668c6b2e615e882c9079b6f198e0360c188264d0083c21d7b332b4cbc9 ./lang/pl.json d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json @@ -84,7 +84,7 @@ fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/wind 804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json fd7390d64a2378352c09382accf6efff43903d952d111b22a982f06c5b2a3b4b ./regenerate-sha.sh c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md -17a8e34882ad7c3f4cd53d3ccdff74a9d533963bd14b7434fc7d4ca2d9e56093 ./scripts/api_dev_test.py +a453bb9c8529c0c37d7004765aef8a47ccc7a5e92080802435b7b1db3a695464 ./scripts/api_dev_test.py 5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh 6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh 1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh @@ -97,11 +97,11 @@ e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/netw 81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh bd703ef841f9763f13367aaa5764381b9c59ee87210e806c24617f3cc0739a9b ./scripts/smoke.sh b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh -b53e1afab342634fedc218ea4517446f19a5ba7985dee599e191b37d9cc8058a ./src/api.rs +ea84f5230da97447c60dfb4f8dadd0747c92fd1f7a74d5a65d6fdb6f23cf4b6d ./src/api.rs 0db0d81da4b8161004ddcbed2096812c285ecec9d944771670e6c8edf0775175 ./src/api/assets.rs 1139dacbd3f6ff94bf721eb5a60d469016fb935ddcf484d71bfeb249b72c833a ./src/api/auth.rs 9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs -27a788efe385ad7180cb7fc7ec66c34ebd5e6062d9cdf35b32fb461e52a42863 ./src/api/configuration.rs +fffad7874465739cd6b508e9d7791387b864fa88420ce0f7e2fc933d129a2751 ./src/api/configuration.rs 19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs ca21f6d5a407e2e3532899d53ba4e8682c2fe961a46d8cd870ddc459721513ef ./src/api/device_groups.rs 8f92909dc575ea6238f4a003f8bd4056c2752fa3247b9ab226e47f875c80b916 ./src/api/devices.rs @@ -110,17 +110,17 @@ ca5bc21e051222dcaf0c507e84cb3e60f52a0b612a6056847fa0aee3a4d1a672 ./src/api/flow 82c02fa135132ea3ebdb4d88dd1210dc536d22f987151d7f82595ee3d3d03440 ./src/api/gree_cloud.rs b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs b6d586bc42b166ac05648f3da7d9aeb88efde2e9eaab5642685eb4d9cf15f95a ./src/api/history.rs -e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/house.rs +655bc8135fbe0b8fa491666942e80817a0eabc7dea694c68c2e5ebb30c8ecf75 ./src/api/house.rs 4ed7835ed622806124a805f54ffde7c39854f2447de0141d756d96f22446fb90 ./src/api/integrations.rs 7658111f7d125db55a6e23f88157339b110b2cd1ed13ceabd8f860b42dba5101 ./src/api/middleware.rs 9d996d326f2ec192c79fb108a8d2c06efaa1c9634d9e9e54fa65a65524f92a75 ./src/api/openapi.rs b777e7fbeccef699e11ffd16bb18633843c99afecacce2221280e54b09f6d2da ./src/api/public_settings.rs 5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs fd08162d0f9287700196028ad0ca2aec7c242238056b0a228ca5bdcb319ece4e ./src/api/settings.rs -bb57f4b28df75ba350c9706719650fff3a689afee0cb6979c1fcae79396ac0cf ./src/api/system.rs +7c974a98c72092886247f67c3a1c62736898d4b279ceb9c4c306690a3c6e4294 ./src/api/system.rs 3b0124a76dbc654584bcf186d43f89d20b6e7fd980ce0ffbf5a9e8eced5ec7f6 ./src/api/websocket.rs ddd1bfc8625b66f6e5f3d24b3122ac86c25f89b0af527e3dfce9102551a8f142 ./src/api/zones.rs -0b7d2ace2e8cb1dd0cb59aa6ca3e80cec14078e93530fd7b58081798585cd833 ./src/config.rs +32fa38d4315682cd3ef3ecdc91e571c2729b5e68b6f2bff965e43de3fdd2aeed ./src/config.rs de0ce7807aebe4c0e325fbef01855e7fc1cfd117529f53f04a416fda3997673a ./src/db.rs 87f1614a80addde69b619421187526d7f86f893c0ac18998854f11675d6e7cca ./src/db/climate.rs a8fe3636ac79df191e2f2c9d8c1eec1e5181e048ef8c9036cfa18f84aef1a462 ./src/db/configuration.rs @@ -135,10 +135,10 @@ d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/sched 28ee42d08147b0f154af818065d7d014d74ee8ddd59fdef98115f5c5368056cc ./src/db/tests.rs f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs 0e49ea4a52017b0a8501d5101b1906a7a1e8d5158acb0bb65caa43cde2836ec0 ./src/engine.rs -a0d60ad81d024488dfec586b0d9e9f447e9a3485985604415465cacba59d645f ./src/engine/automations.rs +f3cc7d2cbf44180372d987d7502409547eadb1c725bcce6eeb5a26cc4277b83f ./src/engine/automations.rs 1dccb8779f87dfdbd4ccda3f02ec51273e7be2e34e85166861668685528eca79 ./src/engine/commands.rs 357ef92c3cc42b243af4da31ac52bb360d7444ae423f58d7a13bdee79bf0a748 ./src/engine/connectivity.rs -ad53cd7be24deb759e03cbc6472522b69ebd36d8fac35398a9b19c899d3b17f2 ./src/engine/control_plan.rs +315116a0dbeeba1af2f72513ea5fcc238da1b5ce9501b039c5188e44976978dc ./src/engine/control_plan.rs 7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs 519f67ab805f61afea107215f5df50562fa1669a48334cc015570952e25e773a ./src/engine/energy.rs e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs @@ -151,9 +151,9 @@ c73691c3637465ab0f79aecf7f5428d0317799a8ab5329ed564cf57963bbe86e ./src/engine/r b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs 682c8b3956c657b655746cbd19ee97ee2f4cf953bc9de67d6d70a1b14f3dd746 ./src/engine/temperature.rs 833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs -0404e040c5d6f388682edb3d103a658e0b963a402e4e847fc92fd0aea1c0e773 ./src/engine/tests.rs +bf1a3840135a71ce64148559cce0fbd570ff99d31c8972cdea7447a49f6868f7 ./src/engine/tests.rs c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs -fb169bcc20f95abe9892367df6dd3a683584bdbc721792d6ffb9311b81ca30d9 ./src/engine/zone_control.rs +87ab87d434a6462a2c3f25249b42c59ecb2bca6ed700ec73e95e4613349cf16f ./src/engine/zone_control.rs bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs d681f2aa53c39f37411834c300e9200299f94ee4ea021f44f70a3ea1774bd698 ./src/home_assistant.rs bcd14e4d1af3ca580f3b95bcd86164c40f762511e1b8e2501cb8a598a6d8adf4 ./src/influxdb.rs @@ -163,13 +163,13 @@ da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb 62f9da6ef4f5fd92701d0857773f9f6236ef25356f21b9b42236abab6ba7da53 ./src/main.rs 23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs e7b6e49d369aaf665a8aea2464950fd795957de0ad7528a002e11c55c7983f35 ./src/models/automation.rs -fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/control_plan.rs +a7a35e60318a620d411ec16ab8faecd34410ccba3310f612e89c06904a16539b ./src/models/control_plan.rs 06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs 698dcbeba72037a251c25d07c95833f121716d949f9217f1f9429e8cd40638e5 ./src/models/device.rs aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs 6e44df6bde2f73c282095c80ac17287e5bef77d9fc1d344e440d863dae5f5031 ./src/models/history.rs 1fee89f939018bf1f3686c15f25434f01ab96b3239164108db7f348a1f1f8d82 ./src/models/integrations.rs -e557ee4a443de586a7ea7dd04318694fb30d6ec053076429503e18f1bcd65c9e ./src/models/runtime.rs +17cac849056b677a3872e600a0d99218fb383ad5ec91cde596562df8e2f47508 ./src/models/runtime.rs 41b85cff6fd234c8d6ea007b2c3c22df5fee5107dcd57c64dcc94a356ff70c12 ./src/models/settings_api.rs c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs @@ -195,26 +195,26 @@ a677ce61d3d0f4cf358055ddbc343e73b452c68daed80798499147370819615e ./src/provider 7e12e8704faa7ac15a312bcc1d714ee1c6f27c1dce0972d4ccd6e5a40c69c884 ./src/queries/maintenance.rs 7da8b73a1c8c30e2a12071062d36f5393938fde0b5ffebbe4af1891da40f3233 ./src/queries/schema.rs f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/zone_history.rs -8c34b70fb5f80e3249c2e0c63686e60ce1697cf44a7e6f11b3ad6f689ce17792 ./src/state.rs +d8a602ed8906d77593bd140315ce1bd56716038e789f53ff31762e9b3cb7220d ./src/state.rs b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html -2d227963624338f958033ec9b211b6bfa2c869ba3d7dc4f8f8125a29d343f7d9 ./web/css/styles.css +fd56aa620ddd63bf7f8d03f00da3b2bd53993240bb37cebdac1bdffec22d867c ./web/css/styles.css 183d122ba366303d9952ac0e68aff4bced4e0d9a468fbe51316c13f15227402b ./web/custom-chart.html e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg -92847143a358e6869ed4f21b156b10bda3f8abfcb9a12fc86256221c93be3b09 ./web/index.html +43f33fc21bb11df0ea149fe7fbca722a57f93890e4d9bb542ced703c1311207e ./web/index.html 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js-dynamic/README.md -e74bb06dba646f298a4d118b5c2dca6ba24f1179ce8ea8d53c1acd6000ca332c ./web/js-dynamic/bootstrap.js +e7318cd92b76e8a0109dc01adc2431474be17d8bb1488cdf6ffb39dc5450909d ./web/js-dynamic/bootstrap.js 5d57253a013fd1cb79e4b8701d4be6e394e3f2acbce73dfcd726a86e5878f1a1 ./web/js-dynamic/charts.js -81012bd88282034cdc4e876af5eb5057289f4db8505847568bc3c3701a348d4b ./web/js-dynamic/core.js -752b3c3b59558b42d867d4a15329238f0213f354b76f7011c10e31b4e6dbb311 ./web/js-dynamic/dashboard.js +07629b331bd498e8bea769d577321bbd12d9b076264242706d4ef16165fb22fa ./web/js-dynamic/core.js +1b444068bf91741c54bd51741a95128e335219275c415fe9ce4531c80593b50f ./web/js-dynamic/dashboard.js 930e67432f55e8476513e808edfa9a0809a8da7479307284fcc136082a4129a4 ./web/js-dynamic/entities.js -b4204241eecf387256364f4230b23ed50d947a362f4ceb1e528d69494e93d7cf ./web/js-dynamic/events.js +4612c958d7b9ccf89d6d45a11c3bc7e864bbd2ceaccc16fd7d76811ba4c74196 ./web/js-dynamic/events.js 43c78a3871cfdb0382081ec30b8d48650450c71b44d006414b161349ac1fa659 ./web/js-dynamic/flows.js 3ae714de6b71c068ba665c81a7689f994b1716284e8d937db669ad500f12cf63 ./web/js-dynamic/forms.js 92d013bb5d64f64aa8c55380704540a5f72732bde7ccd16554965e401daf50ea ./web/js-dynamic/history.js 511c4e7cc4c48fbbdab64558a9d1c27e6759c9b6dca255d11f301b9708e955c7 ./web/js-dynamic/main.js 30cbaebb817d5d416e1a372595b647aae42c14b9cc406c4751d0ced563243b69 ./web/js-dynamic/navigation.js -0e398bad2cde85d4a62d8733c06e73f2b6a5b07db791d4570feba38c7ff8741f ./web/js-dynamic/realtime.js +0c93119b3d5985d3440e1525d908efecd1567ab2caa0e6cc3babeecd87bfde5c ./web/js-dynamic/realtime.js 1b667f0a8bce824b27f56a8d2f44ff8feef9a011289591dc629ee288db70d137 ./web/js-dynamic/router.js c2710fd9c1f2b31878c1e25ceb02d74ae30456c1bbb400e69fcf7619ecc72bc2 ./web/js-dynamic/select-ui.js bc42b4a0dd0d20239ce22ea1dff09b3f94e8c0e1395e01251a4585a051968a8f ./web/js-dynamic/settings-ui.js diff --git a/docs/API.md b/docs/API.md index 1d8890c..8b21020 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,6 @@ # GREE Controller API reference -HTTP and WebSocket API for GREE Controller **0.15.14**. +HTTP and WebSocket API for GREE Controller **0.15.15**. [← Main documentation](../README.md) @@ -163,6 +163,7 @@ Common statuses: | POST | `/api/groups/{id}/control` | Group control enable/mode/preset/custom-temperature control. | | POST | `/api/house/control` | Set global thermostat mode. | | POST | `/api/house/power` | Bulk ON/OFF for all thermostats and enabled units; no persistent global gate. | +| POST | `/api/house/emergency-stop` | Persistently pause/resume automation; activation sends one-shot OFF to enabled units. | | POST | `/api/house/preset` | Set/clear whole-house preset override. | ### Schedules and automations @@ -225,6 +226,7 @@ Common statuses: | POST | `/api/integrations/home-assistant/house/control` | Restricted house mode. | | POST | `/api/integrations/home-assistant/house/preset` | Restricted house preset. | | POST | `/api/integrations/home-assistant/house/power` | Restricted bulk all-thermostat/all-unit power action. | +| POST | `/api/integrations/home-assistant/house/emergency-stop` | Restricted persistent emergency automation pause/resume. | | POST | `/api/integrations/home-assistant/zones/{id}/control` | Restricted thermostat-zone control. | --- @@ -241,7 +243,7 @@ Response: { "status": "ok", "name": "gree-controller", - "version": "0.15.14", + "version": "0.15.15", "uptime_seconds": 1234, "control_ready": true, "time": "2026-08-30T06:54:00Z" @@ -304,7 +306,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.15.14", + "version": "0.15.15", "uptime_seconds": 1234, "auth_required": false, "control_ready": true, @@ -845,6 +847,20 @@ Response includes: } ``` +### `POST /api/house/emergency-stop` + +```json +{ + "active": true +} +``` + +`true` persistently pauses thermostat/schedule/automation execution, clears stale compressor-protection tasks, and makes a one-shot best-effort OFF request to every enabled unit. The pause survives controller restarts, but startup does **not** replay OFF commands. Manual/direct device control remains available while automation is paused. + +`false` releases only the automation safety gate. It does not force devices ON; the controller immediately evaluates current schedules, temperatures and ownership again. + +Response includes `active`, `since`, `changed`, `failed`, and `cleared_queues`. + ### `POST /api/house/preset` ```json @@ -1489,6 +1505,10 @@ Same `{ "preset": "auto|comfort|sleep|away" }` semantics. Same `{ "power": true|false }` semantics. +### `POST /api/integrations/home-assistant/house/emergency-stop` + +Same `{ "active": true|false }` emergency-stop semantics as the administrator endpoint. + ### `POST /api/integrations/home-assistant/zones/{id}/control` Same `ZoneControlPatch` thermostat semantics as the normal zone control endpoint. The internal source is recorded as Home Assistant thermostat control. diff --git a/docs/openapi.json b/docs/openapi.json index 6c61f98..82c01cf 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "GREE Controller API", - "version": "0.15.14", + "version": "0.15.15", "summary": "Local HTTP/WebSocket API for GREE Controller", "description": "Local API used by the GREE Controller Web UI and Home Assistant integration.", "license": { @@ -1814,6 +1814,62 @@ } } }, + "/api/house/emergency-stop": { + "post": { + "tags": [ + "House" + ], + "summary": "Emergency automation stop", + "description": "Persistently pauses thermostat and automation execution. Activating the stop first stores the safety gate, clears pending compressor-protection tasks, then makes a one-shot best-effort OFF request to every enabled unit. The pause survives controller restarts, but OFF commands are not replayed on startup. Releasing the stop does not force devices ON; current control conditions are evaluated again.", + "operationId": "setHouseEmergencyStop", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HouseEmergencyStopPatch" + } + } + } + }, + "security": [ + { + "BearerToken": [] + }, + { + "ApiTokenHeader": [] + } + ], + "responses": { + "200": { + "description": "Emergency stop state and one-shot OFF result", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "502": { + "$ref": "#/components/responses/BadGateway" + }, + "500": { + "$ref": "#/components/responses/InternalError" + }, + "424": { + "$ref": "#/components/responses/FailedDependency" + } + } + } + }, "/api/house/preset": { "post": { "tags": [ @@ -3937,6 +3993,62 @@ } } }, + "/api/integrations/home-assistant/house/emergency-stop": { + "post": { + "tags": [ + "Home Assistant" + ], + "summary": "Emergency automation stop from Home Assistant", + "description": "Restricted Home Assistant surface for the same persistent emergency-stop semantics as the administrator endpoint. Activation pauses automatic control across restarts and sends OFF only once; release resumes evaluation without forcing devices ON.", + "operationId": "haSetHouseEmergencyStop", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HouseEmergencyStopPatch" + } + } + } + }, + "security": [ + { + "BearerToken": [] + }, + { + "ApiTokenHeader": [] + } + ], + "responses": { + "200": { + "description": "Emergency stop state and one-shot OFF result", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "502": { + "$ref": "#/components/responses/BadGateway" + }, + "500": { + "$ref": "#/components/responses/InternalError" + }, + "424": { + "$ref": "#/components/responses/FailedDependency" + } + } + } + }, "/api/integrations/home-assistant/zones/{id}/control": { "post": { "tags": [ @@ -5967,7 +6079,7 @@ }, "version": { "type": "string", - "example": "0.15.14" + "example": "0.15.15" }, "uptime_seconds": { "type": "integer", @@ -7412,6 +7524,19 @@ ], "additionalProperties": false }, + "HouseEmergencyStopPatch": { + "type": "object", + "properties": { + "active": { + "type": "boolean", + "example": true + } + }, + "required": [ + "active" + ], + "additionalProperties": false + }, "HousePresetPatch": { "type": "object", "properties": { 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 47b971f..1bbab5c 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.15.14", + "version": "0.15.15", "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 2cf41e7..f3dde83 100644 --- a/ha-addon/repository/gree-controller/CHANGELOG.md +++ b/ha-addon/repository/gree-controller/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.15.15 + +- Adds a persistent emergency STOP to Dashboard → Global power. Activating it stores a restart-resistant safety gate, clears queued automatic compressor actions, pauses schedules/thermostat automation and makes a one-time attempt to power off enabled units; startup does not replay the OFF command. +- Adds a green return-to-normal action that releases the safety gate and immediately re-evaluates current conditions without forcing devices ON. +- Expands Control plan with a full-width “why the controller is acting this way” section showing per-zone control source/reason and the most recently triggered automations with trigger context. +- Keeps emergency-stop state operationally separate from configuration backup/import, and documents the new `POST /api/house/emergency-stop` endpoint in the API/OpenAPI contract. + ## 0.15.14 - Fixes Home Assistant energy sampling in the packaged add-on when authentication comes from `SUPERVISOR_TOKEN`; background sampling no longer requires persisted manual URL/token fields. diff --git a/ha-addon/repository/gree-controller/README.md b/ha-addon/repository/gree-controller/README.md index 7bd5fe1..63f96b8 100644 --- a/ha-addon/repository/gree-controller/README.md +++ b/ha-addon/repository/gree-controller/README.md @@ -2,7 +2,7 @@ Local GREE HVAC controller packaged as a Home Assistant add-on, with Web UI, HTTP/WebSocket API, built-in Swagger API documentation, UDP discovery/control, `amd64` and `aarch64` support, ingress, and database backup from `/data/gree-controller.db`. -Version 0.15.14 fixes Home Assistant Supervisor authentication detection for background energy sampling and centralizes runtime HA readiness checks, so the add-on uses Supervisor credentials consistently across entity discovery, sensor reads, Flow integrations and energy collection. +Version 0.15.15 adds a persistent emergency STOP in Global power and expands Control plan with per-zone reasons and recent automation trigger context, so the add-on clearly shows why climate control is running, waiting or paused. ![Home Assistant OS + GREE + VLAN topology](https://git.linuxiarz.pl/gru/gree-controller-ha-addon/raw/branch/master/gree-controller/topology-ha-vlan-gree-en.png) diff --git a/ha-addon/repository/gree-controller/config.yaml b/ha-addon/repository/gree-controller/config.yaml index e2be068..3745fc3 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.15.14" +version: "0.15.15" 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 ab00a85..1ebd36d 100644 --- a/lang/en.json +++ b/lang/en.json @@ -441,9 +441,33 @@ "plan.loading": "Loading current automation plan…", "plan.rules": "Extra automations", "plan.ruleCount": "{count} active rules", + "plan.ruleCountPaused": "Rules paused by emergency STOP: {count}", "plan.event": "Change", "plan.groupSummary": "Group zones: {zones} · requesting action: {demand}", "plan.groupOff": "Group off — group units are powered down and group ownership is released.", + "plan.reasonsEyebrow": "Control reason", + "plan.reasonsTitle": "Why the controller is acting this way", + "plan.currentReasons": "Current reasons", + "plan.recentTriggers": "Recently triggered automations", + "plan.noReasons": "No active control reasons.", + "plan.reasonEmergency": "Automation is paused by the emergency stop.", + "plan.reasonManual": "Direct manual device control has priority.", + "plan.reasonDisabled": "The zone is disabled.", + "plan.reasonOffline": "The device is offline or has a communication error.", + "plan.reasonNoIntent": "No active schedule or explicit thermostat request.", + "plan.reasonLocalOff": "The local thermostat explicitly keeps this zone off.", + "plan.reasonLocalOn": "The local thermostat owns this zone.", + "plan.reasonHeatDemand": "{current} is below the {target} target — heating demand is active.", + "plan.reasonCoolDemand": "{current} is above the {target} target — cooling demand is active.", + "plan.reasonTargetSatisfied": "Temperature {current}; target {target} — no extra demand is required.", + "plan.reasonSchedule": "Active schedule: {schedule}.", + "plan.reasonWaiting": "The controller is waiting for a condition that requires action.", + "plan.reasonSourceSchedule": "Schedule: {schedule}", + "plan.reasonSourceManual": "Manual control", + "plan.reasonSourceThermostat": "Local thermostat", + "plan.reasonSourceAutomation": "Automation", + "plan.reasonTriggerReading": "{trigger} · now {current}", + "plan.reasonLastTriggered": "Last triggered: {when}", "zones.enable": "Enable zone", "zones.disable": "Disable zone", "history.90d": "90 days", @@ -626,6 +650,16 @@ "house.powerOnDone": "All thermostats enabled and ON sent to all units", "house.powerOffDone": "All units turned off; thermostats remain locally off", "house.powerPartial": "Could not change power on {count} devices", + "house.emergencyStop": "Emergency STOP", + "house.emergencyStopHint": "Pauses all automation and makes a one-shot attempt to power off every enabled unit.", + "house.emergencyStopConfirm": "Activate the emergency stop? Automation will remain persistently paused until you resume it manually, and the controller will try to power off all units.", + "house.emergencyResume": "Return to normal mode", + "house.emergencyResumeHint": "Releases the automation pause. It does not force devices on; the controller evaluates current conditions again.", + "house.emergencyActive": "Emergency stop is active", + "house.emergencyActiveHint": "The state survives controller restarts. After a restart automation remains blocked, but the OFF command is not replayed.", + "house.emergencyStopped": "Emergency stop active — automation paused", + "house.emergencyStoppedPartial": "Emergency stop is active, but {count} devices could not be powered off", + "house.emergencyResumed": "Normal automation mode restored", "preset.manual": "Manual", "nav.groups": "Groups", "groups.eyebrow": "Group control", diff --git a/lang/pl.json b/lang/pl.json index be4b991..79ceb0d 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -441,9 +441,33 @@ "plan.loading": "Wczytywanie aktualnego planu automatyki…", "plan.rules": "Dodatkowe automatyzacje", "plan.ruleCount": "Aktywne reguły: {count}", + "plan.ruleCountPaused": "Reguły wstrzymane przez STOP: {count}", "plan.event": "Zmiana", "plan.groupSummary": "Strefy w grupie: {zones} · żądające działania: {demand}", "plan.groupOff": "Grupa wyłączona — jednostki grupy są wyłączone, a blokada grupowa jest zwolniona.", + "plan.reasonsEyebrow": "Powód sterowania", + "plan.reasonsTitle": "Dlaczego sterownik działa właśnie tak", + "plan.currentReasons": "Bieżące powody", + "plan.recentTriggers": "Ostatnio wyzwolone automatyzacje", + "plan.noReasons": "Brak aktywnych powodów sterowania.", + "plan.reasonEmergency": "Automatyka wstrzymana przez STOP awaryjny.", + "plan.reasonManual": "Ręczne sterowanie urządzeniem ma pierwszeństwo.", + "plan.reasonDisabled": "Strefa jest wyłączona.", + "plan.reasonOffline": "Urządzenie jest offline lub ma błąd komunikacji.", + "plan.reasonNoIntent": "Brak aktywnego harmonogramu lub jawnego żądania termostatu.", + "plan.reasonLocalOff": "Lokalny termostat jawnie utrzymuje strefę wyłączoną.", + "plan.reasonLocalOn": "Lokalny termostat przejął sterowanie strefą.", + "plan.reasonHeatDemand": "{current} jest poniżej celu {target} — aktywne żądanie grzania.", + "plan.reasonCoolDemand": "{current} jest powyżej celu {target} — aktywne żądanie chłodzenia.", + "plan.reasonTargetSatisfied": "Temperatura {current}; cel {target} — brak potrzeby zwiększania mocy.", + "plan.reasonSchedule": "Aktywny harmonogram: {schedule}.", + "plan.reasonWaiting": "Sterownik czeka na warunek wymagający działania.", + "plan.reasonSourceSchedule": "Harmonogram: {schedule}", + "plan.reasonSourceManual": "Sterowanie ręczne", + "plan.reasonSourceThermostat": "Termostat lokalny", + "plan.reasonSourceAutomation": "Automatyka", + "plan.reasonTriggerReading": "{trigger} · teraz {current}", + "plan.reasonLastTriggered": "Ostatnie wyzwolenie: {when}", "zones.enable": "Włącz strefę", "zones.disable": "Wyłącz strefę", "history.90d": "90 dni", @@ -626,6 +650,16 @@ "house.powerOnDone": "Włączono wszystkie termostaty i wysłano polecenie włączenia jednostek", "house.powerOffDone": "Wyłączono wszystkie jednostki; termostaty pozostają lokalnie wyłączone", "house.powerPartial": "Nie udało się zmienić zasilania {count} urządzeń", + "house.emergencyStop": "STOP awaryjny", + "house.emergencyStopHint": "Wstrzymuje całą automatykę i jednorazowo próbuje wyłączyć wszystkie aktywne jednostki.", + "house.emergencyStopConfirm": "Włączyć STOP awaryjny? Automatyka zostanie trwale wstrzymana do ręcznego wznowienia, a sterownik spróbuje wyłączyć wszystkie jednostki.", + "house.emergencyResume": "Powrót do normalnego trybu", + "house.emergencyResumeHint": "Odblokowuje automatykę. Nie wymusza włączenia urządzeń; sterownik ponownie oceni aktualne warunki.", + "house.emergencyActive": "STOP awaryjny jest aktywny", + "house.emergencyActiveHint": "Stan przetrwa restart kontrolera. Po restarcie automatyka nadal pozostaje zablokowana, ale polecenie OFF nie jest wysyłane ponownie.", + "house.emergencyStopped": "STOP awaryjny aktywny — automatyka wstrzymana", + "house.emergencyStoppedPartial": "STOP awaryjny aktywny, ale nie udało się wyłączyć {count} urządzeń", + "house.emergencyResumed": "Przywrócono normalny tryb automatyki", "preset.manual": "Ręcznie", "nav.groups": "Grupy", "groups.eyebrow": "Sterowanie grupami", diff --git a/scripts/api_dev_test.py b/scripts/api_dev_test.py index 63841bb..fbe7807 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.15.14. +"""Developer smoke/integration tests for GREE Controller API 0.15.15. 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.15.14" +DEFAULT_EXPECTED_VERSION = "0.15.15" 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.15.14 contract", lambda: test_openapi(client, args.expected_version)) + runner.run("OpenAPI 0.15.15 contract", lambda: test_openapi(client, args.expected_version)) runner.run("bootstrap snapshot contract", lambda: test_bootstrap_contract(client)) runner.run("protected API requires auth", lambda: test_protected_auth(client)) diff --git a/src/api.rs b/src/api.rs index acfaa36..dae44d8 100644 --- a/src/api.rs +++ b/src/api.rs @@ -133,6 +133,7 @@ pub fn router(state: AppState) -> Router { .route("/api/groups/{id}/control", post(update_group_control)) .route("/api/house/control", post(update_house_control)) .route("/api/house/power", post(update_house_power)) + .route("/api/house/emergency-stop", post(update_house_emergency_stop)) .route("/api/house/preset", post(update_house_preset)) .route("/api/schedules", get(list_schedules).post(create_schedule)) .route( @@ -297,6 +298,10 @@ pub fn router(state: AppState) -> Router { "/api/integrations/home-assistant/house/power", post(update_house_power), ) + .route( + "/api/integrations/home-assistant/house/emergency-stop", + post(update_house_emergency_stop), + ) .route( "/api/integrations/home-assistant/zones/{id}/control", post(update_home_assistant_zone_control), diff --git a/src/api/configuration.rs b/src/api/configuration.rs index 8a829ce..db07dd9 100644 --- a/src/api/configuration.rs +++ b/src/api/configuration.rs @@ -22,6 +22,10 @@ async fn export_configuration( // including configuration exports. Import can reuse the already configured password on the // target controller when account/region match. export.settings.gree_cloud.password.clear(); + // Emergency STOP is live safety state, not configuration. Never transfer it in backups: + // exporting/importing configuration must not arm or release the controller safety gate. + export.settings.emergency_stop_enabled = false; + export.settings.emergency_stop_since = None; Ok(Json(export)) } @@ -958,6 +962,14 @@ async fn import_configuration( let _resource_guards = lock_configuration_resources(&state, ¤t_zones, ¤t_devices, &export).await; + // Keep the operational safety gate independent from configuration replacement. The locks + // above serialize this snapshot against the emergency-stop endpoint. + { + let settings = state.settings.read().await; + export.settings.emergency_stop_enabled = settings.emergency_stop_enabled; + export.settings.emergency_stop_since = settings.emergency_stop_since.clone(); + } + power_off_detached_devices(&state, ¤t_zones, &export).await?; sanitize_configuration_runtime(&mut export); state diff --git a/src/api/house.rs b/src/api/house.rs index a9f639e..d9a4104 100644 --- a/src/api/house.rs +++ b/src/api/house.rs @@ -106,6 +106,38 @@ async fn command_all_enabled_devices_power( Ok(failed) } +async fn clear_all_automation_compressor_queues(state: &AppState) -> Result { + let mut zone_ids: Vec = state + .db + .list_zones()? + .into_iter() + .map(|zone| zone.id) + .collect(); + zone_ids.sort(); + zone_ids.dedup(); + let mut changed = 0usize; + for zone_id in zone_ids { + let _zone_guard = state.lock_zone_operation(&zone_id).await; + let Some(mut zone) = state.db.get_zone(&zone_id)? else { + continue; + }; + if zone.compressor_pending_action.is_none() + && zone.compressor_cancelled_action.is_none() + && zone.lockout_until.is_none() + && zone.lockout_reason.is_none() + { + continue; + } + engine::rearm_compressor_queue(&mut zone); + zone.revision = zone.revision.saturating_add(1); + zone.updated_at = Utc::now(); + state.db.save_zone(&zone)?; + state.broadcast("zone.updated", serde_json::to_value(&zone)?); + changed += 1; + } + Ok(changed) +} + async fn update_house_control( State(state): State, Json(input): Json, @@ -155,6 +187,96 @@ struct HousePowerPatch { power: bool, } +#[derive(Debug, Deserialize)] +struct HouseEmergencyStopPatch { + active: bool, +} + +async fn update_house_emergency_stop( + State(state): State, + Json(input): Json, +) -> Result, AppError> { + // Automation execution already uses this lock before committing an action. Taking it first + // gives the emergency stop a clean barrier: an in-flight action finishes, then no newer + // automatic action can cross the persisted safety flag. + let _automation_guard = state.lock_automation_operation().await; + let _house_guard = state.lock_house_operation().await; + // Persist the safety gate while automatic control is serialized. Once the flag is stored, + // neither the background regulator nor an immediate thermostat run can emit automation + // commands until the user explicitly resumes normal operation. + let cycle_guard = state.lock_zone_control_cycle().await; + let (changed, emergency_stop_since) = { + let mut settings = state.settings.write().await; + let changed = settings.emergency_stop_enabled != input.active; + if changed { + settings.emergency_stop_enabled = input.active; + settings.emergency_stop_since = if input.active { Some(Utc::now()) } else { None }; + state.db.save_runtime_settings(&settings)?; + } + (changed, settings.emergency_stop_since.clone()) + }; + + // OFF is intentionally a one-shot side effect of activating the emergency stop. The + // persistent flag survives restarts, but startup never replays physical commands from it. + let mut failed = Vec::new(); + let mut cleared_queues = 0usize; + if input.active && changed { + cleared_queues = clear_all_automation_compressor_queues(&state).await?; + failed = command_all_enabled_devices_power(&state, false, "house_emergency_stop").await?; + } + + let payload = json!({ + "active": input.active, + "since": emergency_stop_since, + "changed": changed, + "failed": failed, + "cleared_queues": cleared_queues, + }); + state.broadcast("house.emergency_stop_changed", payload.clone()); + drop(cycle_guard); + state.wake_zone_control(); + + // Resuming does not force any unit ON. It only releases the persistent safety gate and + // immediately re-evaluates current schedules, temperatures and ownership from fresh state. + if !input.active + && changed + && state.initial_device_sync_complete.load(Ordering::Acquire) + { + if let Err(err) = engine::run_zone_control_now(&state).await { + state.log( + "error", + "house.emergency_resume_control_error", + &err.to_string(), + json!({"active": false}), + ); + } + } + + state.log( + "info", + if input.active { + "house.emergency_stop_activated" + } else { + "house.emergency_stop_released" + }, + if input.active { + "Emergency stop activated; automatic climate control paused" + } else { + "Emergency stop released; automatic climate control resumed" + }, + json!({ + "active": input.active, + "changed": changed, + "failed_devices": payload["failed"].as_array().map(Vec::len).unwrap_or(0), + "cleared_queues": cleared_queues, + "persistent_across_restart": true, + "off_replayed_on_restart": false, + }), + ); + + Ok(Json(payload)) +} + async fn update_house_power( State(state): State, Json(input): Json, diff --git a/src/api/system.rs b/src/api/system.rs index 0e54fa0..7151a1f 100644 --- a/src/api/system.rs +++ b/src/api/system.rs @@ -1,6 +1,8 @@ #[derive(Debug, serde::Serialize)] struct HouseSnapshot { mode: String, + emergency_stop_enabled: bool, + emergency_stop_since: Option>, } #[derive(Debug, serde::Serialize)] @@ -140,9 +142,14 @@ async fn bootstrap(State(state): State) -> Result Result { - let (settings, house_mode) = { + let (settings, house_mode, emergency_stop_enabled, emergency_stop_since) = { let settings = state.settings.read().await; - (settings_snapshot(&settings), settings.house_mode.clone()) + ( + settings_snapshot(&settings), + settings.house_mode.clone(), + settings.emergency_stop_enabled, + settings.emergency_stop_since.clone(), + ) }; let devices = state.db.list_devices()?; let device_groups = state.db.list_device_groups()?; @@ -166,7 +173,11 @@ async fn build_bootstrap(state: &AppState) -> Result bool { } async fn run_automations(state: &AppState) -> Result<()> { + if state.settings.read().await.emergency_stop_enabled { + return Ok(()); + } let devices = state.db.list_devices()?; let mut automations = state.db.list_automations()?; // Stable arbitration for same-cycle conflicts: the oldest configured rule wins, then ID. @@ -51,6 +54,11 @@ async fn run_automations(state: &AppState) -> Result<()> { // changed since this cycle snapshot was taken, skip it now and evaluate the new // definition on the next cycle instead of firing stale configuration. let _automation_guard = state.lock_automation_operation().await; + // The emergency-stop handler takes the same lock before persisting its safety flag. + // Re-check here so a rule evaluated immediately before STOP can never execute after it. + if state.settings.read().await.emergency_stop_enabled { + continue; + } let Some(latest_item) = state.db.get_automation(&item.id)? else { continue; }; if latest_item.updated_at != item.updated_at { continue; } item = latest_item; diff --git a/src/engine/control_plan.rs b/src/engine/control_plan.rs index a4c2ef1..ba72380 100644 --- a/src/engine/control_plan.rs +++ b/src/engine/control_plan.rs @@ -97,6 +97,7 @@ async fn control_plan_cache_loop(state: AppState) { pub async fn build_control_plan(state: &AppState) -> Result { let settings = state.settings.read().await.clone(); + let emergency_stop_active = settings.emergency_stop_enabled; let schedules = state.db.list_schedules()?; let devices = state.db.list_devices()?; let zones = state.db.list_zones()?; @@ -131,12 +132,14 @@ pub async fn build_control_plan(state: &AppState) -> Result Result Result Result Result<()> { let _cycle_guard = state.lock_zone_control_cycle().await; - let schedules = state.db.list_schedules()?; let settings = state.settings.read().await.clone(); + if settings.emergency_stop_enabled { + return Ok(()); + } + let schedules = state.db.list_schedules()?; let mut zone_snapshot = state.db.list_zones()?; // 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) diff --git a/src/models/control_plan.rs b/src/models/control_plan.rs index 1a38ff1..dfea6eb 100644 --- a/src/models/control_plan.rs +++ b/src/models/control_plan.rs @@ -99,6 +99,8 @@ pub struct AutomationPlanRule { pub struct ControlPlan { pub generated_at: DateTime, pub house_mode: String, + pub emergency_stop_active: bool, + pub emergency_stop_since: Option>, /// Uniform whole-house preset when every zone uses the same override; None for a mixed state. pub house_preset: Option, /// Compatibility field. Global ON/OFF does not use a persistent master gate; this remains true in v0.9.4+. diff --git a/src/models/runtime.rs b/src/models/runtime.rs index e7b9bed..a243400 100644 --- a/src/models/runtime.rs +++ b/src/models/runtime.rs @@ -67,6 +67,12 @@ pub struct RuntimeSettings { /// Global seasonal mode. Zones follow this by default. Values: cool/heat/off; off pauses house-level thermostat control. #[serde(default = "default_house_mode")] pub house_mode: String, + /// Persistent emergency pause for all automatic climate control. The flag survives + /// process restarts; physical OFF commands are sent only when the pause is activated. + #[serde(default)] + pub emergency_stop_enabled: bool, + #[serde(default)] + pub emergency_stop_since: Option>, /// `setpoint` keeps units powered and modulates compressor demand by changing target temperature. #[serde(default = "default_control_strategy")] pub control_strategy: String, diff --git a/src/state.rs b/src/state.rs index 81ad8a1..cab75a3 100644 --- a/src/state.rs +++ b/src/state.rs @@ -240,7 +240,10 @@ fn control_plan_event_affects_plan(event: &str) -> bool { || event.starts_with("settings.") || matches!( event, - "house.mode_changed" | "outdoor.updated" | "configuration.imported" + "house.mode_changed" + | "house.emergency_stop_changed" + | "outdoor.updated" + | "configuration.imported" ) } @@ -259,6 +262,7 @@ mod tests { "flow.updated", "settings.night.updated", "house.mode_changed", + "house.emergency_stop_changed", "outdoor.updated", "configuration.imported", ] { diff --git a/web/css/styles.css b/web/css/styles.css index ca1bbcb..1631a4f 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -1885,6 +1885,25 @@ button.outside-pill:focus-visible { font-weight: 800; } +.house-power-actions .house-emergency-stop { + border-color: color-mix(in srgb, var(--warning) 58%, var(--line)); + color: var(--warning); + background: var(--warning-soft); + font-weight: 800; +} + +.house-power-actions .house-emergency-stop:hover { + border-color: var(--warning); + background: color-mix(in srgb, var(--warning-soft) 72%, var(--surface-2)); +} + +.house-power-actions .house-emergency-stop.resume, +.house-power-actions .house-emergency-stop.resume.active { + border-color: var(--accent-border); + color: var(--accent-text); + background: var(--accent); +} + .zone-thermostat { overflow: hidden; } @@ -4795,6 +4814,93 @@ body.simulation-standalone [data-view="simulation"] { gap: 8px; } +#controlPlan .plan-reasons { + grid-column: 1 / -1; + gap: 12px; + padding: 14px; +} + +#controlPlan .plan-reason-subsection { + display: grid; + gap: 7px; +} + +#controlPlan .plan-reason-subsection h4 { + margin: 0; + color: var(--muted); + font-size: 10px; + font-weight: 800; + letter-spacing: .08em; + text-transform: uppercase; +} + +#controlPlan .plan-reason-list { + display: grid; + gap: 6px; + margin: 0; + padding: 0; + list-style: none; +} + +#controlPlan .plan-reason-list li { + display: grid; + grid-template-columns: minmax(130px, .75fr) minmax(0, 1.8fr); + gap: 14px; + align-items: center; + padding: 8px 10px; + border: 1px solid var(--line); + border-radius: 10px; + background: var(--surface-muted); +} + +#controlPlan .plan-reason-list li>div { + display: grid; + gap: 2px; + min-width: 0; +} + +#controlPlan .plan-reason-list strong, +#controlPlan .plan-reason-list small, +#controlPlan .plan-reason-list span { + min-width: 0; +} + +#controlPlan .plan-reason-list strong { + font-size: 12px; +} + +#controlPlan .plan-reason-list small { + color: var(--muted); + font-size: 10px; +} + +#controlPlan .plan-reason-list span { + color: var(--text-soft); + font-size: 11px; + line-height: 1.4; +} + +#controlPlan .plan-reason-alert { + display: grid; + gap: 3px; + padding: 10px 12px; + border: 1px solid color-mix(in srgb, var(--warning) 48%, var(--line)); + border-radius: 11px; + background: var(--warning-soft); +} + +#controlPlan .plan-reason-alert strong { + color: var(--warning); + font-size: 12px; +} + +#controlPlan .plan-reason-alert span, +#controlPlan .plan-reason-empty { + color: var(--muted); + font-size: 11px; + line-height: 1.4; +} + #controlPlan .plan-events .plan-rule-summary { display: grid; gap: 2px; @@ -10812,3 +10918,10 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr grid-column: 2; } } + +@media (max-width: 620px) { + #controlPlan .plan-reason-list li { + grid-template-columns: 1fr; + gap: 5px; + } +} diff --git a/web/index.html b/web/index.html index c8629d6..fa831ce 100644 --- a/web/index.html +++ b/web/index.html @@ -85,7 +85,8 @@
+ data-i18n="house.powerOffAll">Turn all off
diff --git a/web/js-dynamic/bootstrap.js b/web/js-dynamic/bootstrap.js index b77bdcc..e21d8f2 100644 --- a/web/js-dynamic/bootstrap.js +++ b/web/js-dynamic/bootstrap.js @@ -45,6 +45,7 @@ function applyBootstrapSnapshot(data) { app.automations = data.automations || []; app.flows = data.flows || []; app.accessTokens = data.access_tokens || []; + app.house = data.house || { mode: 'cool', emergency_stop_enabled: false, emergency_stop_since: null }; app.settings = settingsFromSnapshot(data.settings, data.house?.mode || 'cool'); app.sensorAliases = { ...(app.settings.home_assistant?.sensor_aliases || {}) }; app.flowSharedInputs = JSON.parse(JSON.stringify(app.settings.home_assistant?.flow_inputs || [])); diff --git a/web/js-dynamic/core.js b/web/js-dynamic/core.js index 60b8dd2..263b34a 100644 --- a/web/js-dynamic/core.js +++ b/web/js-dynamic/core.js @@ -25,7 +25,7 @@ const preferredLanguage = getCookie('gree_controller_language') || DEFAULT_LANGU const preferredTheme = ['system', 'light', 'dark'].includes(getCookie('gree_controller_theme')) ? getCookie('gree_controller_theme') : 'system'; const app = { - devices: [], zones: [], groups: [], deviceGroups: [], deviceGroupEnergy: {}, schedules: [], automations: [], flows: [], accessTokens: [], settings: null, system: {}, outdoorTemperature: null, + devices: [], zones: [], groups: [], deviceGroups: [], deviceGroupEnergy: {}, schedules: [], automations: [], flows: [], accessTokens: [], settings: null, house: { mode: 'cool', emergency_stop_enabled: false, emergency_stop_since: null }, system: {}, outdoorTemperature: null, token: localStorage.getItem('gree_controller_token') || '', authTokenSubmitted: false, ws: null, wsTimer: null, wsErrorTimer: null, currentView: 'dashboard', loading: false, bootstrapReloadPending: false, language: preferredLanguage, theme: preferredTheme, connectionStatus: 'connecting', languages: [], translations: {}, locales: {}, defaultLanguage: DEFAULT_LANGUAGE, diff --git a/web/js-dynamic/dashboard.js b/web/js-dynamic/dashboard.js index 6fc27dc..2d94c66 100644 --- a/web/js-dynamic/dashboard.js +++ b/web/js-dynamic/dashboard.js @@ -23,7 +23,8 @@ function renderSummary() { const average = temperatures.length ? temperatures.reduce((a, b) => a + b, 0) / temperatures.length : null; const online = app.devices.filter(d => d.online).length; const active = app.devices.filter(d => d.power).length; - const demand = app.zones.filter(z => z.enabled && z.demand).length; + const emergencyActive = !!(app.house?.emergency_stop_enabled || app.controlPlan?.emergency_stop_active); + const demand = emergencyActive ? 0 : app.zones.filter(z => z.enabled && z.demand).length; const toolbarStatus = $('#toolbarRuntimeStatus'); if (toolbarStatus) toolbarStatus.textContent = tr('dashboard.toolbarWorking', { active, total: app.devices.length }); $('#heroTemperature').innerHTML = `${average === null ? '--' : average.toFixed(1)}°C`; @@ -46,9 +47,23 @@ function renderHouseClimate() { const preset = zonePresets.length && zonePresets.every(value => value === zonePresets[0]) ? zonePresets[0] : null; const outdoor = app.outdoorTemperature == null ? tr('common.unavailable') : fmtTemp(app.outdoorTemperature); const powerOn = $('#housePowerOn'), powerOff = $('#housePowerOff'); + const emergencyButton = $('#houseEmergencyStop'); + const emergencyActive = !!(app.house?.emergency_stop_enabled || app.controlPlan?.emergency_stop_active); + const hasAutomaticControl = (app.zones || []).some(zone => zone.enabled) + || (app.schedules || []).some(item => item.enabled) + || (app.automations || []).some(item => item.enabled); if (powerOn) { powerOn.classList.remove('active'); powerOn.removeAttribute('aria-pressed'); } if (powerOff) { powerOff.classList.remove('active'); powerOff.removeAttribute('aria-pressed'); } + if (emergencyButton) { + emergencyButton.hidden = !emergencyActive && !hasAutomaticControl; + emergencyButton.dataset.value = emergencyActive ? 'false' : 'true'; + emergencyButton.classList.toggle('resume', emergencyActive); + emergencyButton.classList.toggle('active', emergencyActive); + emergencyButton.setAttribute('aria-pressed', String(emergencyActive)); + emergencyButton.textContent = tr(emergencyActive ? 'house.emergencyResume' : 'house.emergencyStop'); + emergencyButton.title = tr(emergencyActive ? 'house.emergencyResumeHint' : 'house.emergencyStopHint'); + } node.innerHTML = `
${esc(tr('house.seasonMode'))}

${esc(tr('house.smartThermostat'))}

${esc(tr('house.setpointStrategy'))}

@@ -58,6 +73,68 @@ function renderHouseClimate() {
${['auto', 'comfort', 'sleep', 'away'].map(p => ``).join('')}
`; } +function planBlockedReasonLabel(reason) { + const key = { + emergency_stop: 'plan.reasonEmergency', + manual_override: 'plan.reasonManual', + zone_disabled: 'plan.reasonDisabled', + offline: 'plan.reasonOffline', + no_active_thermostat_intent: 'plan.reasonNoIntent', + }[reason]; + return key ? tr(key) : reason; +} + +function planZoneReason(zone) { + if (zone.blocked_reason) return planBlockedReasonLabel(zone.blocked_reason); + if (zone.local_thermostat_power === false) return tr('plan.reasonLocalOff'); + if (zone.local_thermostat_power === true) return tr('plan.reasonLocalOn'); + + const current = zone.current_temperature == null ? NaN : Number(zone.current_temperature); + const target = zone.target_temperature == null ? NaN : Number(zone.target_temperature); + if (zone.demand && Number.isFinite(current) && Number.isFinite(target)) { + if (zone.mode === 'heat') return tr('plan.reasonHeatDemand', { current: fmtTemp(current), target: fmtTemp(target) }); + if (zone.mode === 'cool') return tr('plan.reasonCoolDemand', { current: fmtTemp(current), target: fmtTemp(target) }); + } + if (Number.isFinite(current) && Number.isFinite(target)) { + return tr('plan.reasonTargetSatisfied', { current: fmtTemp(current), target: fmtTemp(target) }); + } + if (zone.current_schedule_name) return tr('plan.reasonSchedule', { schedule: zone.current_schedule_name }); + return tr('plan.reasonWaiting'); +} + +function planReasonCard(plan) { + const zones = (plan.zones || []).map(zone => { + const reason = planZoneReason(zone); + const source = zone.control_owner === 'direct_manual' ? tr('plan.reasonSourceManual') + : zone.control_owner === 'local_thermostat' ? tr('plan.reasonSourceThermostat') + : zone.current_schedule_name ? tr('plan.reasonSourceSchedule', { schedule: zone.current_schedule_name }) + : tr('plan.reasonSourceAutomation'); + return `
  • ${esc(zone.zone_name)}${esc(source)}
    ${esc(reason)}
  • `; + }).join(''); + + const recentRules = (plan.rules || []) + .filter(rule => rule.enabled && rule.last_fired_at) + .sort((a, b) => new Date(b.last_fired_at) - new Date(a.last_fired_at)) + .slice(0, 4) + .map(rule => { + const when = new Date(rule.last_fired_at).toLocaleString(locale(), { weekday: 'short', hour: '2-digit', minute: '2-digit' }); + const triggerDevice = rule.trigger_device_id ? (app.devices || []).find(device => device.id === rule.trigger_device_id) : null; + const current = triggerDevice?.current_temperature == null ? null : Number(triggerDevice.current_temperature); + const trigger = Number.isFinite(current) + ? tr('plan.reasonTriggerReading', { trigger: automationTriggerLabel(rule), current: fmtTemp(current) }) + : automationTriggerLabel(rule); + return `
  • ${esc(rule.name)}${esc(trigger)}
    ${esc(tr('plan.reasonLastTriggered', { when }))}
  • `; + }).join(''); + + const emergency = plan.emergency_stop_active + ? `
    ${esc(tr('house.emergencyActive'))}${esc(tr('house.emergencyActiveHint'))}
    ` + : ''; + const rules = recentRules + ? `

    ${esc(tr('plan.recentTriggers'))}

      ${recentRules}
    ` + : ''; + return `
    ${esc(tr('plan.reasonsEyebrow'))}

    ${esc(tr('plan.reasonsTitle'))}

    ${emergency}

    ${esc(tr('plan.currentReasons'))}

      ${zones || `
    • ${esc(tr('plan.noReasons'))}
    • `}
    ${rules}
    `; +} + function planEventMarkup(event) { const when = event?.at ? new Date(event.at).toLocaleString(locale(), { weekday: 'short', hour: '2-digit', minute: '2-digit' }) : '—'; const target = event?.target_temperature == null ? '' : ` · ${fmtTemp(event.target_temperature)}`; @@ -121,7 +198,7 @@ function renderControlPlan() { }).join(''); const rules = (plan.rules || []).filter(rule => rule.enabled); - const ruleCard = rules.length ? `
    ${esc(tr('plan.rules'))}

    ${esc(tr('plan.ruleCount', { count: rules.length }))}

      ${rules.slice(0, 3).map(rule => { + const ruleCard = rules.length ? `
      ${esc(tr('plan.rules'))}

      ${esc(tr(plan.emergency_stop_active ? 'plan.ruleCountPaused' : 'plan.ruleCount', { count: rules.length }))}

        ${rules.slice(0, 3).map(rule => { const automation = (app.automations || []).find(item => item.id === rule.id); const flow = automation?.flow_id ? (app.flows || []).find(item => item.id === automation.flow_id) : null; const displayName = flow?.name || rule.name; @@ -129,7 +206,8 @@ function renderControlPlan() { return `
      • ${esc(displayName)}${target ? `→ ${esc(target)}` : ''}
      • `; }).join('')}
      ` : ''; - const content = house + groupCards + zones + ruleCard; + const reasonCard = planReasonCard(plan); + const content = house + groupCards + zones + ruleCard + reasonCard; host.innerHTML = content; if (section) section.hidden = !content; } diff --git a/web/js-dynamic/events.js b/web/js-dynamic/events.js index 131bbcb..96cc171 100644 --- a/web/js-dynamic/events.js +++ b/web/js-dynamic/events.js @@ -164,6 +164,23 @@ document.addEventListener('click', async event => { finally { button.disabled = false; } return; } + if (action === 'house-emergency-stop') { + const active = button.dataset.value === 'true'; + if (active && !confirm(tr('house.emergencyStopConfirm'))) return; + try { + button.disabled = true; + const result = await enqueueClimateControlTask(async () => { + const response = await api('/api/house/emergency-stop', { method: 'POST', body: { active } }); + await loadBootstrap(); + return response; + }); + const failed = Array.isArray(result.failed) ? result.failed.length : 0; + if (active && failed) toast(tr('house.emergencyStoppedPartial', { count: failed }), true); + else toast(tr(active ? 'house.emergencyStopped' : 'house.emergencyResumed')); + } catch (error) { toast(error.message, true); } + finally { button.disabled = false; } + return; + } if (action === 'house-preset') { try { const result = await enqueueClimateControlTask(async () => { diff --git a/web/js-dynamic/realtime.js b/web/js-dynamic/realtime.js index c627d76..706a8a1 100644 --- a/web/js-dynamic/realtime.js +++ b/web/js-dynamic/realtime.js @@ -111,7 +111,14 @@ async function handleWebSocketMessage(event) { if (!isFormDirty($('#homeAssistantForm'))) { app.sensorAliases = { ...(data.sensor_aliases || {}) }; app.flowSharedInputs = JSON.parse(JSON.stringify(data.flow_inputs || [])); renderHomeAssistantSettings(); } renderHouseClimate(); if (app.flowDraft) renderFlowEditor(); scheduleControlPlanLoad(); } - else if (message.event === 'house.mode_changed') { app.settings = app.settings || {}; app.settings.house_mode = data.mode || 'cool'; renderHouseClimate(); renderGroups(); scheduleControlPlanLoad(); } + else if (message.event === 'house.mode_changed') { app.settings = app.settings || {}; app.settings.house_mode = data.mode || 'cool'; app.house = app.house || {}; app.house.mode = data.mode || 'cool'; renderHouseClimate(); renderGroups(); scheduleControlPlanLoad(); } + else if (message.event === 'house.emergency_stop_changed') { + app.house = app.house || { mode: app.settings?.house_mode || 'cool' }; + app.house.emergency_stop_enabled = !!data.active; + app.house.emergency_stop_since = data.since || null; + renderHouseClimate(); + scheduleControlPlanLoad(); + } else if (message.event === 'outdoor.updated') { app.outdoorTemperature = Number.isFinite(Number(data.temperature)) ? Number(data.temperature) : null; renderHouseClimate(); scheduleControlPlanLoad(); } else if (message.event === 'configuration.imported') { await loadBootstrap(); return; } else if (message.event === 'gree.frame_received') {