This commit is contained in:
Mateusz Gruszczyński
2026-09-21 22:58:30 +02:00
parent 3b969c8754
commit d80005de33
33 changed files with 722 additions and 94 deletions
Generated
+1 -1
View File
@@ -719,7 +719,7 @@ dependencies = [
[[package]]
name = "gree-controller"
version = "0.15.14"
version = "0.15.15"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -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"
+32 -32
View File
@@ -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
+2 -2
View File
@@ -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`.
+31 -31
View File
@@ -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
+23 -3
View File
@@ -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.
+127 -2
View File
@@ -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": {
@@ -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",
@@ -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.
@@ -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)
@@ -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/"
+34
View File
@@ -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",
+34
View File
@@ -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",
+3 -3
View File
@@ -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))
+5
View File
@@ -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),
+12
View File
@@ -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, &current_zones, &current_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, &current_zones, &export).await?;
sanitize_configuration_runtime(&mut export);
state
+122
View File
@@ -106,6 +106,38 @@ async fn command_all_enabled_devices_power(
Ok(failed)
}
async fn clear_all_automation_compressor_queues(state: &AppState) -> Result<usize, AppError> {
let mut zone_ids: Vec<String> = 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<AppState>,
Json(input): Json<HouseControlPatch>,
@@ -155,6 +187,96 @@ struct HousePowerPatch {
power: bool,
}
#[derive(Debug, Deserialize)]
struct HouseEmergencyStopPatch {
active: bool,
}
async fn update_house_emergency_stop(
State(state): State<AppState>,
Json(input): Json<HouseEmergencyStopPatch>,
) -> Result<Json<Value>, 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<AppState>,
Json(input): Json<HousePowerPatch>,
+14 -3
View File
@@ -1,6 +1,8 @@
#[derive(Debug, serde::Serialize)]
struct HouseSnapshot {
mode: String,
emergency_stop_enabled: bool,
emergency_stop_since: Option<chrono::DateTime<Utc>>,
}
#[derive(Debug, serde::Serialize)]
@@ -140,9 +142,14 @@ async fn bootstrap(State(state): State<AppState>) -> Result<Json<BootstrapRespon
}
async fn build_bootstrap(state: &AppState) -> Result<BootstrapResponse, AppError> {
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<BootstrapResponse, AppError
flows: state.db.list_flows()?,
access_tokens: state.db.list_api_tokens()?,
settings,
house: HouseSnapshot { mode: house_mode },
house: HouseSnapshot {
mode: house_mode,
emergency_stop_enabled,
emergency_stop_since,
},
outdoor_temperature: *state.outdoor_temperature.read().await,
control_plan: control_plan.plan.as_ref().clone(),
control_plan_revision: control_plan.revision,
+2
View File
@@ -92,6 +92,8 @@ impl Config {
.unwrap_or(3)
.clamp(1, 10),
house_mode: env::var("GREE_CONTROLLER_HOUSE_MODE").unwrap_or_else(|_| "cool".into()),
emergency_stop_enabled: false,
emergency_stop_since: None,
control_strategy: "setpoint".into(),
outdoor_assist_enabled: env_bool("GREE_CONTROLLER_OUTDOOR_ASSIST_ENABLED")
.unwrap_or(true),
+8
View File
@@ -13,6 +13,9 @@ fn device_has_thermostat_zone(device_id: &str, zones: &[Zone]) -> 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;
+16 -5
View File
@@ -97,6 +97,7 @@ async fn control_plan_cache_loop(state: AppState) {
pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppError> {
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<ControlPlan, AppErro
let active_for_target = active_schedule_for_zone(&zone, &schedules, now);
let has_intent_source =
zone_has_thermostat_intent_source(&zone, active_for_target, Utc::now());
let automation_idle = zone.enabled
let automation_idle = !emergency_stop_active
&& zone.enabled
&& !zone.device_manual_override
&& zone.local_thermostat_power != Some(false)
&& configured_effective_mode != "off"
&& !has_intent_source;
let automatic_active = zone.enabled
let automatic_active = !emergency_stop_active
&& zone.enabled
&& !zone.device_manual_override
&& zone.local_thermostat_power != Some(false)
&& configured_effective_mode != "off"
@@ -175,7 +178,9 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
event.label = format!("{}: {}", zone.name, event.label);
house_events.push(event);
}
let effective_enabled = zone.enabled && zone.local_thermostat_power != Some(false);
let effective_enabled = !emergency_stop_active
&& zone.enabled
&& zone.local_thermostat_power != Some(false);
zones_out.push(ZoneControlPlan {
zone_id: zone.id.clone(),
zone_name: zone.name.clone(),
@@ -232,12 +237,16 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
control_command_source: zone.control_source.clone(),
control_since: zone.control_since,
resume_at: zone.control_resume_at,
control_reason: if automation_idle {
control_reason: if emergency_stop_active {
"Emergency stop pauses automatic climate control".into()
} else if automation_idle {
"Automation idle: no active schedule or explicit thermostat request".into()
} else {
zone.control_reason.clone()
},
blocked_reason: if zone.device_manual_override {
blocked_reason: if emergency_stop_active {
Some("emergency_stop".into())
} else if zone.device_manual_override {
Some("manual_override".into())
} else if zone
.lockout_until
@@ -327,6 +336,8 @@ pub async fn build_control_plan(state: &AppState) -> Result<ControlPlan, AppErro
Ok(ControlPlan {
generated_at: Utc::now(),
house_mode: settings.house_mode,
emergency_stop_active,
emergency_stop_since: settings.emergency_stop_since,
house_preset,
house_power,
outdoor_temperature: *state.outdoor_temperature.read().await,
+5
View File
@@ -48,6 +48,8 @@ mod tests {
let left = ControlPlan {
generated_at: generated_at.clone(),
house_mode: "cool".into(),
emergency_stop_active: false,
emergency_stop_since: None,
house_preset: Some("comfort".into()),
house_power: true,
outdoor_temperature: Some(24.0),
@@ -65,6 +67,9 @@ mod tests {
assert!(control_plans_semantically_equal(&left, &right).unwrap());
right.house_mode = "heat".into();
assert!(!control_plans_semantically_equal(&left, &right).unwrap());
right = left.clone();
right.emergency_stop_active = true;
assert!(!control_plans_semantically_equal(&left, &right).unwrap());
}
#[test]
+4 -1
View File
@@ -535,8 +535,11 @@ async fn handle_zone_pre_control_state(
async fn control_zones(state: &AppState) -> 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)
+2
View File
@@ -99,6 +99,8 @@ pub struct AutomationPlanRule {
pub struct ControlPlan {
pub generated_at: DateTime<Utc>,
pub house_mode: String,
pub emergency_stop_active: bool,
pub emergency_stop_since: Option<DateTime<Utc>>,
/// Uniform whole-house preset when every zone uses the same override; None for a mixed state.
pub house_preset: Option<String>,
/// Compatibility field. Global ON/OFF does not use a persistent master gate; this remains true in v0.9.4+.
+6
View File
@@ -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<DateTime<Utc>>,
/// `setpoint` keeps units powered and modulates compressor demand by changing target temperature.
#[serde(default = "default_control_strategy")]
pub control_strategy: String,
+5 -1
View File
@@ -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",
] {
+113
View File
@@ -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;
}
}
+2 -1
View File
@@ -85,7 +85,8 @@
<div class="house-power-actions"><button id="housePowerOn" type="button" class="secondary"
data-action="house-power" data-value="true" data-i18n="house.powerOnAll">Turn all on</button><button
id="housePowerOff" type="button" class="secondary" data-action="house-power" data-value="false"
data-i18n="house.powerOffAll">Turn all off</button></div>
data-i18n="house.powerOffAll">Turn all off</button><button id="houseEmergencyStop" type="button"
class="secondary house-emergency-stop" data-action="house-emergency-stop" data-value="true" hidden>Emergency stop</button></div>
</div>
<div class="dashboard-global-divider"></div>
<div class="house-climate dashboard-house-climate" id="houseClimate"></div>
+1
View File
@@ -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 || []));
+1 -1
View File
@@ -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,
+81 -3
View File
@@ -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)}<small>°C</small>`;
@@ -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 = `<div class="house-climate-head"><div><span class="eyebrow">${esc(tr('house.seasonMode'))}</span><h3>${esc(tr('house.smartThermostat'))}</h3><p>${esc(tr('house.setpointStrategy'))}</p></div><button type="button" class="outside-pill" data-action="open-outdoor-history" title="${esc(tr('house.outdoorHistoryOpen'))}" aria-label="${esc(tr('house.outdoorHistoryOpen'))}"><small>${esc(tr('house.outdoor'))}</small><strong>${esc(outdoor)}</strong></button></div>
<div class="house-mode-row">
<button class="${mode === 'cool' ? 'active' : ''}" data-action="house-mode" data-value="cool" aria-pressed="${mode === 'cool'}">${esc(tr('mode.cool'))}</button>
@@ -58,6 +73,68 @@ function renderHouseClimate() {
<div class="preset-row house-preset-row">${['auto', 'comfort', 'sleep', 'away'].map(p => `<button class="${preset === p ? 'active' : ''}" data-action="house-preset" data-value="${p}" aria-pressed="${preset === p}">${esc(p === 'sleep' ? tr('house.sleepAll') : p === 'comfort' ? tr('house.comfortAll') : p === 'away' ? tr('house.awayAll') : tr('house.autoAll'))}</button>`).join('')}</div>`;
}
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 `<li><div><strong>${esc(zone.zone_name)}</strong><small>${esc(source)}</small></div><span>${esc(reason)}</span></li>`;
}).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 `<li><div><strong>${esc(rule.name)}</strong><small>${esc(trigger)}</small></div><span>${esc(tr('plan.reasonLastTriggered', { when }))}</span></li>`;
}).join('');
const emergency = plan.emergency_stop_active
? `<div class="plan-reason-alert"><strong>${esc(tr('house.emergencyActive'))}</strong><span>${esc(tr('house.emergencyActiveHint'))}</span></div>`
: '';
const rules = recentRules
? `<div class="plan-reason-subsection"><h4>${esc(tr('plan.recentTriggers'))}</h4><ul class="plan-reason-list">${recentRules}</ul></div>`
: '';
return `<article class="panel plan-card plan-reasons"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.reasonsEyebrow'))}</span><h3>${esc(tr('plan.reasonsTitle'))}</h3></div></div>${emergency}<div class="plan-reason-subsection"><h4>${esc(tr('plan.currentReasons'))}</h4><ul class="plan-reason-list">${zones || `<li class="plan-reason-empty">${esc(tr('plan.noReasons'))}</li>`}</ul></div>${rules}</article>`;
}
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 ? `<article class="panel plan-card plan-rules"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.rules'))}</span><h3>${esc(tr('plan.ruleCount', { count: rules.length }))}</h3></div></div><ul class="plan-events">${rules.slice(0, 3).map(rule => {
const ruleCard = rules.length ? `<article class="panel plan-card plan-rules ${plan.emergency_stop_active ? 'disabled' : ''}"><div class="plan-card-head"><div><span class="eyebrow">${esc(tr('plan.rules'))}</span><h3>${esc(tr(plan.emergency_stop_active ? 'plan.ruleCountPaused' : 'plan.ruleCount', { count: rules.length }))}</h3></div></div><ul class="plan-events">${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 `<li><time>${esc(automationTriggerLabel(rule))}</time><span class="plan-rule-summary"><strong>${esc(displayName)}</strong>${target ? `<small>→ ${esc(target)}</small>` : ''}</span></li>`;
}).join('')}</ul></article>` : '';
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;
}
+17
View File
@@ -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 () => {
+8 -1
View File
@@ -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') {