v0.14.4
This commit is contained in:
Generated
+1
-1
@@ -700,7 +700,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.14.2"
|
||||
version = "0.14.4"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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 być dostę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.",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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)]
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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('');
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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; }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user