This commit is contained in:
Mateusz Gruszczyński
2026-09-15 18:47:20 +02:00
parent 7ffaba6c7a
commit 3d69e74319
37 changed files with 426 additions and 318 deletions
Generated
+1 -1
View File
@@ -700,7 +700,7 @@ dependencies = [
[[package]]
name = "gree-controller"
version = "0.14.2"
version = "0.14.4"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gree-controller"
version = "0.14.2"
version = "0.14.4"
edition = "2021"
authors = ["GREE Controller contributors"]
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
+37 -37
View File
@@ -1,19 +1,19 @@
80472072ffaa0e14ef6fd28b02f757d1947fc656687fef19fa96b58b5f13baa5 ./.env.example
b99b779e89161c581acf74b67a32aface8f410a9fdf1d6d6fd7424226c0c1075 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
59fe6d7de7f780481d0a7f614d4325f905a8aee8d971717566fc8889340223b3 ./Cargo.lock
97b67fd716185b18bb12d9170b502f81c6ffaf099816c893305c9f837048cb1f ./Cargo.toml
6428e098916d33b88865b31840f3a0ec7d1191ed86eb30a634c5c74a188f0fb9 ./Cargo.lock
708d68ca64a4623fb2e21be1e6e770b33facd2b901b3057e7f289b0ec0611e03 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
ee65b2f48b4cc5553e01689ace0f33151bbb2a2738f07ff7918cbf033b6a0f4d ./README.md
31d804b6af6addcdda8964a08a8a8c4b90fde7cc92eba7527d895d3c3deecca9 ./SOURCE_MANIFEST.sha256
8601961aee2029e5bc8281b7f950d7de25fb1d85c6b89827cab471625a745d6c ./README.md
2345439df2b16d0478cff70d305307580052b1d2495e699c99fd743f3d3aeb2e ./SOURCE_MANIFEST.sha256
bb549919b96b0413152dd905aec8640bb53301f07ec7d0b1be48ccbd37936fef ./build.rs
40199fea4917f9278344780440b0f47ca3ed5c6e7873e56040e16519edac7834 ./docs/API.md
ff213ad348e7f8b5cae7fe4adcad84d72db693765d424f270faa1acca9c23ce7 ./docs/FLOW.md
be8879ede6b807450dd79af22ad739f4089d0ce0b04f66a8bc00ada685789506 ./docs/openapi.json
1aa5bb37b5d0c315fcc085d40678a63964be1fd537bb3b8f35f5497fea1af6a3 ./docs/API.md
f21d732eb1147f8f3167d2172cdf0b57e61edabdfecbaf97811d46c5bff7e47a ./docs/FLOW.md
0022f50fa10077531c3d1aa6afea5b4ea5ecff5e9d015269327df701e4a23c04 ./docs/openapi.json
c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
a18d2460340264d34c267fa704ee51a42e59bcc1783ee4c0dbb2a8ef42fa0f16 ./ha-addon/Dockerfile
0417c88a990c72aecf071fba64d5d9da65dcda4d396cd7565477f7a4532c8c5e ./ha-addon/README.md
cd7fd4b937a1d6138699f7effd184e3c077912b881e9c73c37b90bbafba68477 ./ha-addon/build.sh
fd041032f353ceb0a2b02005f5a78d74a460b12e4e78de13dcb60129859f8c4e ./ha-addon/home-assistant/README.md
082dfeb6326f3e1b9841ccf7f2954117ecdf0aa1a90b0aef1e6a1960dacdecbf ./ha-addon/home-assistant/README.md
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./ha-addon/home-assistant/custom_components/gree_controller/__init__.py
3f6ef15ef58456376ac53fde7cace1ef359d6a6f7a64c5b575ad77ea6e55ccc4 ./ha-addon/home-assistant/custom_components/gree_controller/api.py
0c008329aad15f5af6328380c1afe1f49db186b37b3d1cafec4b6f4088386e8e ./ha-addon/home-assistant/custom_components/gree_controller/climate.py
@@ -22,7 +22,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
41a8958a6fe6d10e5679c95ac34832583a2a15ecfd67d09b4e094a83d06f5c47 ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
25c5ffb84460bf81f0c54abfc912e5bfd85535d3558df7a10e1de9541dd2afa3 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
b730225f7303eb2b4a19930a2cb80c724bea7afb3427770e72a987152b5402de ./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
6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
56d3c922fdabb05213715d239e2b6279431d75e81a9a861d8172bf35b9bb4e15 ./ha-addon/repository/gree-controller/CHANGELOG.md
9372c3d9cfcbffd4aea4d195a646570ab52be5019c756827645bbd56725eb402 ./ha-addon/repository/gree-controller/CHANGELOG.md
3c06bd11671452ea6472afd5543fb91a39aba73a1d2d95c7762c04f94ab64030 ./ha-addon/repository/gree-controller/DOCS.md
c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md
a3a74b2aaa20fd39a050f8f360eec543d70b3fd43271617a60a0b95fd0cdf2ca ./ha-addon/repository/gree-controller/config.yaml
9a902d607e10f8a0dc1fffb5b41da92a2d5326ccec2b2df13aa82cd58a9ae3b4 ./ha-addon/repository/gree-controller/config.yaml
3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
d13e4c2b993d209078342c0c86d852b300692089b377aa044f811308dd188a1e ./ha-addon/repository/gree-controller/translations/en.yaml
@@ -43,8 +43,8 @@ c123b190219556382f0e7f3058056fb233f6261f257a01eceb96a2c0bbfa12f3 ./ha-addon/run
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
2db3d2a8611d6bc328452a6a8bd3da6fc6d52cddf65846b778e8e01ca841c070 ./lang/en.json
da9bb73b06e84965b7f4f85c18c94b87f0d1dc4460459fdb6b2a45b271b884c0 ./lang/pl.json
8e64e916d21bca02f1a29dac76e6458c5b6b2ebd90eabf9037ae6463ee057d7e ./lang/en.json
ee29bebe2cda2cdf818c9a6829feecb9cb069974f9d3602bb1e3fbb6faba2744 ./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
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
4094c8b169b7c3babc15b7af3d4d6f7fa12239a279dc8c73c546ea671ad1aef8 ./scripts/api_dev_test.py
a23ca9caaf80da017b1186893b0c56fc89bdd87c05ebf401ee769354a1842b7e ./scripts/api_dev_test.py
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
@@ -102,7 +102,7 @@ b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/upda
3dfd1e053cbfa0d684dd88cf99b244a96a36e2ff23d2a0715b8327ab62258254 ./src/api/assets.rs
05c255919f7b3d81188fc5f1410be6a0037471dfc44e93bffb009f29a1e6a3fc ./src/api/auth.rs
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
cc439eeeeb3cb87202ab9f78a9e2e746ce79c15e18647c7f32442b4e9053f160 ./src/api/configuration.rs
1c913c3119deaa0f473b1e4b40e2f8ba8f508a656640539141cd22fa69fe70a8 ./src/api/configuration.rs
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
32ade46ec45d6faf7852242b9f5ba1be6eacbacbeb33d2deabfab59bf1610ce0 ./src/api/device_groups.rs
fcbb3da0538aab451f71810f829cc248d6bb67e586dc418d71a572980583823d ./src/api/devices.rs
@@ -110,18 +110,18 @@ fcbb3da0538aab451f71810f829cc248d6bb67e586dc418d71a572980583823d ./src/api/devi
d59b080b392b574ec94276c7b485b455d0d1abacc7de2ef29dcbfcf7d86dbdb0 ./src/api/flows.rs
2cfee7029f065e81628a8942e3dad00a03c3a5cf4f8492d07f12b75d05c07359 ./src/api/gree_cloud.rs
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
cecf1cc7ebf97b6b2ea4dcad0845b2dd434718c86b2e8300a1a52c2ceaf01611 ./src/api/history.rs
376ac63794378c427d798e853a060612e76a95c9aa0357c2f216ba9e4f769f2f ./src/api/history.rs
e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/house.rs
27e85400ccbdb0adcb8f5c1624e7187e9b649237cfb97349fcf1c0d78300540d ./src/api/integrations.rs
8bcaf384ab3a2c297f232800fbffa72edcaa09ea340de0d665f2dc63ccba139c ./src/api/integrations.rs
6bfbe5135d4f93bd8d19a707877b53ed2029fd8490b4b1954a7520888fb1d664 ./src/api/middleware.rs
45e34a447ff384d99a3c42c489ca9fa75849c74d4d246caa91c63d927de17396 ./src/api/openapi.rs
744ce18aa9e0227c6cdfb6980063d0d738d74c5865d45c20fd20026d4742bb3e ./src/api/public_settings.rs
4feca416de6ce8acc34082f432721016b109c7e6e70116e5e0b710ea403c7006 ./src/api/public_settings.rs
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs
1602f70f93b8fc5dbbc2d6b370d5a43faae90b13bb38beb043b285a8a121b4e1 ./src/api/settings.rs
513743ac7752d92f16213099d1fca6431759f2cd05a8a12f4ee1226cfd1637b3 ./src/api/settings.rs
1c1fe39aec55d10608ca3db83827855b07249cd1a81ae054eb1ee6464514092b ./src/api/system.rs
9118a5a3a381b010e22d646498ff56d2e05f51656789682cc4371b09c384aa98 ./src/api/websocket.rs
455c7238c9788d378e909a4fbc94d5390b67edd92e127f48b4b913ddcbe82fb0 ./src/api/zones.rs
788d80732d1266798b0d67ed016ecfcc55161ef90bf240a034d186a0ff186e5b ./src/config.rs
ddd1bfc8625b66f6e5f3d24b3122ac86c25f89b0af527e3dfce9102551a8f142 ./src/api/zones.rs
ca2b42d341a1ca0c7772a1256aed12c06667778d6588687f3edc9e88ed1db4fc ./src/config.rs
f8394e0daca284070ac9ae0844d6b04fed7c30386f2d1ec409ecf07880bb0e6a ./src/db.rs
fdc9d019097becc5c36a0560caa1619aa35d802c0d208902b3bfecbe1643e3e8 ./src/db/climate.rs
e5d2027d40a99e61c2c572499af617a92de9e554f8916f3ea6f6731d1d6b09b4 ./src/db/configuration.rs
@@ -152,11 +152,11 @@ c1e8de5392681ba1255e3e2d871a6ee860fb8ca987a0a8f973d9ded3977a68fb ./src/engine/s
b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs
682c8b3956c657b655746cbd19ee97ee2f4cf953bc9de67d6d70a1b14f3dd746 ./src/engine/temperature.rs
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
47330f0d8ff170ea91ba37d019c0d5577151c87f0474bcc2a287850415f6b953 ./src/engine/tests.rs
82fde10161ac760e728532ac214018018a1cf7519b86ef6539705396206f5bf8 ./src/engine/tests.rs
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs
28571e25e539fb1fe5e77e8bd003caf2da9e84fc34d156fd5f3e65e805798ca0 ./src/engine/zone_control.rs
81eab9a7ed4be387572f2377a14bf7f7d84f7c3f4ed8b33059a3f7e685514a7a ./src/engine/zone_control.rs
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
ea553a1d8b44c74b300d7c4a76dcd3b370524eb57766730a1376d1ab3079913b ./src/home_assistant.rs
10271a141ec441964c66d5732155abda8993d9fcba8dc0cc77e7c35d5a40e466 ./src/home_assistant.rs
e78ca9aff23362971eb6450534ea21bdc15150456c378ef9b226cb93a8dbc612 ./src/influxdb.rs
da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb/codec.rs
b96624fa34b218a6d5f028ba09579633f6f8a04c109f0e94801e8d89ceeeb4b5 ./src/influxdb/query.rs
@@ -169,11 +169,11 @@ fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/c
5e212a072bf74ef32a186b172404844b02a92363d216167f814056e1f2a9a7a8 ./src/models/device.rs
aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs
ba5d359e4d6c12d9f62c1b28bdfb40d312737930df5421814f5310f7183e96c1 ./src/models/history.rs
14e053b0dbea9ed7e88e8affaa595fa126ff833c18696fa695e923488ec40d70 ./src/models/integrations.rs
110b73c179ea1f85b7afa14f4cdcfd9b8232dd005a6417cf357b8efc73e702c0 ./src/models/integrations.rs
f1359b68f1d041cd441f80429e77893eeb6f6194613b58fd3e7a212fb511db3e ./src/models/runtime.rs
4caae5c95404d2ffefb7db9507dcf4ed40a90d54bc6aeb51098e77e544d85c42 ./src/models/settings_api.rs
f8bd9b4106145ebcf20d003471bb54a329e7373cf14a3a712ecabf87ac59d6d3 ./src/models/settings_api.rs
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
7d798228a8d4d4e60a112167c001535f4b8a5fa32ec7a33554c73125c42256c2 ./src/models/zone.rs
bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs
62e696df8b86835df7ad127f03c257a78580769dc82bc66daa5041f71e8440a5 ./src/notifications.rs
cca742bb2b500eb9269f69385fcf88053cd08aabf6a98b7669b8a7a2fd67df13 ./src/protocol/crypto.rs
cd0612021f8ed6c9e0823d63883c0790db09aa485583f3f3fc9f2579f2c28ea3 ./src/protocol/gree.rs
@@ -201,23 +201,23 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
db1d0583efbc48c3ab8226aeba83275a14cd5c78be011555548a7491ec28b4d5 ./web/index.html
4c243c29743b402966f48d904a17bf48d0ce9022a0b25100f0b7cd8b81926f5c ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
27eff2821c40395369be15e197fa830535d2327fcb27c93a0e8e65a1c62de3ad ./web/js/bootstrap.js
713bf6526c83b85780ed9c0f0807c5a1aa009f44ddbc9a67c47141f925db9402 ./web/js/charts.js
ead5b315171a4bf4d443f3bba00a573ffd016a7667475d0e2cde480047446c70 ./web/js/core.js
d880ffdac6aa07cd10d79fbacf12f87eb4c69b48a80564bbfdd0248abe8884d5 ./web/js/dashboard.js
def704d31b00706c26bae73190be15b289e72e6ecc4d8c9e9a5bb1804c747206 ./web/js/charts.js
f33a59e6863346d6751a74e1f912b6f0cd7a187defc3b01267a8121c9cd7ec8f ./web/js/core.js
021736e8b8d02248a41788765af2dd46914a700e8cadb0c43a75e83ed3f2dd14 ./web/js/dashboard.js
ed31601df41cc82032709f960a2b4061966abd58cde456ef7dda05e42eb91432 ./web/js/entities.js
08e8e130dd33be91d2155e40aa0bf72a807355b234223b10eb01178ddbbefd13 ./web/js/events.js
8c156909d2d2e59b7d7dc969b9520c978141a448a6cc5aae8fe74a8dc124f2bc ./web/js/flows.js
9e2209d41df7624c29741f03f12ed8e83b47cee2891c5a80cea8ca1bdacf1d5a ./web/js/forms.js
ef778fb570958704c2e079fa5cd645f67f884881e16984daa6f3ed52627b26a5 ./web/js/events.js
89d5f4098e74162ecdee2eef9441f88484a09e9197d21e30c40c220cedbf7e53 ./web/js/flows.js
52b4807f05a1325565d5e866007f49ea83c568574155cded131ce42a49df44f5 ./web/js/forms.js
ae6195905f33a4a83d5ba7c22d698419e2de05d80ed37df6066e0380fa715b6e ./web/js/history.js
0dc879c0056251618633c9efd188fc1593eeae6965ad1bc993132dea6e171b44 ./web/js/main.js
53ebdaa64a1dc8327db5cd4e08fb3f551dc2df33c214f218c6bb1ae3f27ad4dd ./web/js/navigation.js
e3d146a683b8beb332c752e533826160c2cf2236bc269ced7a4dd1249871027b ./web/js/realtime.js
9546ca5c5566994aff3c33d92295be46b65d51d874681764abe77470dc9c9c73 ./web/js/router.js
e62ca4a0cad10ca8b60c1fe4b5ecd0ccf9bd890cd58206a937dd1655fe216353 ./web/js/settings-ui.js
0dde5e1a19ab2384156229fe2a5d708e06bf58a2227f834d215af97d4375a5d3 ./web/js/settings.js
cdc55061a41b7325aad552357d8359b42d881589c39b438cdc2a48bf0f690db9 ./web/js/settings-ui.js
f00c3f603eb314f755a0c46424681b579e0b890453e2550007c719273815147c ./web/js/settings.js
3815fea7ae1694568c2f249bc8460e0563ced6bde89214f74f21b41fe8cad9a6 ./web/lang-init.js
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
4b3c94a1988e8e18bd0c39e5ebf73ce6ef31ddea5bb352ec40b1ae7b3609f5e9 ./web/styles.css
+8 -7
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.14.2**
**Version: 0.14.4**
Version 0.14.2 adds separate vertical and horizontal swing control plus connectivity history. Local/LAN units can be sampled periodically for response time, jitter and packet loss, while optional GREE Cloud diagnostics measure REST and MQTT responsiveness. It keeps the split/multisplit energy and outdoor-temperature improvements from 0.14.1, while preserving independent Local and Cloud device entries.
Version 0.14.4 removes the obsolete Home Assistant test/default entity. The HA connection test validates only the server/API token. `HA_OUTDOOR_ENTITY_ID` is the global outdoor-temperature sensor, and each zone can optionally override it with `ha_outdoor_entity_id`; room-temperature HA sensors remain configured per zone. Outdoor overrides participate in aliases, metrics/history and Home Assistant entity suggestions in Visual Flow.
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
@@ -286,19 +286,20 @@ Configure **Home Assistant / Sensors** in the Web UI or use:
```text
HA_URL=
HA_TOKEN=
HA_ENTITY_ID=
HA_OUTDOOR_ENTITY_ID=
HA_SENSOR_STALE_AFTER_SECONDS=300
HA_ALLOW_INVALID_TLS=false
```
Per zone, `sensor_source` can be:
The Home Assistant connection test checks only `HA_URL` and `HA_TOKEN`; no entity is required. `HA_OUTDOOR_ENTITY_ID` is the optional global outdoor-temperature sensor. Per zone, `sensor_source` can be:
- `device` — GREE indoor sensor,
- `home_assistant`configured HA room sensor,
- `combined` — weighted GREE + HA value.
- `home_assistant`Home Assistant room sensor,
- `combined` — weighted GREE + Home Assistant value.
Stale/unavailable external data falls back to the GREE sensor when possible. The optional outdoor sensor is only an assist signal; it never replaces room temperature.
For `home_assistant` and `combined`, each zone must set its own `ha_entity_id` room-temperature sensor. Room sensors are independent from the outdoor-temperature source and stale/unavailable room data falls back to the GREE sensor when possible.
`HA_OUTDOOR_ENTITY_ID` is the global outdoor-temperature source used by outdoor-temperature assist. A zone may leave `ha_outdoor_entity_id` empty to use that global sensor, or set `ha_outdoor_entity_id` to use a different outdoor sensor only for that zone. If a zone override is unavailable, the controller falls back to the global outdoor source. Global and per-zone outdoor sensors are exposed through the normal alias/history/metric paths and appear in Home Assistant entity suggestions used by Visual Flows.
### 2. GREE Controller entities inside Home Assistant
+36 -36
View File
@@ -1,18 +1,18 @@
80472072ffaa0e14ef6fd28b02f757d1947fc656687fef19fa96b58b5f13baa5 ./.env.example
b99b779e89161c581acf74b67a32aface8f410a9fdf1d6d6fd7424226c0c1075 ./.env.example
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
59fe6d7de7f780481d0a7f614d4325f905a8aee8d971717566fc8889340223b3 ./Cargo.lock
97b67fd716185b18bb12d9170b502f81c6ffaf099816c893305c9f837048cb1f ./Cargo.toml
6428e098916d33b88865b31840f3a0ec7d1191ed86eb30a634c5c74a188f0fb9 ./Cargo.lock
708d68ca64a4623fb2e21be1e6e770b33facd2b901b3057e7f289b0ec0611e03 ./Cargo.toml
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
ee65b2f48b4cc5553e01689ace0f33151bbb2a2738f07ff7918cbf033b6a0f4d ./README.md
8601961aee2029e5bc8281b7f950d7de25fb1d85c6b89827cab471625a745d6c ./README.md
bb549919b96b0413152dd905aec8640bb53301f07ec7d0b1be48ccbd37936fef ./build.rs
40199fea4917f9278344780440b0f47ca3ed5c6e7873e56040e16519edac7834 ./docs/API.md
ff213ad348e7f8b5cae7fe4adcad84d72db693765d424f270faa1acca9c23ce7 ./docs/FLOW.md
be8879ede6b807450dd79af22ad739f4089d0ce0b04f66a8bc00ada685789506 ./docs/openapi.json
1aa5bb37b5d0c315fcc085d40678a63964be1fd537bb3b8f35f5497fea1af6a3 ./docs/API.md
f21d732eb1147f8f3167d2172cdf0b57e61edabdfecbaf97811d46c5bff7e47a ./docs/FLOW.md
0022f50fa10077531c3d1aa6afea5b4ea5ecff5e9d015269327df701e4a23c04 ./docs/openapi.json
c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
a18d2460340264d34c267fa704ee51a42e59bcc1783ee4c0dbb2a8ef42fa0f16 ./ha-addon/Dockerfile
0417c88a990c72aecf071fba64d5d9da65dcda4d396cd7565477f7a4532c8c5e ./ha-addon/README.md
cd7fd4b937a1d6138699f7effd184e3c077912b881e9c73c37b90bbafba68477 ./ha-addon/build.sh
fd041032f353ceb0a2b02005f5a78d74a460b12e4e78de13dcb60129859f8c4e ./ha-addon/home-assistant/README.md
082dfeb6326f3e1b9841ccf7f2954117ecdf0aa1a90b0aef1e6a1960dacdecbf ./ha-addon/home-assistant/README.md
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./ha-addon/home-assistant/custom_components/gree_controller/__init__.py
3f6ef15ef58456376ac53fde7cace1ef359d6a6f7a64c5b575ad77ea6e55ccc4 ./ha-addon/home-assistant/custom_components/gree_controller/api.py
0c008329aad15f5af6328380c1afe1f49db186b37b3d1cafec4b6f4088386e8e ./ha-addon/home-assistant/custom_components/gree_controller/climate.py
@@ -21,7 +21,7 @@ b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/hom
41a8958a6fe6d10e5679c95ac34832583a2a15ecfd67d09b4e094a83d06f5c47 ./ha-addon/home-assistant/custom_components/gree_controller/coordinator.py
5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./ha-addon/home-assistant/custom_components/gree_controller/entity_map.py
c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./ha-addon/home-assistant/custom_components/gree_controller/icon.png
25c5ffb84460bf81f0c54abfc912e5bfd85535d3558df7a10e1de9541dd2afa3 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
b730225f7303eb2b4a19930a2cb80c724bea7afb3427770e72a987152b5402de ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
c52a484b671ce738ecc00228a1db19a5d703f69ab27530c2896a5c2ae3b4a96f ./ha-addon/home-assistant/custom_components/gree_controller/number.py
39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./ha-addon/home-assistant/custom_components/gree_controller/select.py
cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/home-assistant/custom_components/gree_controller/sensor.py
@@ -29,10 +29,10 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./ha-addon/hom
6bddb7b4620021ecd2099a86a77ef5c7f2c2dcd3d07d5db4e7b4c4ce6d3e8c03 ./ha-addon/home-assistant/custom_components/gree_controller/translations/en.json
13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./ha-addon/home-assistant/custom_components/gree_controller/translations/pl.json
c5fc5c87273d82d2834a0a5a365c821a416413ed509613441e24e4dc3507c4d2 ./ha-addon/home-assistant/generated/gree_controller_entities.example.json
56d3c922fdabb05213715d239e2b6279431d75e81a9a861d8172bf35b9bb4e15 ./ha-addon/repository/gree-controller/CHANGELOG.md
9372c3d9cfcbffd4aea4d195a646570ab52be5019c756827645bbd56725eb402 ./ha-addon/repository/gree-controller/CHANGELOG.md
3c06bd11671452ea6472afd5543fb91a39aba73a1d2d95c7762c04f94ab64030 ./ha-addon/repository/gree-controller/DOCS.md
c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md
a3a74b2aaa20fd39a050f8f360eec543d70b3fd43271617a60a0b95fd0cdf2ca ./ha-addon/repository/gree-controller/config.yaml
9a902d607e10f8a0dc1fffb5b41da92a2d5326ccec2b2df13aa82cd58a9ae3b4 ./ha-addon/repository/gree-controller/config.yaml
3aae6cdae4c3aaab7786a9575e7093b7e2c5b69288167075f4b7475611691a6b ./ha-addon/repository/gree-controller/topologia-ha-vlan-gree-pl.png
0d0b42a7639b128946eec379175436fa1f150294f39d2dfd169077bfa87fd14d ./ha-addon/repository/gree-controller/topology-ha-vlan-gree-en.png
d13e4c2b993d209078342c0c86d852b300692089b377aa044f811308dd188a1e ./ha-addon/repository/gree-controller/translations/en.yaml
@@ -42,8 +42,8 @@ c123b190219556382f0e7f3058056fb233f6261f257a01eceb96a2c0bbfa12f3 ./ha-addon/run
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
2db3d2a8611d6bc328452a6a8bd3da6fc6d52cddf65846b778e8e01ca841c070 ./lang/en.json
da9bb73b06e84965b7f4f85c18c94b87f0d1dc4460459fdb6b2a45b271b884c0 ./lang/pl.json
8e64e916d21bca02f1a29dac76e6458c5b6b2ebd90eabf9037ae6463ee057d7e ./lang/en.json
ee29bebe2cda2cdf818c9a6829feecb9cb069974f9d3602bb1e3fbb6faba2744 ./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
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh
c64b1c6deeb24f20af662bfce7fa3683703914987241107ce664fa2f6d1a9dcb ./scripts/README.md
4094c8b169b7c3babc15b7af3d4d6f7fa12239a279dc8c73c546ea671ad1aef8 ./scripts/api_dev_test.py
a23ca9caaf80da017b1186893b0c56fc89bdd87c05ebf401ee769354a1842b7e ./scripts/api_dev_test.py
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
@@ -101,7 +101,7 @@ b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/upda
3dfd1e053cbfa0d684dd88cf99b244a96a36e2ff23d2a0715b8327ab62258254 ./src/api/assets.rs
05c255919f7b3d81188fc5f1410be6a0037471dfc44e93bffb009f29a1e6a3fc ./src/api/auth.rs
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
cc439eeeeb3cb87202ab9f78a9e2e746ce79c15e18647c7f32442b4e9053f160 ./src/api/configuration.rs
1c913c3119deaa0f473b1e4b40e2f8ba8f508a656640539141cd22fa69fe70a8 ./src/api/configuration.rs
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
32ade46ec45d6faf7852242b9f5ba1be6eacbacbeb33d2deabfab59bf1610ce0 ./src/api/device_groups.rs
fcbb3da0538aab451f71810f829cc248d6bb67e586dc418d71a572980583823d ./src/api/devices.rs
@@ -109,18 +109,18 @@ fcbb3da0538aab451f71810f829cc248d6bb67e586dc418d71a572980583823d ./src/api/devi
d59b080b392b574ec94276c7b485b455d0d1abacc7de2ef29dcbfcf7d86dbdb0 ./src/api/flows.rs
2cfee7029f065e81628a8942e3dad00a03c3a5cf4f8492d07f12b75d05c07359 ./src/api/gree_cloud.rs
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
cecf1cc7ebf97b6b2ea4dcad0845b2dd434718c86b2e8300a1a52c2ceaf01611 ./src/api/history.rs
376ac63794378c427d798e853a060612e76a95c9aa0357c2f216ba9e4f769f2f ./src/api/history.rs
e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/house.rs
27e85400ccbdb0adcb8f5c1624e7187e9b649237cfb97349fcf1c0d78300540d ./src/api/integrations.rs
8bcaf384ab3a2c297f232800fbffa72edcaa09ea340de0d665f2dc63ccba139c ./src/api/integrations.rs
6bfbe5135d4f93bd8d19a707877b53ed2029fd8490b4b1954a7520888fb1d664 ./src/api/middleware.rs
45e34a447ff384d99a3c42c489ca9fa75849c74d4d246caa91c63d927de17396 ./src/api/openapi.rs
744ce18aa9e0227c6cdfb6980063d0d738d74c5865d45c20fd20026d4742bb3e ./src/api/public_settings.rs
4feca416de6ce8acc34082f432721016b109c7e6e70116e5e0b710ea403c7006 ./src/api/public_settings.rs
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs
1602f70f93b8fc5dbbc2d6b370d5a43faae90b13bb38beb043b285a8a121b4e1 ./src/api/settings.rs
513743ac7752d92f16213099d1fca6431759f2cd05a8a12f4ee1226cfd1637b3 ./src/api/settings.rs
1c1fe39aec55d10608ca3db83827855b07249cd1a81ae054eb1ee6464514092b ./src/api/system.rs
9118a5a3a381b010e22d646498ff56d2e05f51656789682cc4371b09c384aa98 ./src/api/websocket.rs
455c7238c9788d378e909a4fbc94d5390b67edd92e127f48b4b913ddcbe82fb0 ./src/api/zones.rs
788d80732d1266798b0d67ed016ecfcc55161ef90bf240a034d186a0ff186e5b ./src/config.rs
ddd1bfc8625b66f6e5f3d24b3122ac86c25f89b0af527e3dfce9102551a8f142 ./src/api/zones.rs
ca2b42d341a1ca0c7772a1256aed12c06667778d6588687f3edc9e88ed1db4fc ./src/config.rs
f8394e0daca284070ac9ae0844d6b04fed7c30386f2d1ec409ecf07880bb0e6a ./src/db.rs
fdc9d019097becc5c36a0560caa1619aa35d802c0d208902b3bfecbe1643e3e8 ./src/db/climate.rs
e5d2027d40a99e61c2c572499af617a92de9e554f8916f3ea6f6731d1d6b09b4 ./src/db/configuration.rs
@@ -151,11 +151,11 @@ c1e8de5392681ba1255e3e2d871a6ee860fb8ca987a0a8f973d9ded3977a68fb ./src/engine/s
b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs
682c8b3956c657b655746cbd19ee97ee2f4cf953bc9de67d6d70a1b14f3dd746 ./src/engine/temperature.rs
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
47330f0d8ff170ea91ba37d019c0d5577151c87f0474bcc2a287850415f6b953 ./src/engine/tests.rs
82fde10161ac760e728532ac214018018a1cf7519b86ef6539705396206f5bf8 ./src/engine/tests.rs
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs
28571e25e539fb1fe5e77e8bd003caf2da9e84fc34d156fd5f3e65e805798ca0 ./src/engine/zone_control.rs
81eab9a7ed4be387572f2377a14bf7f7d84f7c3f4ed8b33059a3f7e685514a7a ./src/engine/zone_control.rs
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
ea553a1d8b44c74b300d7c4a76dcd3b370524eb57766730a1376d1ab3079913b ./src/home_assistant.rs
10271a141ec441964c66d5732155abda8993d9fcba8dc0cc77e7c35d5a40e466 ./src/home_assistant.rs
e78ca9aff23362971eb6450534ea21bdc15150456c378ef9b226cb93a8dbc612 ./src/influxdb.rs
da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb/codec.rs
b96624fa34b218a6d5f028ba09579633f6f8a04c109f0e94801e8d89ceeeb4b5 ./src/influxdb/query.rs
@@ -168,11 +168,11 @@ fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/c
5e212a072bf74ef32a186b172404844b02a92363d216167f814056e1f2a9a7a8 ./src/models/device.rs
aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs
ba5d359e4d6c12d9f62c1b28bdfb40d312737930df5421814f5310f7183e96c1 ./src/models/history.rs
14e053b0dbea9ed7e88e8affaa595fa126ff833c18696fa695e923488ec40d70 ./src/models/integrations.rs
110b73c179ea1f85b7afa14f4cdcfd9b8232dd005a6417cf357b8efc73e702c0 ./src/models/integrations.rs
f1359b68f1d041cd441f80429e77893eeb6f6194613b58fd3e7a212fb511db3e ./src/models/runtime.rs
4caae5c95404d2ffefb7db9507dcf4ed40a90d54bc6aeb51098e77e544d85c42 ./src/models/settings_api.rs
f8bd9b4106145ebcf20d003471bb54a329e7373cf14a3a712ecabf87ac59d6d3 ./src/models/settings_api.rs
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
7d798228a8d4d4e60a112167c001535f4b8a5fa32ec7a33554c73125c42256c2 ./src/models/zone.rs
bc2818ce8d0dae1edfb1b98e5d1c00b193f03eeaa28e343d3532f6023288738c ./src/models/zone.rs
62e696df8b86835df7ad127f03c257a78580769dc82bc66daa5041f71e8440a5 ./src/notifications.rs
cca742bb2b500eb9269f69385fcf88053cd08aabf6a98b7669b8a7a2fd67df13 ./src/protocol/crypto.rs
cd0612021f8ed6c9e0823d63883c0790db09aa485583f3f3fc9f2579f2c28ea3 ./src/protocol/gree.rs
@@ -200,23 +200,23 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
db1d0583efbc48c3ab8226aeba83275a14cd5c78be011555548a7491ec28b4d5 ./web/index.html
4c243c29743b402966f48d904a17bf48d0ce9022a0b25100f0b7cd8b81926f5c ./web/index.html
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
27eff2821c40395369be15e197fa830535d2327fcb27c93a0e8e65a1c62de3ad ./web/js/bootstrap.js
713bf6526c83b85780ed9c0f0807c5a1aa009f44ddbc9a67c47141f925db9402 ./web/js/charts.js
ead5b315171a4bf4d443f3bba00a573ffd016a7667475d0e2cde480047446c70 ./web/js/core.js
d880ffdac6aa07cd10d79fbacf12f87eb4c69b48a80564bbfdd0248abe8884d5 ./web/js/dashboard.js
def704d31b00706c26bae73190be15b289e72e6ecc4d8c9e9a5bb1804c747206 ./web/js/charts.js
f33a59e6863346d6751a74e1f912b6f0cd7a187defc3b01267a8121c9cd7ec8f ./web/js/core.js
021736e8b8d02248a41788765af2dd46914a700e8cadb0c43a75e83ed3f2dd14 ./web/js/dashboard.js
ed31601df41cc82032709f960a2b4061966abd58cde456ef7dda05e42eb91432 ./web/js/entities.js
08e8e130dd33be91d2155e40aa0bf72a807355b234223b10eb01178ddbbefd13 ./web/js/events.js
8c156909d2d2e59b7d7dc969b9520c978141a448a6cc5aae8fe74a8dc124f2bc ./web/js/flows.js
9e2209d41df7624c29741f03f12ed8e83b47cee2891c5a80cea8ca1bdacf1d5a ./web/js/forms.js
ef778fb570958704c2e079fa5cd645f67f884881e16984daa6f3ed52627b26a5 ./web/js/events.js
89d5f4098e74162ecdee2eef9441f88484a09e9197d21e30c40c220cedbf7e53 ./web/js/flows.js
52b4807f05a1325565d5e866007f49ea83c568574155cded131ce42a49df44f5 ./web/js/forms.js
ae6195905f33a4a83d5ba7c22d698419e2de05d80ed37df6066e0380fa715b6e ./web/js/history.js
0dc879c0056251618633c9efd188fc1593eeae6965ad1bc993132dea6e171b44 ./web/js/main.js
53ebdaa64a1dc8327db5cd4e08fb3f551dc2df33c214f218c6bb1ae3f27ad4dd ./web/js/navigation.js
e3d146a683b8beb332c752e533826160c2cf2236bc269ced7a4dd1249871027b ./web/js/realtime.js
9546ca5c5566994aff3c33d92295be46b65d51d874681764abe77470dc9c9c73 ./web/js/router.js
e62ca4a0cad10ca8b60c1fe4b5ecd0ccf9bd890cd58206a937dd1655fe216353 ./web/js/settings-ui.js
0dde5e1a19ab2384156229fe2a5d708e06bf58a2227f834d215af97d4375a5d3 ./web/js/settings.js
cdc55061a41b7325aad552357d8359b42d881589c39b438cdc2a48bf0f690db9 ./web/js/settings-ui.js
f00c3f603eb314f755a0c46424681b579e0b890453e2550007c719273815147c ./web/js/settings.js
3815fea7ae1694568c2f249bc8460e0563ced6bde89214f74f21b41fe8cad9a6 ./web/lang-init.js
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
4b3c94a1988e8e18bd0c39e5ebf73ce6ef31ddea5bb352ec40b1ae7b3609f5e9 ./web/styles.css
+11 -17
View File
@@ -1,6 +1,6 @@
# GREE Controller API reference
HTTP and WebSocket API for GREE Controller **0.14.2**.
HTTP and WebSocket API for GREE Controller **0.14.4**.
[← Main documentation](../README.md)
@@ -196,7 +196,7 @@ Common statuses:
| GET/PUT | `/api/settings/debug` | Debug overlay/GREE frame settings. |
| GET | `/api/configuration/export` | Export full application configuration. |
| POST | `/api/configuration/import` | Import/replace application configuration. |
| POST | `/api/integrations/home-assistant/test` | Test HA temperature read. |
| POST | `/api/integrations/home-assistant/test` | Test HA server/API authentication without requiring an entity. |
| POST | `/api/integrations/home-assistant/entity` | Read raw HA entity state/attributes for shared Flow input diagnostics. |
| POST | `/api/integrations/notifications/test` | Send a test notification. |
@@ -232,7 +232,7 @@ Response:
{
"status": "ok",
"name": "gree-controller",
"version": "0.14.2",
"version": "0.14.4",
"uptime_seconds": 1234,
"control_ready": true,
"time": "2026-08-30T06:54:00Z"
@@ -283,7 +283,7 @@ Returns the initial Web UI snapshot:
"max_fan_speed": 1, "force_quiet": true, "use_native_sleep": true
},
"home_assistant": {
"url": "", "token_configured": false, "default_entity_id": "",
"url": "", "token_configured": false,
"outdoor_entity_id": "", "sensor_stale_after_seconds": 300,
"allow_invalid_tls": false, "sensor_aliases": {}, "flow_inputs": [],
"outdoor_assist_enabled": true
@@ -295,7 +295,7 @@ Returns the initial Web UI snapshot:
"control_plan": {"generated_at": "2026-09-04T08:00:00Z", "zones": [], "rules": []},
"control_plan_revision": 42,
"system": {
"version": "0.14.2",
"version": "0.14.4",
"uptime_seconds": 1234,
"auth_required": false,
"control_ready": true,
@@ -523,6 +523,7 @@ When the physical device belongs to a disabled thermostat zone, direct web/API c
"smart_fan": true,
"sensor_source": "combined",
"ha_entity_id": "sensor.living_room_temperature",
"ha_outdoor_entity_id": "sensor.garden_temperature",
"external_sensor_weight": 0.4,
"max_sensor_difference": 3.0,
"sensor_stale_after_seconds": 300,
@@ -540,6 +541,8 @@ Important rules:
- `separate_hysteresis`: when `true`, cooling uses `cool_hysteresis` and heating uses `heat_hysteresis`; both use the same `0.1..5.0` °C range.
- `standby_offset_c`: bounded thermostat offset.
- `sensor_source`: `device`, `home_assistant` or `combined`.
- for `home_assistant`/`combined`, `ha_entity_id` is the required Home Assistant room-temperature sensor for that zone.
- `ha_outdoor_entity_id` is optional; when empty, the zone uses global Home Assistant `outdoor_entity_id`, and when set it overrides the outdoor-temperature source only for that zone. If the override is unavailable, the global outdoor source is used.
- `external_sensor_weight`: `0..1`.
- `revision` is used for optimistic concurrency where supplied; stale updates can return `409`.
@@ -1182,7 +1185,6 @@ Times must use `HH:MM`; maximum fan speed is clamped to `1..5`.
{
"url": "http://homeassistant.local:8123",
"token_configured": true,
"default_entity_id": "sensor.room_temperature",
"outdoor_entity_id": "sensor.outdoor_temperature",
"sensor_stale_after_seconds": 300,
"allow_invalid_tls": false,
@@ -1194,7 +1196,7 @@ Times must use `HH:MM`; maximum fan speed is clamped to `1..5`.
}
```
`PUT` replaces `token_configured` with optional `token`. Omitted/`null` token preserves the saved token; an explicit empty string clears it. Sensor age is clamped to `30..86400` seconds. URLs, aliases, entity IDs and shared Flow inputs are normalized/validated before persistence. Shared inputs are value sources only; comparison operators and thresholds belong to Flow nodes.
`PUT` replaces `token_configured` with optional `token`. Omitted/`null` token preserves the saved token; an explicit empty string clears it. `outdoor_entity_id` is the optional global Home Assistant outdoor-temperature sensor. Each zone can set `ha_outdoor_entity_id` to override that source only for the zone; a blank override uses the global sensor. Home Assistant room-temperature sensors remain configured per zone with `ha_entity_id` when `sensor_source` is `home_assistant` or `combined`. Sensor age is clamped to `30..86400` seconds. URLs, aliases, entity IDs and shared Flow inputs are normalized/validated before persistence. Global/per-zone outdoor sensors and per-zone room sensors are exposed through the normal alias and sensor-history/metric surfaces, and the Web UI offers them as suggestions in Flow Home Assistant entity fields. Shared inputs are value sources only; comparison operators and thresholds belong to Flow nodes. The Home Assistant connection test validates only the server/API token and requires no entity.
### `/api/settings/debug`
@@ -1266,19 +1268,11 @@ Response:
### `POST /api/integrations/home-assistant/test`
```json
{
"entity_id": "sensor.room_temperature"
}
```
`entity_id` is optional; controller defaults/aliases are resolved. Response:
No request body is required. The endpoint validates the configured Home Assistant URL and access token against the Home Assistant API root; it does not read or require any entity. Response:
```json
{
"ok": true,
"temperature_c": 23.4,
"entity_id": "sensor.room_temperature"
"ok": true
}
```
+1 -1
View File
@@ -67,7 +67,7 @@ Actions include thermostat zone, GREE device, climate group, and generic Home As
The editor ships 37 editable presets grouped into seven categories: Comfort, Energy & cost, Protection & safety, Night, Reliability, Home Assistant / heat sources and Advanced. The library UI also provides search, Favorites, Recent, a mini graph preview and requirement checks before applying a preset. The library includes workday/weekend comfort, weather-aware demand, morning boost, presence and energy-price eco modes, peak-power limiting, open-window/humidity/overheat/frost guards, night profiles, sensor/device resilience, dual heating/cooling thresholds, multi-room group control, nested AND/OR/NOT Home Assistant scenarios, and gas-boiler/external-heat-source coordination including off, reduced-target, boost and fallback heating patterns.
Shared Flow inputs show the Flows that reference them and link directly to those editors. Home Assistant-backed shared inputs can be tested live in the input modal. Shared inputs report the current raw value/attribute. Each Flow reference defines its own comparison without changing the shared source.
Shared Flow inputs show the Flows that reference them and link directly to those editors. Home Assistant-backed shared inputs can be tested live in the input modal. Shared inputs report the current raw value/attribute. Each Flow reference defines its own comparison without changing the shared source. Home Assistant temperature entities remain normal HA entities: room-temperature sensors are configured per zone, while outdoor temperature has one global `outdoor_entity_id` plus an optional per-zone `ha_outdoor_entity_id` override. Their aliases are shared with the rest of the UI, outdoor overrides are recorded in sensor metrics/history, and all configured entities are offered as suggestions in Home Assistant Flow blocks; selecting a zone sensor does not create a separate shared input.
Presets are data files, not JavaScript definitions. Every preset lives in `presets/<id>.json` and contains `id`, `category`, localized `name`, localized `description`, and the complete `flow` graph (`nodes` + `edges`). `build.rs` validates and embeds the directory, exposes `/presets/index.json` plus `/presets/<file>.json`, and the Flow editor loads that library dynamically. Device/zone/group placeholders such as `$zone1`, `$zone2`, `$device1` and `$group1` are resolved when a preset is applied. Adding a preset therefore does not require adding graph-building code to `web/js/flows.js`.
+36 -54
View File
@@ -2,9 +2,9 @@
"openapi": "3.1.0",
"info": {
"title": "GREE Controller API",
"version": "0.14.2",
"version": "0.14.4",
"summary": "Local HTTP/WebSocket API for GREE HVAC control",
"description": "Self-hosted API used by the GREE Controller Web UI and Home Assistant integration.\n\n### Authentication\nAdministrator endpoints accept `Authorization: Bearer <APP_TOKEN>` or `x-api-token: <APP_TOKEN>` when configured. Restricted Home Assistant control endpoints also accept generated integration tokens.\n\n### Local and GREE Cloud\nDevices use one API model and one command surface. `connection_type=local` is dispatched only to the existing LAN/UDP provider; `connection_type=gree_cloud` is dispatched only to the GREE Cloud provider. No automatic transport fallback is performed.\n\n### GREE Cloud security\nCloud passwords, REST/MQTT tokens, Authorization values and device cipher keys are never returned in normal API responses or Cloud diagnostics. MQTT uses TLS with certificate and hostname validation.\n\n### Energy\nGREE Cloud `ElcAll` and selected Home Assistant cumulative energy sensors are normalized to kWh and converted to consumption deltas before aggregation. Raw cumulative values are retained only as meter samples/baselines.\n\n### 0.14.2\nAdds independent vertical/horizontal swing controls across manual device control, thermostat zones, legacy automations, Visual Flow and Home Assistant zone climates. Swing is treated as an auxiliary one-shot unit setting and does not replace thermostat ownership.\n\n### 0.14.1\nAdds split/multisplit installation groups with shared energy and outdoor-temperature sources, multi-target energy charts with period comparison, cleaner Cloud status/alerts and safe MQTT cleanup after the last Cloud device is removed.\n\n### 0.14.0\nAdds first-class GREE Cloud MQTT status/control, reconnect/resubscribe lifecycle, Cloud diagnostics, per-device capabilities, Cloud/HA energy sources and energy-history aggregation while preserving the LAN transport contract.",
"description": "Self-hosted API used by the GREE Controller Web UI and Home Assistant integration.\n\n### Authentication\nAdministrator endpoints accept `Authorization: Bearer <APP_TOKEN>` or `x-api-token: <APP_TOKEN>` when configured. Restricted Home Assistant control endpoints also accept generated integration tokens.\n\n### Local and GREE Cloud\nDevices use one API model and one command surface. `connection_type=local` is dispatched only to the existing LAN/UDP provider; `connection_type=gree_cloud` is dispatched only to the GREE Cloud provider. No automatic transport fallback is performed.\n\n### GREE Cloud security\nCloud passwords, REST/MQTT tokens, Authorization values and device cipher keys are never returned in normal API responses or Cloud diagnostics. MQTT uses TLS with certificate and hostname validation.\n\n### Energy\nGREE Cloud `ElcAll` and selected Home Assistant cumulative energy sensors are normalized to kWh and converted to consumption deltas before aggregation. Raw cumulative values are retained only as meter samples/baselines.\n\n### 0.14.4\nHome Assistant connection tests validate only the configured server/API token and require no entity. `outdoor_entity_id` is the global Home Assistant outdoor-temperature sensor; zones can override it with `ha_outdoor_entity_id`. Room-temperature Home Assistant sensors remain configured per zone with `ha_entity_id`. Global/per-zone outdoor sensors are integrated with aliases, metrics/history and Visual Flow entity suggestions.\n\n### 0.14.2\nAdds independent vertical/horizontal swing controls across manual device control, thermostat zones, legacy automations, Visual Flow and Home Assistant zone climates. Swing is treated as an auxiliary one-shot unit setting and does not replace thermostat ownership.\n\n### 0.14.1\nAdds split/multisplit installation groups with shared energy and outdoor-temperature sources, multi-target energy charts with period comparison, cleaner Cloud status/alerts and safe MQTT cleanup after the last Cloud device is removed.\n\n### 0.14.0\nAdds first-class GREE Cloud MQTT status/control, reconnect/resubscribe lifecycle, Cloud diagnostics, per-device capabilities, Cloud/HA energy sources and energy-history aggregation while preserving the LAN transport contract.",
"license": {
"name": "MIT"
}
@@ -3076,19 +3076,9 @@
"tags": [
"Diagnostics"
],
"summary": "Test Home Assistant temperature",
"description": "Tests the configured Home Assistant connection and resolves a temperature from the supplied entity or controller default/alias.",
"summary": "Test Home Assistant connection",
"description": "Validates the configured Home Assistant URL and access token against the Home Assistant API root. No entity is required or read.",
"operationId": "testHomeAssistant",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HaTestRequest"
}
}
}
},
"security": [
{
"BearerToken": []
@@ -3099,7 +3089,7 @@
],
"responses": {
"200": {
"description": "Test result",
"description": "Connection test result",
"content": {
"application/json": {
"schema": {
@@ -3107,27 +3097,15 @@
"properties": {
"ok": {
"type": "boolean"
},
"temperature_c": {
"type": [
"number",
"null"
]
},
"entity_id": {
"type": "string"
}
},
"required": [
"ok",
"entity_id"
"ok"
],
"additionalProperties": false
},
"example": {
"ok": true,
"temperature_c": 23.4,
"entity_id": "sensor.room_temperature"
"ok": true
}
}
}
@@ -5497,7 +5475,7 @@
},
"version": {
"type": "string",
"example": "0.14.2"
"example": "0.14.4"
},
"uptime_seconds": {
"type": "integer",
@@ -6536,7 +6514,16 @@
"string",
"null"
],
"example": "sensor.living_room_temperature"
"example": "sensor.living_room_temperature",
"description": "Per-zone Home Assistant room-temperature entity. Required when sensor_source is home_assistant or combined."
},
"ha_outdoor_entity_id": {
"type": [
"string",
"null"
],
"example": "sensor.garden_temperature",
"description": "Optional per-zone Home Assistant outdoor-temperature override. When empty, the zone uses global outdoor_entity_id; when unavailable, control falls back to the global outdoor source."
},
"external_sensor_weight": {
"type": "number",
@@ -6586,6 +6573,20 @@
"type": "string",
"example": "gree-aabbccddeeff"
},
"ha_entity_id": {
"type": [
"string",
"null"
],
"description": "Per-zone Home Assistant room-temperature entity used by external/combined control."
},
"ha_outdoor_entity_id": {
"type": [
"string",
"null"
],
"description": "Optional per-zone Home Assistant outdoor-temperature override; empty uses global outdoor_entity_id."
},
"enabled": {
"type": "boolean"
},
@@ -7973,19 +7974,6 @@
],
"additionalProperties": false
},
"HaTestRequest": {
"type": "object",
"properties": {
"entity_id": {
"type": [
"string",
"null"
],
"description": "Optional. Defaults/aliases are resolved when omitted.",
"example": "sensor.room_temperature"
}
}
},
"HaEntityRequest": {
"type": "object",
"properties": {
@@ -8487,11 +8475,9 @@
"url": {
"type": "string"
},
"default_entity_id": {
"type": "string"
},
"outdoor_entity_id": {
"type": "string"
"type": "string",
"description": "Optional global Home Assistant outdoor-temperature entity. Zones use it by default and may override it with ha_outdoor_entity_id."
},
"sensor_stale_after_seconds": {
"type": "integer"
@@ -8521,7 +8507,6 @@
},
"required": [
"url",
"default_entity_id",
"outdoor_entity_id",
"sensor_stale_after_seconds",
"allow_invalid_tls",
@@ -8538,11 +8523,9 @@
"url": {
"type": "string"
},
"default_entity_id": {
"type": "string"
},
"outdoor_entity_id": {
"type": "string"
"type": "string",
"description": "Optional global Home Assistant outdoor-temperature entity. Zones use it by default and may override it with ha_outdoor_entity_id."
},
"sensor_stale_after_seconds": {
"type": "integer"
@@ -8576,7 +8559,6 @@
},
"required": [
"url",
"default_entity_id",
"outdoor_entity_id",
"sensor_stale_after_seconds",
"allow_invalid_tls",
+2 -7
View File
@@ -112,14 +112,9 @@ Tokens are used only during validation and are not written to the mapping file.
## HA as an external temperature source
This is independent from the custom climate integration. Each Rust controller zone can assign its own HA room-temperature entity, for example:
This is independent from the custom climate integration. Home Assistant room-temperature sensors are configured per zone with `ha_entity_id`. The recommended `combined` strategy keeps the GREE sensor as the primary input and uses the selected Home Assistant room sensor as a configurable supporting measurement (40% weight by default). If HA or the selected room entity becomes unavailable, the controller falls back to the corresponding GREE unit, so local control and schedules continue to run.
```text
Living room -> GREE Living Room + sensor.living_room_temperature
Bedroom -> GREE Bedroom + sensor.bedroom_temperature
```
The recommended `combined` strategy keeps the GREE sensor as the primary input and uses the room sensor as a configurable supporting measurement (40% weight by default). A zone may also select the room sensor as its preferred source. If HA or that entity becomes unavailable, the controller falls back to the corresponding GREE unit, so local control and schedules continue to run.
Outdoor temperature is separate: `outdoor_entity_id` / `HA_OUTDOOR_ENTITY_ID` is the global Home Assistant outdoor-temperature sensor. A zone may optionally set `ha_outdoor_entity_id` to use a different outdoor sensor for that zone's outdoor-temperature assist. If the override is unavailable, the global outdoor source is used. Global and per-zone outdoor entities participate in aliases, metrics/history and Flow entity suggestions. The Home Assistant connection test validates only the server URL/API token and does not require any entity.
## Zone climate and optional unit features
Each controller zone is exposed as a full Home Assistant `climate` entity with current/target temperature and HVAC modes: Off, Auto (follow the controller house mode), Cool and Heat. For a **zone thermostat**, Auto does not mean the GREE unit's native automatic heat/cool algorithm: it means **inherit the whole-house Heating/Cooling selection from GREE Controller**. Direct physical-device climate entities still use the native GREE Auto mode. The target temperature remains published while a zone is Off, so Home Assistant can display the configured setpoint instead of `unknown`. The existing zone target `number` and enabled `switch` remain available for compatibility.
@@ -1,7 +1,7 @@
{
"domain": "gree_controller",
"name": "GREE Controller",
"version": "0.14.2",
"version": "0.14.4",
"config_flow": true,
"integration_type": "hub",
"iot_class": "local_polling",
@@ -1,5 +1,17 @@
# Changelog
## 0.14.4
- Removes the obsolete Home Assistant connection-test/default entity; HA connection testing uses only the configured URL and token.
- `outdoor_entity_id` / `HA_OUTDOOR_ENTITY_ID` is the global Home Assistant outdoor-temperature sensor.
- Adds optional per-zone `ha_outdoor_entity_id`; when empty, the zone uses the global outdoor sensor, and when set it overrides the outdoor source only for that zone.
- Keeps Home Assistant room temperature sensors per zone via `ha_entity_id`.
- Global and per-zone outdoor sensors are available in aliases, metrics/history and Home Assistant entity suggestions used by Visual Flow.
## 0.14.3
- Home Assistant connection test now validates the configured server and token without requiring any entity.
## 0.14.2
- Improves connectivity controls: Local/LAN ping measurement can be explicitly enabled/disabled in Settings, disabled workers gray out their interval/sample fields, Cloud REST/MQTT measurement settings live in a dedicated block, and History → Pings can hide/show jitter with one button while rendering jitter as a dashed line.
@@ -1,5 +1,5 @@
name: "GREE Controller"
version: "0.14.2"
version: "0.14.4"
slug: "gree_controller"
description: "Local GREE HVAC controller with Web UI and Home Assistant integration"
url: "https://git.linuxiarz.pl/gru/gree-controller-ha-addon/"
+7 -5
View File
@@ -131,7 +131,10 @@
"zones.roomSensorEntity": "Room sensor entity_id",
"zones.roomSensorWeight": "Room sensor weight %",
"zones.maxDifference": "Max. sensor difference °C",
"zones.sensorHelp": "This sensor belongs only to this zone. If it becomes unavailable, the controller automatically falls back to the GREE sensor.",
"zones.sensorHelp": "Required when the temperature strategy uses Home Assistant. If it becomes unavailable, the controller falls back to the GREE sensor.",
"zones.sensorRequired": "Enter the Home Assistant room temperature entity_id for this zone.",
"zones.outdoorSensorEntity": "Outdoor temperature entity_id override",
"zones.outdoorSensorHelp": "Leave empty to use the global Outdoor temperature entity_id. Set another entity_id to use a different outdoor sensor only for this zone. It is used by outdoor-temperature assist and recorded in history/metrics.",
"zones.greeTemp": "GREE",
"zones.externalTemp": "Room",
"zones.usedSource": "used",
@@ -180,8 +183,6 @@
"settings.haTokenKeep": "Leave empty to keep the saved token",
"settings.haTokenSaved": "Token saved — leave empty to keep it",
"settings.haLongLivedToken": "Long-Lived Access Token",
"settings.defaultEntity": "Connection test entity_id",
"settings.defaultEntityHint": "This entity is used by the Home Assistant connection test when no other entity is specified. It is not automatically used as a zone temperature source.",
"settings.testHa": "Test HA",
"settings.systemState": "System status",
"settings.version": "Version",
@@ -247,7 +248,7 @@
"label.schedule": "schedule",
"label.automation": "automation",
"toast.found": "Found: {count}",
"toast.haTemperature": "Home Assistant: {temperature}°C",
"toast.haConnected": "Home Assistant connection OK",
"day.1": "Mon",
"day.2": "Tue",
"day.3": "Wed",
@@ -328,7 +329,8 @@
"schedules.chooseZone": "Choose a zone first",
"schedules.templateApplied": "Schedule preset applied",
"schedules.profile": "Profile",
"settings.outdoorEntity": "Outdoor temperature entity_id",
"settings.outdoorEntity": "Global outdoor temperature entity_id",
"settings.outdoorEntityHint": "Global Home Assistant outdoor temperature sensor. Zones use it by default and may override it with another outdoor sensor in zone settings.",
"settings.outdoorAssist": "Use outdoor temperature as smart-control assist",
"settings.allowInvalidTls": "Allow invalid/self-signed HTTPS certificate",
"settings.allowInvalidTlsHint": "Use only for a trusted local Home Assistant server, for example https://192.168.50.25.",
+8 -6
View File
@@ -128,10 +128,13 @@
"zones.demand": "Żądanie",
"zones.emptyTitle": "Brak stref",
"zones.emptyText": "Dodaj strefę, aby sterować temperaturą automatycznie.",
"zones.roomSensorEntity": "entity_id czujnika pokojowego",
"zones.roomSensorEntity": "entity_id czujnika temperatury pokojowej",
"zones.roomSensorWeight": "Waga czujnika pokojowego %",
"zones.maxDifference": "Maks. różnica czujników °C",
"zones.sensorHelp": "Ten czujnik jest przypisany tylko do tej strefy. Gdy przestanie bdostępny, kontroler automatycznie wróci do sensora GREE.",
"zones.sensorHelp": "Wymagany, gdy strategia temperatury używa Home Assistant. Gdy czujnik będzie niedostępny, kontroler wróci do sensora GREE.",
"zones.sensorRequired": "Podaj entity_id czujnika temperatury pokojowej z Home Assistant dla tej strefy.",
"zones.outdoorSensorEntity": "Nadpisanie entity_id temperatury zewnętrznej",
"zones.outdoorSensorHelp": "Pozostaw puste, aby użyć globalnego Outdoor temperature entity_id. Wybierz inne entity_id, aby tylko ta strefa używała innego czujnika zewnętrznego. Czujnik jest używany przez wspomaganie temperaturą zewnętrzną oraz zapisywany w historii i metrykach.",
"zones.greeTemp": "GREE",
"zones.externalTemp": "Pokój",
"zones.usedSource": "użyte",
@@ -180,8 +183,6 @@
"settings.haTokenKeep": "Pozostaw puste, aby zachować zapisany token",
"settings.haTokenSaved": "Token zapisany — pozostaw puste",
"settings.haLongLivedToken": "Długotrwały token dostępu",
"settings.defaultEntity": "Entity_id do testu połączenia",
"settings.defaultEntityHint": "Ta encja jest używana przez test połączenia z Home Assistant, gdy nie wskazano innej encji. Nie jest automatycznie źródłem temperatury strefy.",
"settings.testHa": "Testuj HA",
"settings.systemState": "Stan systemu",
"settings.version": "Wersja",
@@ -247,7 +248,7 @@
"label.schedule": "harmonogram",
"label.automation": "automatyzację",
"toast.found": "Znaleziono: {count}",
"toast.haTemperature": "Home Assistant: {temperature}°C",
"toast.haConnected": "Połączenie z Home Assistant działa",
"day.1": "Pn",
"day.2": "Wt",
"day.3": "Śr",
@@ -328,7 +329,8 @@
"schedules.chooseZone": "Najpierw wybierz strefę",
"schedules.templateApplied": "Zastosowano preset harmonogramu",
"schedules.profile": "Profil",
"settings.outdoorEntity": "entity_id temperatury zewnętrznej",
"settings.outdoorEntity": "Globalny entity_id temperatury zewnętrznej",
"settings.outdoorEntityHint": "Globalny czujnik temperatury zewnętrznej z Home Assistant. Strefy używają go domyślnie i mogą wskazać własny czujnik zewnętrzny w konfiguracji strefy.",
"settings.outdoorAssist": "Używaj temperatury zewnętrznej jako wsparcia inteligentnego sterowania",
"settings.allowInvalidTls": "Zezwól na nieważny/samopodpisany certyfikat HTTPS",
"settings.allowInvalidTlsHint": "Używaj tylko dla zaufanego lokalnego serwera Home Assistant, np. https://192.168.50.25.",
+4 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Developer smoke/integration tests for GREE Controller API 0.14.2.
"""Developer smoke/integration tests for GREE Controller API 0.14.4.
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.14.2"
DEFAULT_EXPECTED_VERSION = "0.14.4"
SETTINGS_PATHS: dict[str, set[str]] = {
"/api/settings/application": {"simulator_enabled"},
@@ -88,7 +88,6 @@ SETTINGS_PATHS: dict[str, set[str]] = {
"/api/settings/home-assistant": {
"url",
"token_configured",
"default_entity_id",
"outdoor_entity_id",
"sensor_stale_after_seconds",
"allow_invalid_tls",
@@ -580,7 +579,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.14.2 contract", lambda: test_openapi(client, args.expected_version))
runner.run("OpenAPI 0.14.4 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))
@@ -649,7 +648,7 @@ def run_suite(args: argparse.Namespace) -> int:
def parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Developer smoke/integration tests for GREE Controller API 0.14.2.",
description="Developer smoke/integration tests for GREE Controller API 0.14.4.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""Examples:
python3 scripts/api_dev_test.py
+14 -2
View File
@@ -27,7 +27,7 @@ async fn export_configuration(
fn validate_configuration_header(export: &ConfigurationExport) -> Result<(), AppError> {
if export.format_version != 3 {
return Err(AppError::BadRequest("unsupported configuration export version; version 3 is required by GREE Controller 0.14.2".into()));
return Err(AppError::BadRequest("unsupported configuration export version; version 3 is required by GREE Controller 0.14.4".into()));
}
if export.settings.control_strategy != "setpoint" {
return Err(AppError::BadRequest(
@@ -157,6 +157,18 @@ fn validate_configuration_devices_and_zones(
"import contains an invalid zone sensor source".into(),
));
}
if matches!(zone.sensor_source.as_str(), "home_assistant" | "combined")
&& zone
.ha_entity_id
.as_deref()
.map(str::trim)
.unwrap_or("")
.is_empty()
{
return Err(AppError::BadRequest(
"import contains a zone that needs a per-zone Home Assistant room temperature entity_id".into(),
));
}
}
let mut installation_devices = std::collections::HashSet::new();
for group in &export.device_groups {
@@ -687,7 +699,7 @@ fn normalize_imported_runtime_settings(settings: &mut RuntimeSettings) -> Result
fn prepare_configuration_import(export: &mut ConfigurationExport) -> Result<(), AppError> {
normalize_imported_runtime_settings(&mut export.settings)?;
for zone in &mut export.zones {
canonicalize_zone_ha_entity(zone, &export.settings.home_assistant);
canonicalize_zone_ha_entities(zone, &export.settings.home_assistant);
}
Ok(())
}
+46 -35
View File
@@ -130,13 +130,17 @@ fn sensor_history_with_fallback(
let mut existing: std::collections::HashSet<String> =
values.iter().map(|row| row.entity_id.clone()).collect();
for zone in state.db.list_zones()? {
let Some(entity_id) = zone
if !matches!(zone.sensor_source.as_str(), "home_assistant" | "combined") {
continue;
}
let entity_id = zone
.ha_entity_id
.as_deref()
.filter(|value| !value.trim().is_empty())
else {
.map(str::trim)
.unwrap_or("");
if entity_id.is_empty() {
continue;
};
}
if existing.contains(entity_id) {
continue;
}
@@ -162,31 +166,37 @@ fn sensor_history_with_fallback(
existing.insert(entity_id.to_string());
}
}
let outdoor_entity = outdoor_entity.trim();
if !outdoor_entity.is_empty() && !existing.contains(outdoor_entity) {
for zone in state.db.list_zones()? {
let rows =
state
.db
.list_zone_history(Some(&zone.id), since.clone(), bucket_seconds, limit)?;
let mut added = false;
for row in rows {
if let Some(temperature) = row.outdoor_temperature {
values.push(HaReading {
id: row.id,
entity_id: outdoor_entity.to_string(),
zone_id: None,
kind: "outdoor".into(),
timestamp: row.timestamp,
temperature,
});
added = true;
}
}
if added {
break;
let global_outdoor_entity = outdoor_entity.trim();
for zone in state.db.list_zones()? {
let zone_override = zone
.ha_outdoor_entity_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
let entity_id = zone_override.unwrap_or(global_outdoor_entity);
if entity_id.is_empty() || existing.contains(entity_id) {
continue;
}
let rows = state
.db
.list_zone_history(Some(&zone.id), since.clone(), bucket_seconds, limit)?;
let mut added = false;
for row in rows {
if let Some(temperature) = row.outdoor_temperature {
values.push(HaReading {
id: row.id,
entity_id: entity_id.to_string(),
zone_id: zone_override.map(|_| zone.id.clone()),
kind: "outdoor".into(),
timestamp: row.timestamp,
temperature,
});
added = true;
}
}
if added {
existing.insert(entity_id.to_string());
}
}
values.sort_by(|left, right| left.timestamp.cmp(&right.timestamp));
if values.len() > limit as usize {
@@ -332,7 +342,13 @@ async fn combined_sensor_history(
.db
.list_ha_history(entity_id, start, bucket_seconds, limit)?)
} else {
sensor_history_with_fallback(state, start, bucket_seconds, limit, outdoor_entity)
sensor_history_with_fallback(
state,
start,
bucket_seconds,
limit,
outdoor_entity,
)
}
};
if !influx.enabled || since >= cutoff {
@@ -391,13 +407,8 @@ async fn history(
let bucket_seconds = history_bucket_seconds(hours);
let limit = query.limit.unwrap_or(12_000).clamp(1, 20_000);
let scope = query.scope.as_deref().unwrap_or("zones");
let outdoor_entity = state
.settings
.read()
.await
.home_assistant
.outdoor_entity_id
.clone();
let ha_settings = state.settings.read().await.home_assistant.clone();
let outdoor_entity = ha_settings.outdoor_entity_id;
let (device_count, zone_count, ha_count) = state.db.history_counts()?;
match scope {
+4 -18
View File
@@ -11,28 +11,14 @@ async fn home_assistant_snapshot(
})))
}
#[derive(Debug, Deserialize)]
struct HaTestRequest {
entity_id: Option<String>,
}
async fn test_home_assistant(
State(state): State<AppState>,
Json(input): Json<HaTestRequest>,
) -> Result<Json<Value>, AppError> {
let settings = state.settings.read().await.clone();
let resolved_entity_id =
home_assistant::resolve_entity_id(&settings.home_assistant, input.entity_id.as_deref());
let temperature = home_assistant::read_temperature(
&state.http,
&settings.home_assistant,
resolved_entity_id.as_deref(),
Some(settings.home_assistant.sensor_stale_after_seconds),
)
.await
.map_err(|e| AppError::Device(e.to_string()))?;
Ok(Json(
json!({"ok": true, "temperature_c": temperature, "entity_id": resolved_entity_id}),
))
home_assistant::test_connection(&state.http, &settings.home_assistant)
.await
.map_err(|e| AppError::Device(e.to_string()))?;
Ok(Json(json!({"ok": true})))
}
#[derive(Debug, Deserialize)]
-1
View File
@@ -52,7 +52,6 @@ fn public_settings(settings: &RuntimeSettings) -> Value {
"url": settings.home_assistant.url,
"token": "",
"token_configured": !settings.home_assistant.token.trim().is_empty(),
"default_entity_id": settings.home_assistant.default_entity_id,
"outdoor_entity_id": settings.home_assistant.outdoor_entity_id,
"sensor_stale_after_seconds": settings.home_assistant.sensor_stale_after_seconds,
"allow_invalid_tls": settings.home_assistant.allow_invalid_tls,
+17 -13
View File
@@ -80,7 +80,6 @@ fn home_assistant_settings(settings: &RuntimeSettings) -> HomeAssistantSettingsV
HomeAssistantSettingsView {
url: settings.home_assistant.url.clone(),
token_configured: !settings.home_assistant.token.trim().is_empty(),
default_entity_id: settings.home_assistant.default_entity_id.clone(),
outdoor_entity_id: settings.home_assistant.outdoor_entity_id.clone(),
sensor_stale_after_seconds: settings.home_assistant.sensor_stale_after_seconds,
allow_invalid_tls: settings.home_assistant.allow_invalid_tls,
@@ -618,10 +617,6 @@ fn validate_flow_shared_inputs(
}
fn canonicalize_home_assistant_entities(settings: &mut HomeAssistantSettings) {
let default_entity = settings.default_entity_id.clone();
if let Some(entity_id) = home_assistant::resolve_entity_id(settings, Some(&default_entity)) {
settings.default_entity_id = entity_id;
}
let outdoor_entity = settings.outdoor_entity_id.clone();
if !outdoor_entity.trim().is_empty() {
if let Some(entity_id) = home_assistant::resolve_entity_id(settings, Some(&outdoor_entity))
@@ -645,11 +640,20 @@ fn validate_home_assistant_url(settings: &HomeAssistantSettings) -> Result<(), A
Ok(())
}
fn canonicalize_zone_ha_entity(zone: &mut Zone, settings: &HomeAssistantSettings) {
let Some(configured) = zone.ha_entity_id.clone() else {
return;
fn canonicalize_zone_ha_entities(zone: &mut Zone, settings: &HomeAssistantSettings) {
let room = zone.ha_entity_id.clone().unwrap_or_default();
zone.ha_entity_id = if room.trim().is_empty() {
None
} else {
home_assistant::resolve_entity_id(settings, Some(room.trim()))
};
let outdoor = zone.ha_outdoor_entity_id.clone().unwrap_or_default();
zone.ha_outdoor_entity_id = if outdoor.trim().is_empty() {
None
} else {
home_assistant::resolve_entity_id(settings, Some(outdoor.trim()))
};
zone.ha_entity_id = home_assistant::resolve_entity_id(settings, Some(&configured));
}
async fn canonicalize_saved_zone_entities(
@@ -673,9 +677,10 @@ async fn canonicalize_saved_zone_entities(
let Some(mut zone) = state.db.get_zone(&zone_id)? else {
continue;
};
let previous = zone.ha_entity_id.clone();
canonicalize_zone_ha_entity(&mut zone, settings);
if zone.ha_entity_id != previous {
let previous_room = zone.ha_entity_id.clone();
let previous_outdoor = zone.ha_outdoor_entity_id.clone();
canonicalize_zone_ha_entities(&mut zone, settings);
if zone.ha_entity_id != previous_room || zone.ha_outdoor_entity_id != previous_outdoor {
zone.updated_at = Utc::now();
state.db.save_zone(&zone)?;
state.broadcast("zone.updated", serde_json::to_value(&zone)?);
@@ -691,7 +696,6 @@ fn apply_home_assistant_update(
let mut next = HomeAssistantSettings {
url: input.url,
token: current.token.clone(),
default_entity_id: input.default_entity_id,
outdoor_entity_id: input.outdoor_entity_id,
sensor_stale_after_seconds: input.sensor_stale_after_seconds.clamp(30, 86_400),
allow_invalid_tls: input.allow_invalid_tls,
+11 -6
View File
@@ -44,6 +44,8 @@ struct ZoneInput {
sensor_source: String,
#[serde(default)]
ha_entity_id: Option<String>,
#[serde(default)]
ha_outdoor_entity_id: Option<String>,
#[serde(default = "external_sensor_weight")]
external_sensor_weight: f64,
#[serde(default = "max_sensor_difference")]
@@ -170,11 +172,13 @@ impl ZoneInput {
&& self
.ha_entity_id
.as_deref()
.map(|value| value.trim())
.map(str::trim)
.unwrap_or("")
.is_empty()
{
return Err(AppError::BadRequest("a per-zone Home Assistant entity_id is required for external or combined temperature control".into()));
return Err(AppError::BadRequest(
"a per-zone Home Assistant room temperature entity_id is required for external or combined temperature control".into(),
));
}
Ok(())
}
@@ -205,6 +209,7 @@ impl ZoneInput {
smart_fan: self.smart_fan,
sensor_source: self.sensor_source,
ha_entity_id: self.ha_entity_id.filter(|v| !v.trim().is_empty()),
ha_outdoor_entity_id: self.ha_outdoor_entity_id.filter(|v| !v.trim().is_empty()),
external_sensor_weight: self.external_sensor_weight,
max_sensor_difference: self.max_sensor_difference,
sensor_stale_after_seconds: self.sensor_stale_after_seconds,
@@ -289,6 +294,7 @@ async fn create_zone(
) -> Result<(StatusCode, Json<Zone>), AppError> {
let _configuration_guard = state.lock_configuration_operation().await;
let _reference_guard = state.lock_automation_operation().await;
let settings = state.settings.read().await.clone();
input.validate()?;
if state.db.get_device(&input.device_id)?.is_none() {
return Err(AppError::BadRequest("zone device does not exist".into()));
@@ -299,8 +305,7 @@ async fn create_zone(
// the newly created zone without participating in the zone operation lock.
let _device_guard = state.lock_device_operation(&input.device_id).await;
let mut zone = input.into_zone(Uuid::new_v4().to_string(), Utc::now());
let settings = state.settings.read().await.clone();
canonicalize_zone_ha_entity(&mut zone, &settings.home_assistant);
canonicalize_zone_ha_entities(&mut zone, &settings.home_assistant);
state.db.save_zone(&zone)?;
state.broadcast("zone.created", serde_json::to_value(&zone)?);
state.wake_zone_control();
@@ -313,6 +318,7 @@ async fn update_zone(
) -> Result<Json<Zone>, AppError> {
let _configuration_guard = state.lock_configuration_operation().await;
let _reference_guard = state.lock_automation_operation().await;
let settings = state.settings.read().await.clone();
input.validate()?;
let _zone_guard = state.lock_zone_operation(&id).await;
let mut existing = state
@@ -409,8 +415,7 @@ async fn update_zone(
.await?;
zone.revision = existing.revision.saturating_add(1);
}
let settings = state.settings.read().await.clone();
canonicalize_zone_ha_entity(&mut zone, &settings.home_assistant);
canonicalize_zone_ha_entities(&mut zone, &settings.home_assistant);
let power_off_device = !device_changed && existing.enabled && !zone.enabled;
if power_off_device {
// Full configuration PUT and quick-control disable use the same ownership cleanup.
-1
View File
@@ -124,7 +124,6 @@ impl Config {
home_assistant: HomeAssistantSettings {
url: env::var("HA_URL").unwrap_or_default(),
token: env::var("HA_TOKEN").unwrap_or_default(),
default_entity_id: env::var("HA_ENTITY_ID").unwrap_or_default(),
outdoor_entity_id: env::var("HA_OUTDOOR_ENTITY_ID").unwrap_or_default(),
sensor_stale_after_seconds: env_u64("HA_SENSOR_STALE_AFTER_SECONDS")
.unwrap_or(300)
+1 -1
View File
@@ -230,7 +230,7 @@ mod tests {
heat_comfort_setpoint: 21.0, heat_sleep_setpoint: 19.0, heat_away_setpoint: 17.0,
hysteresis: 0.6, separate_hysteresis: false, cool_hysteresis: 0.6, heat_hysteresis: 0.6,
min_on_seconds: 180, min_off_seconds: 180, min_adjust_seconds: 120, standby_offset_c: 2.0, smart_fan: true,
sensor_source: source.into(), ha_entity_id: Some("sensor.room_temperature".into()),
sensor_source: source.into(), ha_entity_id: Some("sensor.room_temperature".into()), ha_outdoor_entity_id: None,
external_sensor_weight: 0.4, max_sensor_difference: 3.0, sensor_stale_after_seconds: 300, device_temperature: None, external_temperature: None,
current_temperature: None, control_temperature_source: "device".into(), active_preset: "comfort".into(),
manual_preset: None, manual_setpoint: None, manual_override_until: None, local_thermostat_power: None, local_thermostat_resume_at: None, local_thermostat_restore_zone_enabled: None, temporary_quick_thermostat: None,
+64 -6
View File
@@ -86,6 +86,58 @@ async fn resolve_cycle_outdoor_temperature(
temperature
}
async fn read_cycle_zone_outdoor_sensors(
state: &AppState,
settings: &RuntimeSettings,
zones: &[Zone],
) -> HashMap<String, Option<f64>> {
futures_util::future::join_all(zones.iter().filter_map(|zone| {
let configured = zone
.ha_outdoor_entity_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())?;
let zone_id = zone.id.clone();
let resolved_entity =
home_assistant::resolve_entity_id(&settings.home_assistant, Some(configured));
let http = &state.http;
let ha_settings = &settings.home_assistant;
let poll_interval_seconds = settings.poll_interval_seconds;
Some(async move {
let Some(entity_id) = resolved_entity else {
return (zone_id, None);
};
match home_assistant::read_temperature(
http,
ha_settings,
Some(&entity_id),
Some(ha_settings.sensor_stale_after_seconds),
)
.await
{
Ok(value) => {
record_ha_history(
state,
&entity_id,
Some(&zone_id),
"outdoor",
value,
poll_interval_seconds,
);
(zone_id, Some(value))
}
Err(err) => {
tracing::debug!(zone_id=%zone_id, resolved_entity=%entity_id, error=?err, "zone outdoor Home Assistant sensor unavailable; using global outdoor fallback");
(zone_id, None)
}
}
})
}))
.await
.into_iter()
.collect()
}
async fn read_cycle_room_sensors(
state: &AppState,
settings: &RuntimeSettings,
@@ -479,13 +531,10 @@ async fn control_zones(state: &AppState) -> Result<()> {
.await?;
let device_snapshot = state.db.list_devices()?;
let outdoor_temperature =
let global_outdoor_temperature =
resolve_cycle_outdoor_temperature(state, &settings, &device_snapshot).await;
let outdoor_assist_temperature = if settings.outdoor_assist_enabled {
outdoor_temperature
} else {
None
};
let mut zone_outdoor_sensor_results =
read_cycle_zone_outdoor_sensors(state, &settings, &zone_snapshot).await;
let night_active = night_mode_active(&settings.night_mode, Local::now().time());
let mut room_sensor_results = read_cycle_room_sensors(state, &settings, &zone_snapshot).await;
@@ -498,6 +547,15 @@ async fn control_zones(state: &AppState) -> Result<()> {
continue;
};
let cycle_started_at = zone.updated_at.clone();
let outdoor_temperature = zone_outdoor_sensor_results
.remove(&zone.id)
.flatten()
.or(global_outdoor_temperature);
let outdoor_assist_temperature = if settings.outdoor_assist_enabled {
outdoor_temperature
} else {
None
};
if zone
.manual_override_until
.map(|until| until <= Utc::now())
+35 -10
View File
@@ -26,11 +26,7 @@ pub fn resolve_entity_id(
) -> Option<String> {
let requested = entity_override
.filter(|value| !value.trim().is_empty())
.map(str::trim)
.unwrap_or_else(|| settings.default_entity_id.trim());
if requested.is_empty() {
return None;
}
.map(str::trim)?;
// Aliases are presentation-only. Accepting an alias here is a defensive
// compatibility path for settings saved by older UI revisions or manual edits;
@@ -48,6 +44,39 @@ pub fn resolve_entity_id(
Some(requested.to_string())
}
pub async fn test_connection(
default_client: &reqwest::Client,
settings: &HomeAssistantSettings,
) -> Result<()> {
if settings.url.trim().is_empty() {
bail!("Home Assistant URL is not configured")
}
if settings.token.trim().is_empty() {
bail!("Home Assistant token is not configured")
}
let mut base = Url::parse(settings.url.trim()).context("invalid Home Assistant URL")?;
if !matches!(base.scheme(), "http" | "https") {
bail!("Home Assistant URL must use http or https")
}
base = base.join("api/").context("cannot build Home Assistant API URL")?;
let response = request_client(default_client, settings)?
.get(base)
.bearer_auth(settings.token.trim())
.header("Accept", "application/json")
.send()
.await
.context("Home Assistant request failed")?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
bail!(
"Home Assistant returned {status}: {}",
body.chars().take(200).collect::<String>()
)
}
Ok(())
}
pub async fn read_temperature(
default_client: &reqwest::Client,
settings: &HomeAssistantSettings,
@@ -136,7 +165,6 @@ mod tests {
HomeAssistantSettings {
url: "http://homeassistant.local:8123".into(),
token: "token".into(),
default_entity_id: "sensor.salon_temperature".into(),
outdoor_entity_id: "sensor.zewnatrz_temperature".into(),
sensor_stale_after_seconds: 300,
allow_invalid_tls: false,
@@ -156,10 +184,7 @@ mod tests {
resolve_entity_id(&settings, Some("Gabinet")).as_deref(),
Some("sensor.gabinet_temperature")
);
assert_eq!(
resolve_entity_id(&settings, None).as_deref(),
Some("sensor.salon_temperature")
);
assert_eq!(resolve_entity_id(&settings, None), None);
}
}
+1 -3
View File
@@ -13,9 +13,7 @@ pub struct HomeAssistantSettings {
pub url: String,
#[serde(default)]
pub token: String,
#[serde(default)]
pub default_entity_id: String,
/// Optional outdoor temperature sensor used only as an assist signal.
/// Global Home Assistant outdoor temperature sensor. Zones may override it individually.
#[serde(default)]
pub outdoor_entity_id: String,
/// Maximum accepted age of Home Assistant sensor samples.
-2
View File
@@ -128,7 +128,6 @@ pub struct HomeAssistantSettingsUpdate {
pub url: String,
#[serde(default)]
pub token: Option<String>,
pub default_entity_id: String,
pub outdoor_entity_id: String,
pub sensor_stale_after_seconds: u64,
pub allow_invalid_tls: bool,
@@ -141,7 +140,6 @@ pub struct HomeAssistantSettingsUpdate {
pub struct HomeAssistantSettingsView {
pub url: String,
pub token_configured: bool,
pub default_entity_id: String,
pub outdoor_entity_id: String,
pub sensor_stale_after_seconds: u64,
pub allow_invalid_tls: bool,
+5
View File
@@ -52,8 +52,13 @@ pub struct Zone {
pub smart_fan: bool,
#[serde(default = "default_sensor_source")]
pub sensor_source: String,
/// Per-zone Home Assistant room temperature sensor used by external/combined control.
#[serde(default)]
pub ha_entity_id: Option<String>,
/// Optional per-zone Home Assistant outdoor temperature sensor. When absent, the zone
/// uses `HomeAssistantSettings::outdoor_entity_id` (and then the existing GREE fallback).
#[serde(default)]
pub ha_outdoor_entity_id: Option<String>,
/// Weight of the optional room sensor when sensor_source is `combined`.
#[serde(default = "default_external_sensor_weight")]
pub external_sensor_weight: f64,
+11 -11
View File
@@ -437,13 +437,9 @@
</div>
</div>
<div class="settings-grid">
<label class="wide"><span data-i18n="settings.defaultEntity">Connection test entity_id</span><input
name="ha_entity_id" placeholder="sensor.living_room_temperature"></label>
<p class="field-note wide" data-i18n="settings.defaultEntityHint">This entity is used by the Home Assistant
connection test when no other entity is specified. It is not automatically used as a zone temperature
source.</p>
<label class="wide"><span data-i18n="settings.outdoorEntity">Outdoor temperature entity_id</span><input
name="ha_outdoor_entity_id" placeholder="sensor.outdoor_temperature"></label>
<label class="wide"><span data-i18n="settings.outdoorEntity">Global outdoor temperature entity_id</span><input
name="ha_outdoor_entity_id" list="haEntitySuggestions" placeholder="sensor.outdoor_temperature"></label>
<p class="field-note wide" data-i18n="settings.outdoorEntityHint">Global Home Assistant outdoor temperature sensor. Zones use it by default and may override it with another outdoor sensor in zone settings.</p>
<label class="wide"><span data-i18n="settings.haSensorStaleAfterMinutes">Maximum HA sensor reading age
(min)</span><input type="number" name="ha_sensor_stale_after_minutes" min="1" max="1440" step="1"
value="5"></label>
@@ -477,7 +473,7 @@
</div>
<div class="sensor-alias-manager">
<div id="sensorAliasList" class="sensor-alias-list"></div>
<div class="sensor-alias-add"><input id="sensorAliasEntity" placeholder="sensor.gabinet_temperature"><input
<div class="sensor-alias-add"><input id="sensorAliasEntity" list="haEntitySuggestions" placeholder="sensor.gabinet_temperature"><input
id="sensorAliasName" data-i18n-placeholder="settings.aliasPlaceholder" placeholder="Gabinet"><button
type="button" class="secondary" id="addSensorAlias" data-i18n="actions.add">Add</button></div>
</div>
@@ -1303,6 +1299,8 @@
</div>
</dialog>
<datalist id="haEntitySuggestions"></datalist>
<dialog id="zoneDialog">
<form method="dialog" id="zoneForm" class="dialog-form">
<input type="hidden" name="id">
@@ -1358,15 +1356,17 @@
°C</span><input type="text" inputmode="decimal" step="0.1" min="0.1" max="5" name="heat_hysteresis"
value="0.6"></label></div>
<div id="externalSensorFields">
<label><span data-i18n="zones.roomSensorEntity">Room sensor entity_id</span><input name="ha_entity_id"
<label><span data-i18n="zones.roomSensorEntity">Room sensor entity_id</span><input name="ha_entity_id" list="haEntitySuggestions"
placeholder="sensor.living_room_temperature"></label>
<div class="two"><label><span data-i18n="zones.roomSensorWeight">Room sensor weight %</span><input type="number"
step="5" min="0" max="100" name="external_sensor_weight_percent" value="40"></label><label><span
data-i18n="zones.maxDifference">Max. sensor difference °C</span><input type="text" inputmode="decimal"
step="0.1" min="0.1" max="20" name="max_sensor_difference" value="3.0"></label></div>
<p class="field-note" data-i18n="zones.sensorHelp">The room sensor is assigned only to this zone. If it becomes
unavailable, the controller falls back to the GREE sensor.</p>
<p class="field-note" data-i18n="zones.sensorHelp">This room sensor is required when the temperature strategy uses Home Assistant. If it becomes unavailable, the controller falls back to the GREE sensor.</p>
</div>
<label><span data-i18n="zones.outdoorSensorEntity">Outdoor temperature entity_id override</span><input name="ha_outdoor_entity_id" list="haEntitySuggestions"
placeholder="sensor.outdoor_temperature"></label>
<p class="field-note" data-i18n="zones.outdoorSensorHelp">Leave empty to use the global Outdoor temperature entity_id. Set another entity_id to use a different outdoor sensor only for this zone. It is used by outdoor-temperature assist and recorded in history/metrics.</p>
<div class="two"><label><span data-i18n="zones.minAdjust">Minimum adjustment interval (s)</span><input
type="number" min="15" name="min_adjust_seconds" value="120"></label><label><span
data-i18n="zones.standbyOffset">Standby setpoint offset °C</span><input type="text" inputmode="decimal"
+2 -1
View File
@@ -18,7 +18,8 @@ function historyEntityOptions() {
const devices = app.devices.map(device => `<option value="${esc(device.id)}">${esc(device.name)}</option>`).join('');
const entities = [...new Set([
...app.historyData.sensors.map(row => row.entity_id),
...app.zones.map(zone => zone.ha_entity_id).filter(Boolean),
...app.zones.map(zoneHaEntityId).filter(Boolean),
...app.zones.map(zone => zone.ha_outdoor_entity_id).filter(Boolean),
app.settings?.home_assistant?.outdoor_entity_id,
].filter(Boolean))].sort();
const sensors = entities.map(entity => `<option value="${esc(entity)}">${esc(haSensorLabel(entity))}</option>`).join('');
+1
View File
@@ -177,6 +177,7 @@ function updateTemporaryThermostatCountdowns() {
}
}
const haSensorLabel = entity => app.sensorAliases?.[entity] || app.settings?.home_assistant?.sensor_aliases?.[entity] || entity;
const zoneHaEntityId = zone => zone?.ha_entity_id || '';
function syncTopbarHeight() {
const topbar = $('.topbar');
+1 -1
View File
@@ -392,7 +392,7 @@ function renderSimulationPage() {
const next = (planZone.next_events || [])[0];
const source = zoneControlSourceLabel(planZone.control_source);
const sensorName = planZone.control_source === 'external' || planZone.control_source === 'combined'
? haSensorLabel(zone?.ha_entity_id || source)
? haSensorLabel(zoneHaEntityId(zone) || source)
: (planZone.device_name || tr('common.device'));
const fanText = sim.fanSpeed == null ? '—' : fanLabel(sim.fanSpeed);
const quietText = sim.quiet;
+3 -1
View File
@@ -298,6 +298,7 @@ $('#copyZoneSettings')?.addEventListener('click', () => {
device_id: f.elements.device_id.value,
sensor_source: f.elements.sensor_source.value,
ha_entity_id: f.elements.ha_entity_id.value,
ha_outdoor_entity_id: f.elements.ha_outdoor_entity_id.value,
enabled: f.elements.enabled.checked,
};
const copyFields = ['setpoint', 'cool_comfort_setpoint', 'cool_sleep_setpoint', 'cool_away_setpoint', 'heat_comfort_setpoint', 'heat_sleep_setpoint', 'heat_away_setpoint', 'hysteresis', 'cool_hysteresis', 'heat_hysteresis', 'min_on_seconds', 'min_off_seconds', 'min_adjust_seconds', 'standby_offset_c', 'external_sensor_weight_percent', 'max_sensor_difference', 'sensor_stale_after_seconds'];
@@ -317,6 +318,7 @@ $('#copyZoneSettings')?.addEventListener('click', () => {
f.elements.device_id.value = protectedValues.device_id;
f.elements.sensor_source.value = protectedValues.sensor_source;
f.elements.ha_entity_id.value = protectedValues.ha_entity_id;
f.elements.ha_outdoor_entity_id.value = protectedValues.ha_outdoor_entity_id;
f.elements.enabled.checked = protectedValues.enabled;
f.dataset.editingZoneId = targetId;
updateZoneSensorFields();
@@ -525,7 +527,7 @@ $('#zoneForm').addEventListener('submit', async event => {
heat_comfort_setpoint: parseDecimal(raw.heat_comfort_setpoint), heat_sleep_setpoint: parseDecimal(raw.heat_sleep_setpoint), heat_away_setpoint: parseDecimal(raw.heat_away_setpoint),
hysteresis: parseDecimal(raw.hysteresis), separate_hysteresis: form.separate_hysteresis.checked, cool_hysteresis: parseDecimal(raw.cool_hysteresis), heat_hysteresis: parseDecimal(raw.heat_hysteresis), min_on_seconds: Number(raw.min_on_seconds), min_off_seconds: Number(raw.min_off_seconds),
min_adjust_seconds: Number(raw.min_adjust_seconds), standby_offset_c: parseDecimal(raw.standby_offset_c), smart_fan: form.smart_fan.checked,
sensor_source: raw.sensor_source, ha_entity_id: raw.ha_entity_id || null, external_sensor_weight: Number(raw.external_sensor_weight_percent) / 100, max_sensor_difference: parseDecimal(raw.max_sensor_difference), sensor_stale_after_seconds: Number(raw.sensor_stale_after_seconds || 300), revision: raw.revision ? Number(raw.revision) : null
sensor_source: raw.sensor_source, ha_entity_id: raw.ha_entity_id || null, ha_outdoor_entity_id: raw.ha_outdoor_entity_id || null, external_sensor_weight: Number(raw.external_sensor_weight_percent) / 100, max_sensor_difference: parseDecimal(raw.max_sensor_difference), sensor_stale_after_seconds: Number(raw.sensor_stale_after_seconds || 300), revision: raw.revision ? Number(raw.revision) : null
};
await runFormTask(form, async () => {
await api(id ? `/api/zones/${encodeURIComponent(id)}` : '/api/zones', { method: id ? 'PUT' : 'POST', body }); form.closest('dialog').close(); form.reset(); await loadBootstrap(); toast(tr('common.saved'));
+11 -11
View File
@@ -316,10 +316,10 @@ function flowNodeSummary(node) {
if (node.kind === 'outdoor_temperature') return `${flowOperatorLabel(c.operator)} ${fmtTemp(c.value)}`;
if (node.kind === 'device_temperature') return `${app.devices.find(d => d.id === c.device_id)?.name || tr('common.noDevice')} · ${flowOperatorLabel(c.operator)} ${fmtTemp(c.value)}`;
if (node.kind === 'zone_temperature') return `${app.zones.find(z => z.id === c.zone_id)?.name || tr('common.noZone')} · ${flowOperatorLabel(c.operator)} ${fmtTemp(c.value)}`;
if (node.kind === 'ha_state') return `${c.entity_id || 'entity_id'} ${c.operator === 'neq' ? '≠' : '='} ${c.value || '—'}`;
if (node.kind === 'ha_numeric') return `${c.entity_id || 'entity_id'} · ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`;
if (node.kind === 'ha_attribute') return `${c.entity_id || 'entity_id'}.${c.attribute || 'attribute'} ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`;
if (node.kind === 'ha_available') return `${c.entity_id || 'entity_id'} · ${tr('flow.available')}`;
if (node.kind === 'ha_state') return `${c.entity_id ? haSensorLabel(c.entity_id) : 'entity_id'} ${c.operator === 'neq' ? '≠' : '='} ${c.value || '—'}`;
if (node.kind === 'ha_numeric') return `${c.entity_id ? haSensorLabel(c.entity_id) : 'entity_id'} · ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`;
if (node.kind === 'ha_attribute') return `${c.entity_id ? haSensorLabel(c.entity_id) : 'entity_id'}.${c.attribute || 'attribute'} ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`;
if (node.kind === 'ha_available') return `${c.entity_id ? haSensorLabel(c.entity_id) : 'entity_id'} · ${tr('flow.available')}`;
if (node.kind === 'house_mode') return `${tr('flow.houseMode')} ${flowOperatorLabel(c.operator)} ${c.value || '—'}`;
if (node.kind === 'device_state') return `${app.devices.find(d => d.id === c.device_id)?.name || tr('common.noDevice')} · ${c.field || 'state'} ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`;
if (node.kind === 'zone_state') return `${app.zones.find(z => z.id === c.zone_id)?.name || tr('common.noZone')} · ${c.field || 'state'} ${flowOperatorLabel(c.operator)} ${c.value ?? '—'}`;
@@ -506,15 +506,15 @@ function renderFlowInspector() {
else if (node.kind === 'on_change') fields = `<label><span>${esc(tr('flow.changeMode'))}</span><select data-flow-config="mode"><option value="result" ${c.mode !== 'value' ? 'selected' : ''}>${esc(tr('flow.changeModeResult'))}</option><option value="value" ${c.mode === 'value' ? 'selected' : ''}>${esc(tr('flow.changeModeValue'))}</option></select></label><p class="field-note">${esc(tr('flow.onChangeHint'))}</p>`;
else if (node.kind === 'rate_limit') fields = `<div class="two"><label><span>${esc(tr('flow.maxExecutions'))}</span><input type="number" min="1" max="1000" data-flow-config="max_count" value="${Number(c.max_count || 1)}"></label><label><span>${esc(tr('flow.periodSeconds'))}</span><input type="number" min="1" max="2678400" data-flow-config="period_seconds" value="${Number(c.period_seconds || 3600)}"></label></div><p class="field-note">${esc(tr('flow.rateLimitHint'))}</p>`;
else if (node.kind === 'delay') fields = `<label><span>${esc(tr('flow.durationSeconds'))}</span><input type="number" min="1" max="604800" data-flow-config="seconds" value="${Number(c.seconds || 30)}"></label><p class="field-note">${esc(tr('flow.delayHint'))}</p>`;
else if (node.kind === 'rolling_stat') { const source = c.source || 'outdoor_temperature'; fields = `<label><span>${esc(tr('flow.source'))}</span><select data-flow-config="source">${[['outdoor_temperature', tr('flow.node.outdoorTemperature')], ['device_temperature', tr('flow.node.deviceTemperature')], ['zone_temperature', tr('flow.node.zoneTemperature')], ['ha_numeric', tr('flow.node.haNumeric')]].map(([v, l]) => `<option value="${v}" ${source === v ? 'selected' : ''}>${esc(l)}</option>`).join('')}</select></label>${source === 'device_temperature' ? `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label>` : ''}${source === 'zone_temperature' ? `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label>` : ''}${source === 'ha_numeric' ? `<label><span>entity_id</span><input data-flow-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="sensor.temperature"></label>` : ''}<div class="two"><label><span>${esc(tr('flow.statistic'))}</span><select data-flow-config="statistic"><option value="mean" ${c.statistic !== 'median' ? 'selected' : ''}>${esc(tr('flow.mean'))}</option><option value="median" ${c.statistic === 'median' ? 'selected' : ''}>${esc(tr('flow.median'))}</option></select></label><label><span>${esc(tr('flow.windowSeconds'))}</span><input type="number" min="10" max="604800" data-flow-config="window_seconds" value="${Number(c.window_seconds || 300)}"></label></div>${flowComparisonFields(c, false)}`; }
else if (node.kind === 'oscillates') { const source = c.source || 'outdoor_temperature'; fields = `<label><span>${esc(tr('flow.source'))}</span><select data-flow-config="source">${[['outdoor_temperature', tr('flow.node.outdoorTemperature')], ['device_temperature', tr('flow.node.deviceTemperature')], ['zone_temperature', tr('flow.node.zoneTemperature')], ['ha_numeric', tr('flow.node.haNumeric')]].map(([v, l]) => `<option value="${v}" ${source === v ? 'selected' : ''}>${esc(l)}</option>`).join('')}</select></label>${source === 'device_temperature' ? `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label>` : ''}${source === 'zone_temperature' ? `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label>` : ''}${source === 'ha_numeric' ? `<label><span>entity_id</span><input data-flow-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="sensor.temperature"></label>` : ''}<div class="two"><label><span>${esc(tr('flow.windowSeconds'))}</span><input type="number" min="10" max="604800" data-flow-config="window_seconds" value="${Number(c.window_seconds || 300)}"></label><label><span>${esc(tr('flow.minSpan'))}</span><input type="number" min="0.001" step="0.1" data-flow-config="min_span" value="${Number(c.min_span ?? 1)}"></label></div><label><span>${esc(tr('flow.minDirectionChanges'))}</span><input type="number" min="1" max="1000" data-flow-config="min_direction_changes" value="${Number(c.min_direction_changes || 2)}"></label><p class="field-note">${esc(tr('flow.oscillatesHint'))}</p>`; }
else if (node.kind === 'rolling_stat') { const source = c.source || 'outdoor_temperature'; fields = `<label><span>${esc(tr('flow.source'))}</span><select data-flow-config="source">${[['outdoor_temperature', tr('flow.node.outdoorTemperature')], ['device_temperature', tr('flow.node.deviceTemperature')], ['zone_temperature', tr('flow.node.zoneTemperature')], ['ha_numeric', tr('flow.node.haNumeric')]].map(([v, l]) => `<option value="${v}" ${source === v ? 'selected' : ''}>${esc(l)}</option>`).join('')}</select></label>${source === 'device_temperature' ? `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label>` : ''}${source === 'zone_temperature' ? `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label>` : ''}${source === 'ha_numeric' ? `<label><span>entity_id</span><input data-flow-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="sensor.temperature"></label>` : ''}<div class="two"><label><span>${esc(tr('flow.statistic'))}</span><select data-flow-config="statistic"><option value="mean" ${c.statistic !== 'median' ? 'selected' : ''}>${esc(tr('flow.mean'))}</option><option value="median" ${c.statistic === 'median' ? 'selected' : ''}>${esc(tr('flow.median'))}</option></select></label><label><span>${esc(tr('flow.windowSeconds'))}</span><input type="number" min="10" max="604800" data-flow-config="window_seconds" value="${Number(c.window_seconds || 300)}"></label></div>${flowComparisonFields(c, false)}`; }
else if (node.kind === 'oscillates') { const source = c.source || 'outdoor_temperature'; fields = `<label><span>${esc(tr('flow.source'))}</span><select data-flow-config="source">${[['outdoor_temperature', tr('flow.node.outdoorTemperature')], ['device_temperature', tr('flow.node.deviceTemperature')], ['zone_temperature', tr('flow.node.zoneTemperature')], ['ha_numeric', tr('flow.node.haNumeric')]].map(([v, l]) => `<option value="${v}" ${source === v ? 'selected' : ''}>${esc(l)}</option>`).join('')}</select></label>${source === 'device_temperature' ? `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label>` : ''}${source === 'zone_temperature' ? `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label>` : ''}${source === 'ha_numeric' ? `<label><span>entity_id</span><input data-flow-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="sensor.temperature"></label>` : ''}<div class="two"><label><span>${esc(tr('flow.windowSeconds'))}</span><input type="number" min="10" max="604800" data-flow-config="window_seconds" value="${Number(c.window_seconds || 300)}"></label><label><span>${esc(tr('flow.minSpan'))}</span><input type="number" min="0.001" step="0.1" data-flow-config="min_span" value="${Number(c.min_span ?? 1)}"></label></div><label><span>${esc(tr('flow.minDirectionChanges'))}</span><input type="number" min="1" max="1000" data-flow-config="min_direction_changes" value="${Number(c.min_direction_changes || 2)}"></label><p class="field-note">${esc(tr('flow.oscillatesHint'))}</p>`; }
else if (node.kind === 'outdoor_temperature') fields = flowComparisonFields(c);
else if (node.kind === 'device_temperature') fields = `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label>${flowComparisonFields(c)}`;
else if (node.kind === 'zone_temperature') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label>${flowComparisonFields(c)}`;
else if (node.kind === 'ha_state') fields = `<label><span>entity_id</span><input data-flow-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label><div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator"><option value="eq" ${c.operator !== 'neq' ? 'selected' : ''}>=</option><option value="neq" ${c.operator === 'neq' ? 'selected' : ''}>≠</option></select></label><label><span>${esc(tr('flow.state'))}</span><input data-flow-config="value" value="${esc(c.value || '')}" placeholder="on"></label></div>`;
else if (node.kind === 'ha_numeric') fields = `<label><span>entity_id</span><input data-flow-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="sensor.outdoor_temperature"></label>${flowComparisonFields(c, false)}`;
else if (node.kind === 'ha_attribute') fields = `<label><span>entity_id</span><input data-flow-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="climate.living_room"></label><label><span>${esc(tr('flow.attribute'))}</span><input data-flow-config="attribute" value="${esc(c.attribute || '')}" placeholder="hvac_action"></label>${flowTextComparisonFields(c, true)}`;
else if (node.kind === 'ha_available') fields = `<label><span>entity_id</span><input data-flow-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label><p class="field-note">${esc(tr('flow.haAvailableHint'))}</p>`;
else if (node.kind === 'ha_state') fields = `<label><span>entity_id</span><input data-flow-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label><div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator"><option value="eq" ${c.operator !== 'neq' ? 'selected' : ''}>=</option><option value="neq" ${c.operator === 'neq' ? 'selected' : ''}>≠</option></select></label><label><span>${esc(tr('flow.state'))}</span><input data-flow-config="value" value="${esc(c.value || '')}" placeholder="on"></label></div>`;
else if (node.kind === 'ha_numeric') fields = `<label><span>entity_id</span><input data-flow-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="sensor.outdoor_temperature"></label>${flowComparisonFields(c, false)}`;
else if (node.kind === 'ha_attribute') fields = `<label><span>entity_id</span><input data-flow-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="climate.living_room"></label><label><span>${esc(tr('flow.attribute'))}</span><input data-flow-config="attribute" value="${esc(c.attribute || '')}" placeholder="hvac_action"></label>${flowTextComparisonFields(c, true)}`;
else if (node.kind === 'ha_available') fields = `<label><span>entity_id</span><input data-flow-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label><p class="field-note">${esc(tr('flow.haAvailableHint'))}</p>`;
else if (node.kind === 'house_mode') fields = `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator"><option value="eq" ${c.operator !== 'neq' ? 'selected' : ''}>=</option><option value="neq" ${c.operator === 'neq' ? 'selected' : ''}>≠</option></select></label><label><span>${esc(tr('flow.houseMode'))}</span><select data-flow-config="value">${['cool', 'heat', 'off'].map(v => `<option value="${v}" ${c.value === v ? 'selected' : ''}>${esc(v === 'off' ? tr('common.off') : tr(`mode.${v}`))}</option>`).join('')}</select></label></div>`;
else if (node.kind === 'device_state') fields = `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-flow-config="field">${['enabled', 'online', 'power', 'mode', 'fan_speed', 'swing_vertical', 'swing_horizontal', 'quiet', 'turbo', 'light', 'air', 'xfan', 'health', 'sleep'].map(v => `<option value="${v}" ${c.field === v ? 'selected' : ''}>${esc(deviceCommandFieldLabel(v))}</option>`).join('')}</select></label>${flowTextComparisonFields(c)}`;
else if (node.kind === 'zone_state') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-flow-config="field">${['enabled', 'mode', 'active_preset', 'demand', 'control_owner', 'device_manual_override', 'local_thermostat_power'].map(v => `<option value="${v}" ${c.field === v ? 'selected' : ''}>${esc(v)}</option>`).join('')}</select></label>${flowTextComparisonFields(c)}`;
@@ -533,7 +533,7 @@ function renderFlowInspector() {
else if (node.kind === 'zone_thermostat') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-flow-config="zone_id">${flowSelectOptions(app.zones, c.zone_id)}</select></label><div class="two"><label><span>${esc(tr('schedules.profile'))}</span><select data-flow-config="preset">${['auto', 'comfort', 'sleep', 'away', 'custom'].map(v => `<option value="${v}" ${c.preset === v ? 'selected' : ''}>${esc(zonePresetLabel(v))}</option>`).join('')}</select></label><label><span>${esc(tr('common.targetC'))}</span><input type="number" min="8" max="30" step="0.1" data-flow-config="setpoint" value="${Number(c.setpoint ?? 21)}"></label></div><div class="two"><label><span>${esc(tr('common.mode'))}</span><select data-flow-config="mode"><option value="auto" ${c.mode === 'auto' ? 'selected' : ''}>Auto</option><option value="heat" ${c.mode === 'heat' ? 'selected' : ''}>${esc(tr('mode.heat'))}</option><option value="cool" ${c.mode === 'cool' ? 'selected' : ''}>${esc(tr('mode.cool'))}</option></select></label><label><span>${esc(tr('common.power'))}</span><select data-flow-config="power"><option value="" ${c.power == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option><option value="true" ${c.power === true ? 'selected' : ''}>${esc(tr('common.on'))}</option><option value="false" ${c.power === false ? 'selected' : ''}>${esc(tr('common.off'))}</option></select></label></div><details class="flow-device-options"><summary>${esc(tr('flow.thermostatDeviceOptions'))}</summary><div class="two">${flowOptionalBoolField(c, 'swing_vertical')}${flowOptionalBoolField(c, 'swing_horizontal')}</div><p class="field-note">${esc(tr('flow.thermostatDeviceOptionsHint'))}</p></details><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
else if (node.kind === 'device_action') fields = `<label><span>${esc(tr('common.device'))}</span><select data-flow-config="device_id">${flowSelectOptions(app.devices, c.device_id)}</select></label>${flowActionFields(c, false)}`;
else if (node.kind === 'group_action') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-flow-config="group_id">${flowSelectOptions(app.groups, c.group_id)}</select></label>${flowActionFields(c, true)}`;
else if (node.kind === 'ha_service_action') fields = `<div class="two"><label><span>domain</span><input data-flow-config="domain" value="${esc(c.domain || 'switch')}" placeholder="switch"></label><label><span>service</span><input data-flow-config="service" value="${esc(c.service || 'turn_on')}" placeholder="turn_on"></label></div><p class="field-note">${esc(tr('flow.serviceExample'))}</p><label><span>entity_id</span><input data-flow-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="switch.gas"></label><label><span>${esc(tr('flow.serviceData'))}</span><textarea rows="5" data-flow-config="data_json">${esc(JSON.stringify(c.data || {}, null, 2))}</textarea></label><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
else if (node.kind === 'ha_service_action') fields = `<div class="two"><label><span>domain</span><input data-flow-config="domain" value="${esc(c.domain || 'switch')}" placeholder="switch"></label><label><span>service</span><input data-flow-config="service" value="${esc(c.service || 'turn_on')}" placeholder="turn_on"></label></div><p class="field-note">${esc(tr('flow.serviceExample'))}</p><label><span>entity_id</span><input data-flow-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="switch.gas"></label><label><span>${esc(tr('flow.serviceData'))}</span><textarea rows="5" data-flow-config="data_json">${esc(JSON.stringify(c.data || {}, null, 2))}</textarea></label><label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
host.innerHTML = `<div class="flow-inspector-head"><div><span class="eyebrow">${esc(tr('flow.blockSettings'))}</span><h3>${esc(flowNodeTitle(meta))}</h3></div><div class="flow-inspector-head-actions"><button type="button" class="secondary flow-inspector-expand" data-action="flow-toggle-inspector" aria-label="${esc(tr('flow.expandSettings'))}">↕</button><button type="button" class="secondary flow-inspector-close" data-action="flow-clear-selection" aria-label="${esc(tr('flow.clearSelection'))}">×</button><button type="button" class="danger flow-inspector-delete" data-flow-remove="${esc(node.id)}">${esc(tr('actions.delete'))}</button></div></div>${fields}<div class="flow-inspector-meta"><small>ID</small><code>${esc(node.id)}</code></div>`;
}
+13 -3
View File
@@ -114,7 +114,7 @@ function validateForm(form) {
if (form.id === 'homeAssistantForm') {
const url = form.elements.ha_url;
if (url?.value && !httpUrlValid(url.value)) { showFieldError(url, tr('validation.url')); valid = false; }
['ha_entity_id', 'ha_outdoor_entity_id'].forEach(name => {
['ha_outdoor_entity_id'].forEach(name => {
const field = form.elements[name];
if (field?.value && !entityIdValid(field.value)) { showFieldError(field, tr('validation.entityId')); valid = false; }
});
@@ -133,8 +133,18 @@ function validateForm(form) {
if (form.id === 'zoneForm') {
const source = form.elements.sensor_source?.value;
const entity = form.elements.ha_entity_id;
if (source !== 'device' && (!entity?.value?.trim() || !entityIdValid(entity.value))) {
showFieldError(entity, entity?.value?.trim() ? tr('validation.entityId') : tr('validation.required'));
const configuredEntity = entity?.value?.trim() || '';
if (source !== 'device' && !configuredEntity) {
showFieldError(entity, tr('zones.sensorRequired'));
valid = false;
} else if (source !== 'device' && !entityIdValid(configuredEntity)) {
showFieldError(entity, tr('validation.entityId'));
valid = false;
}
const outdoorEntity = form.elements.ha_outdoor_entity_id;
const configuredOutdoorEntity = outdoorEntity?.value?.trim() || '';
if (configuredOutdoorEntity && !entityIdValid(configuredOutdoorEntity)) {
showFieldError(outdoorEntity, tr('validation.entityId'));
valid = false;
}
const zoneDecimalFields = [
+18 -9
View File
@@ -11,7 +11,8 @@ function renderAccessTokens() {
function metricHaEntities() {
return new Set([
app.settings?.home_assistant?.outdoor_entity_id,
...app.zones.filter(zone => ['home_assistant', 'combined'].includes(zone.sensor_source)).map(zone => zone.ha_entity_id),
...app.zones.map(zone => zone.ha_outdoor_entity_id).filter(Boolean),
...app.zones.filter(zone => ['home_assistant', 'combined'].includes(zone.sensor_source)).map(zoneHaEntityId),
...app.historyData.sensors.map(row => row.entity_id),
].filter(Boolean));
}
@@ -24,15 +25,24 @@ function knownHaEntities() {
return [...new Set([
...Object.keys(app.sensorAliases || {}),
...app.zones.map(zone => zone.ha_entity_id).filter(Boolean),
app.settings?.home_assistant?.default_entity_id,
...app.zones.map(zone => zone.ha_outdoor_entity_id).filter(Boolean),
app.settings?.home_assistant?.outdoor_entity_id,
...(app.flowSharedInputs || []).map(item => item?.config?.entity_id).filter(Boolean),
...app.historyData.sensors.map(row => row.entity_id),
].filter(Boolean))].sort();
}
function renderHaEntitySuggestions() {
const host = $('#haEntitySuggestions'); if (!host) return;
host.innerHTML = knownHaEntities().map(entity => {
const alias = haSensorLabel(entity);
return `<option value="${esc(entity)}" label="${esc(alias === entity ? '' : alias)}"></option>`;
}).join('');
}
function renderSensorAliases() {
const host = $('#sensorAliasList'); if (!host) return;
renderHaEntitySuggestions();
const entities = knownHaEntities();
const metricEntities = metricHaEntities(), flowEntities = flowHaEntities();
host.innerHTML = entities.length ? entities.map(entity => {
@@ -70,8 +80,8 @@ function sharedFlowInputSourceSummary(item) {
if (item.kind === 'outdoor_temperature') return tr('flow.node.outdoorTemperature');
if (item.kind === 'device_temperature') return app.devices.find(value => value.id === c.device_id)?.name || tr('common.noDevice');
if (item.kind === 'zone_temperature') return app.zones.find(value => value.id === c.zone_id)?.name || tr('common.noZone');
if (item.kind === 'ha_state' || item.kind === 'ha_numeric' || item.kind === 'ha_available') return c.entity_id || 'entity_id';
if (item.kind === 'ha_attribute') return `${c.entity_id || 'entity_id'}.${c.attribute || 'attribute'}`;
if (item.kind === 'ha_state' || item.kind === 'ha_numeric' || item.kind === 'ha_available') return c.entity_id ? haSensorLabel(c.entity_id) : 'entity_id';
if (item.kind === 'ha_attribute') return `${c.entity_id ? haSensorLabel(c.entity_id) : 'entity_id'}.${c.attribute || 'attribute'}`;
if (item.kind === 'house_mode') return tr('flow.houseMode');
if (item.kind === 'device_state') return `${app.devices.find(value => value.id === c.device_id)?.name || tr('common.noDevice')} · ${c.field || 'state'}`;
if (item.kind === 'zone_state') return `${app.zones.find(value => value.id === c.zone_id)?.name || tr('common.noZone')} · ${c.field || 'state'}`;
@@ -111,10 +121,10 @@ function renderFlowSharedInputFields(kind, config = {}) {
const c = config || {};
let fields = '';
if (kind === 'constant') fields = `<label><span>${esc(tr('flow.value'))}</span><select data-shared-config="value"><option value="true" ${c.value !== false ? 'selected' : ''}>${esc(tr('common.yes'))}</option><option value="false" ${c.value === false ? 'selected' : ''}>${esc(tr('common.no'))}</option></select></label>`;
else if (kind === 'ha_state') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label>`;
else if (kind === 'ha_numeric') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="sensor.energy_price"></label>`;
else if (kind === 'ha_attribute') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="climate.living_room"></label><label><span>${esc(tr('flow.attribute'))}</span><input data-shared-config="attribute" value="${esc(c.attribute || '')}" placeholder="hvac_action"></label>`;
else if (kind === 'ha_available') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label><p class="field-note">${esc(tr('flow.haAvailableHint'))}</p>`;
else if (kind === 'ha_state') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label>`;
else if (kind === 'ha_numeric') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="sensor.energy_price"></label>`;
else if (kind === 'ha_attribute') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="climate.living_room"></label><label><span>${esc(tr('flow.attribute'))}</span><input data-shared-config="attribute" value="${esc(c.attribute || '')}" placeholder="hvac_action"></label>`;
else if (kind === 'ha_available') fields = `<label><span>entity_id</span><input data-shared-config="entity_id" list="haEntitySuggestions" value="${esc(c.entity_id || '')}" placeholder="binary_sensor.window"></label><p class="field-note">${esc(tr('flow.haAvailableHint'))}</p>`;
else if (kind === 'outdoor_temperature') fields = `<p class="field-note">${esc(tr('flow.sharedInputSourceOnlyHint'))}</p>`;
else if (kind === 'device_temperature') fields = `<label><span>${esc(tr('common.device'))}</span><select data-shared-config="device_id">${sharedFlowOptions(app.devices, c.device_id)}</select></label>`;
else if (kind === 'zone_temperature') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-shared-config="zone_id">${sharedFlowOptions(app.zones, c.zone_id)}</select></label>`;
@@ -334,7 +344,6 @@ function renderHomeAssistantSettings() {
form.ha_url.value = app.settings.home_assistant?.url || '';
form.ha_token.value = '';
form.ha_token.placeholder = app.settings.home_assistant?.token_configured ? tr('settings.haTokenSaved') : tr('settings.haLongLivedToken');
form.ha_entity_id.value = app.settings.home_assistant?.default_entity_id || '';
form.ha_outdoor_entity_id.value = app.settings.home_assistant?.outdoor_entity_id || '';
form.ha_sensor_stale_after_minutes.value = String(Math.max(1, Math.round(Number(app.settings.home_assistant?.sensor_stale_after_seconds || 300) / 60)));
form.ha_allow_invalid_tls.checked = !!app.settings.home_assistant?.allow_invalid_tls;
+2 -4
View File
@@ -143,7 +143,6 @@ function homeAssistantSettingsBodyFromForm(form) {
const raw = Object.fromEntries(new FormData(form));
const body = {
url: raw.ha_url,
default_entity_id: raw.ha_entity_id,
outdoor_entity_id: raw.ha_outdoor_entity_id,
sensor_stale_after_seconds: Math.max(60, Math.min(86400, Math.round(Number(raw.ha_sensor_stale_after_minutes || 5) * 60))),
allow_invalid_tls: form.ha_allow_invalid_tls.checked,
@@ -289,10 +288,9 @@ $('#haTest').addEventListener('click', async event => {
button.textContent = tr('settings.testingHa');
try {
await saveHomeAssistantSettings(form, false);
const entity = form.ha_entity_id.value || form.ha_outdoor_entity_id.value || null;
const result = await api('/api/integrations/home-assistant/test', { method: 'POST', body: { entity_id: entity } });
await api('/api/integrations/home-assistant/test', { method: 'POST' });
markFormClean(form);
toast(tr('toast.haTemperature', { temperature: result.temperature_c.toFixed(1) }));
toast(tr('toast.haConnected'));
} catch (error) { presentFormError(form, error); }
finally { button.disabled = false; button.textContent = idle; }
});