v0.14.2
This commit is contained in:
Generated
+1
-1
@@ -700,7 +700,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gree-controller"
|
||||
version = "0.14.1"
|
||||
version = "0.14.2"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gree-controller"
|
||||
version = "0.14.1"
|
||||
version = "0.14.2"
|
||||
edition = "2021"
|
||||
authors = ["GREE Controller contributors"]
|
||||
description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support"
|
||||
|
||||
+36
-36
@@ -1,14 +1,14 @@
|
||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
1baa24e6934a05f48ea7f289ce5f8804fc0981236a2b6c468539a052c0d4e583 ./Cargo.lock
|
||||
f3204447421f334110e1a626ec29fdd8ee88bb2b9d1e9e1a391c2720c9af7de9 ./Cargo.toml
|
||||
59fe6d7de7f780481d0a7f614d4325f905a8aee8d971717566fc8889340223b3 ./Cargo.lock
|
||||
97b67fd716185b18bb12d9170b502f81c6ffaf099816c893305c9f837048cb1f ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
a4f7dc180e422d6de9d78b1235206ebc3b0436d6041730df8ca065532c36a939 ./README.md
|
||||
b27097a57e62a3096b133899dea455a68aa04c0fa428f6228d08615e1b11ca29 ./SOURCE_MANIFEST.sha256
|
||||
de56b105caea31afd80b4051912329a0c1c03233979fd710732cb606acae1dbd ./README.md
|
||||
c45e67ac73f7606e0b4f90fa05bd9c3a20dc07441ac04e8bc68235ef558143cc ./SOURCE_MANIFEST.sha256
|
||||
bb549919b96b0413152dd905aec8640bb53301f07ec7d0b1be48ccbd37936fef ./build.rs
|
||||
50a46eac5dfcdc80c5db068f0eb1ed7d696a2b6070e2ddff808f2bb2a33a913b ./docs/API.md
|
||||
2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md
|
||||
919d54a6825385fdb4e9b8f07db756640d512b2c21d2d33f407f850680285ed0 ./docs/openapi.json
|
||||
0eb39057762c51718e7e7ba220ae1e3a729409c3d91736afa3baa88c64d3a8d6 ./docs/API.md
|
||||
ff213ad348e7f8b5cae7fe4adcad84d72db693765d424f270faa1acca9c23ce7 ./docs/FLOW.md
|
||||
52181be4d4a161710bbb5e7cde1b3061976e41691d9e10bf99f0c224c65732d1 ./docs/openapi.json
|
||||
c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
|
||||
a18d2460340264d34c267fa704ee51a42e59bcc1783ee4c0dbb2a8ef42fa0f16 ./ha-addon/Dockerfile
|
||||
0417c88a990c72aecf071fba64d5d9da65dcda4d396cd7565477f7a4532c8c5e ./ha-addon/README.md
|
||||
@@ -16,13 +16,13 @@ cd7fd4b937a1d6138699f7effd184e3c077912b881e9c73c37b90bbafba68477 ./ha-addon/bui
|
||||
fd041032f353ceb0a2b02005f5a78d74a460b12e4e78de13dcb60129859f8c4e ./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
|
||||
021a235617852c3adc4990936c3daf497309ea9a18574223805ae7496b1366c3 ./ha-addon/home-assistant/custom_components/gree_controller/climate.py
|
||||
0c008329aad15f5af6328380c1afe1f49db186b37b3d1cafec4b6f4088386e8e ./ha-addon/home-assistant/custom_components/gree_controller/climate.py
|
||||
5e4aef2143e81bedb5a15dd4be5c71b64a3ab448ec6d33a20851edd098e5f529 ./ha-addon/home-assistant/custom_components/gree_controller/config_flow.py
|
||||
b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/home-assistant/custom_components/gree_controller/const.py
|
||||
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
|
||||
5a7a8cf9545c9346d9bcc92e16265fa2ef63567fa28abe153a2e361cc279c082 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
|
||||
25c5ffb84460bf81f0c54abfc912e5bfd85535d3558df7a10e1de9541dd2afa3 ./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
|
||||
131509e7dfefcca4ad61a3a060bc0a33ed2f731141496ba99a3cb81a563399b1 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
e561712958ce5ef1068989af0515687358adc69b1d92af29d6b576d46a182859 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
3c06bd11671452ea6472afd5543fb91a39aba73a1d2d95c7762c04f94ab64030 ./ha-addon/repository/gree-controller/DOCS.md
|
||||
c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md
|
||||
09e38d1d821a6c318152e5a023954310e9f361985cfddc456abfcc0b88d79c62 ./ha-addon/repository/gree-controller/config.yaml
|
||||
a3a74b2aaa20fd39a050f8f360eec543d70b3fd43271617a60a0b95fd0cdf2ca ./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
|
||||
301df7a84ce546db1c2e25da841aa0fc9696435d7bb4db358e3fb082a723afd9 ./lang/en.json
|
||||
c01e081782251122fec4fb50500a5f3ee4d865a2213af89cc77f3f3661606dbd ./lang/pl.json
|
||||
4bbfa7cfb7644b6949aa8bc212154fce4dbf1f8bc5c7c56863a30fa63a0efd80 ./lang/en.json
|
||||
bd35f928fbca252a90057a04dc1d363c85bd844b29076238e00751d8ecf196cc ./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
|
||||
3461b89eff40737f5f250b50bcccadb882bb0b5afbd96542d9d67184142191f8 ./scripts/api_dev_test.py
|
||||
506942d9c96b586b7f265742421fcea45c8d7e86824ad8934147a4388873c3cc ./scripts/api_dev_test.py
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
|
||||
@@ -102,12 +102,12 @@ e89b3503b838670f3213e696395024e74d91c718bc67a4cb295077e944ea4c6c ./src/api.rs
|
||||
3dfd1e053cbfa0d684dd88cf99b244a96a36e2ff23d2a0715b8327ab62258254 ./src/api/assets.rs
|
||||
05c255919f7b3d81188fc5f1410be6a0037471dfc44e93bffb009f29a1e6a3fc ./src/api/auth.rs
|
||||
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
|
||||
be865a0be2167131a3778d7ceb825d9a65991a7c063fffadbe7188eac459d078 ./src/api/configuration.rs
|
||||
a9e8b74c9ba3ae723d36d2d87c0b1a72138f9ad2fed8e62dc5ad442278a1986d ./src/api/configuration.rs
|
||||
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
|
||||
32ade46ec45d6faf7852242b9f5ba1be6eacbacbeb33d2deabfab59bf1610ce0 ./src/api/device_groups.rs
|
||||
fcbb3da0538aab451f71810f829cc248d6bb67e586dc418d71a572980583823d ./src/api/devices.rs
|
||||
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
|
||||
44b74965f3256c2972d6b3cd384a63c68e3b422f414a0902c085bc92e17369bf ./src/api/flows.rs
|
||||
d59b080b392b574ec94276c7b485b455d0d1abacc7de2ef29dcbfcf7d86dbdb0 ./src/api/flows.rs
|
||||
2cfee7029f065e81628a8942e3dad00a03c3a5cf4f8492d07f12b75d05c07359 ./src/api/gree_cloud.rs
|
||||
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
|
||||
63733cf1fd70b9c2cb1bde3c9304365e8940af4a7524bfc82b47444458a56f4a ./src/api/history.rs
|
||||
@@ -118,7 +118,7 @@ e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/hous
|
||||
744ce18aa9e0227c6cdfb6980063d0d738d74c5865d45c20fd20026d4742bb3e ./src/api/public_settings.rs
|
||||
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs
|
||||
e69df8a5f612cd37d25fbf9f8e824c5a7d655ac5a6376ec3b31addf1fe8597e2 ./src/api/settings.rs
|
||||
264f884e2fdef24303d441ce9cfe11190ed8f2bba8ca5782c1d13c2ee30e6a7f ./src/api/system.rs
|
||||
1c1fe39aec55d10608ca3db83827855b07249cd1a81ae054eb1ee6464514092b ./src/api/system.rs
|
||||
9118a5a3a381b010e22d646498ff56d2e05f51656789682cc4371b09c384aa98 ./src/api/websocket.rs
|
||||
455c7238c9788d378e909a4fbc94d5390b67edd92e127f48b4b913ddcbe82fb0 ./src/api/zones.rs
|
||||
f4feaff281163eda543bf094d0fe9158dc6662be5f76f39c81570da7ed0355e7 ./src/config.rs
|
||||
@@ -134,14 +134,14 @@ bb5048ee60053f39ce3c1203dc1f24c7047310538d2357f105a052c016d21ec4 ./src/db/flows
|
||||
d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs
|
||||
a76938b2e49719b97f8abf40a5cce03df6740ce2e8ac138b179c761619b32ce6 ./src/db/tests.rs
|
||||
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
|
||||
3c97e04e93d14f37d39ee9f8f53257101517a3d90a0dfeba81ca3dcd5e39ff31 ./src/engine.rs
|
||||
6046bdbc9c02ac74f47068229c59af69abc8bfa46cfeff6fe7ba573b493e128c ./src/engine/automations.rs
|
||||
a66b4c2f4f62bd4a981f57ad574c797b73d8400f3492f8ff69126f74b5010899 ./src/engine.rs
|
||||
ed5c9a0c61c6e597acb33f58593ebe02b562ddc68fa61271e1c1cd436c1003d3 ./src/engine/automations.rs
|
||||
0bf7fe7d28b4749e53ec25d71effbb78bc8f70c716714f838a4a2d8f20627d63 ./src/engine/commands.rs
|
||||
925135985f640ba9ae0a6109d908187b659341a91451e91d27e533adfd610b76 ./src/engine/control_plan.rs
|
||||
7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs
|
||||
9a51cc46b8befb992d7f7dd3dbe5025cd739755a684ed36013be78eaaf1a1743 ./src/engine/energy.rs
|
||||
67ddb482ad08608e799a88faa6183d9bb65f62ca8ac138c6314cab18bc033033 ./src/engine/energy.rs
|
||||
e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs
|
||||
0e78e8d1c31503b313926af025281714a6e22a23f46af081b3da8e94a2d7315a ./src/engine/history.rs
|
||||
9f650203959acebc72f3ee87faaeebe4c5c8beee7d77d2039d56fa2c0277aab4 ./src/engine/history.rs
|
||||
41b7045877771b7c352328998cb45ce2433e7f9631fad3eb1a882ab4897dc31f ./src/engine/local_thermostat.rs
|
||||
99527adab9fea1caede9ebb24faa6a63d5cef4810c64c06306b571cf5cbbb0da ./src/engine/ownership.rs
|
||||
1a29b7fd8de77f0a5e5964ca05e41cc5ea78855c1ab961750d40461faec305e5 ./src/engine/polling.rs
|
||||
@@ -150,9 +150,9 @@ c1e8de5392681ba1255e3e2d871a6ee860fb8ca987a0a8f973d9ded3977a68fb ./src/engine/s
|
||||
b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs
|
||||
682c8b3956c657b655746cbd19ee97ee2f4cf953bc9de67d6d70a1b14f3dd746 ./src/engine/temperature.rs
|
||||
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
|
||||
338161bd94c868be6155efabdc63e81e9a572d559cc00493d6c69f15cc1612a3 ./src/engine/tests.rs
|
||||
47330f0d8ff170ea91ba37d019c0d5577151c87f0474bcc2a287850415f6b953 ./src/engine/tests.rs
|
||||
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs
|
||||
6db07dc9c3003c97b80d3955761329ffea9876279e7afc0644bfba8878e41209 ./src/engine/zone_control.rs
|
||||
28571e25e539fb1fe5e77e8bd003caf2da9e84fc34d156fd5f3e65e805798ca0 ./src/engine/zone_control.rs
|
||||
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
|
||||
ea553a1d8b44c74b300d7c4a76dcd3b370524eb57766730a1376d1ab3079913b ./src/home_assistant.rs
|
||||
b43d95ac42f5edc1a38323e47626b6c0ec6cbbda3c30ad6f245d23865cd1993e ./src/influxdb.rs
|
||||
@@ -199,25 +199,25 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
6cd0fa21dcc1b5170c4375d7fb70b72fc753f6ac80fbd1f6a91ba0e52ccca7e4 ./web/index.html
|
||||
54350093b25e335f7e2bcf99fd0e17ba6df9035a0711c02bcbe0c4dd612c62e5 ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
|
||||
dfea965a2093f9f85744b50993c867118afc7a65d2c5d3abd975d4b6f4acc6f2 ./web/js/bootstrap.js
|
||||
761ad87ed20a819d8d87bc260bcd7f5b39f19582c346a69c8b717c94e7e94302 ./web/js/charts.js
|
||||
e6c459f92f18d72f11ffaead5caede0b9c8e6b6a1c9cf1a6e97e6b07d5425ab5 ./web/js/core.js
|
||||
a183492c9cf91d1318675f242c57ed30c11661ffc8a5a52f03e5aaeea060ef24 ./web/js/bootstrap.js
|
||||
e987328623938637135c8a8d3952043fe7ae5d8568eff12bf4d95a601409f1df ./web/js/charts.js
|
||||
3c61d86420d1f3765c9ccc184553425132fc432fd60d93763da4669d113b449b ./web/js/core.js
|
||||
d880ffdac6aa07cd10d79fbacf12f87eb4c69b48a80564bbfdd0248abe8884d5 ./web/js/dashboard.js
|
||||
83189a47d114e7b911c9cbf368bf80232a7a481d416297db5fc451c12bb710c7 ./web/js/entities.js
|
||||
1ff265beab8eb15ced854b5da260fd3952add3acc6b290ea73c76b1a54853165 ./web/js/events.js
|
||||
2e822a00b5a6941b6a251eaf2c54ca95a53399b5d1056f3d8240e11f82d6065f ./web/js/flows.js
|
||||
ed31601df41cc82032709f960a2b4061966abd58cde456ef7dda05e42eb91432 ./web/js/entities.js
|
||||
424b52fa05cc6f45ab3bcac0f5f1fd0ac2102d840334fc3d762fd208816565a8 ./web/js/events.js
|
||||
8c156909d2d2e59b7d7dc969b9520c978141a448a6cc5aae8fe74a8dc124f2bc ./web/js/flows.js
|
||||
9e2209d41df7624c29741f03f12ed8e83b47cee2891c5a80cea8ca1bdacf1d5a ./web/js/forms.js
|
||||
6d2c2fc497f0ea4e3ef4a35e6518c9e4be680ace9100ed09a327f9dbe4261d0b ./web/js/history.js
|
||||
688390149f023b4f7e8701ca30ea5cb56c27692dc5083344739411405e320bb1 ./web/js/history.js
|
||||
0dc879c0056251618633c9efd188fc1593eeae6965ad1bc993132dea6e171b44 ./web/js/main.js
|
||||
341c4bc441782bf60bd92c899154ca565ac1ba884a6f3a1ea89aae58f5d0322f ./web/js/navigation.js
|
||||
d105fb2a3b4000a9f22ed23c7b030c1018fee4d2a78dba3ad96b3d9d95689f2a ./web/js/realtime.js
|
||||
419e0859d1553972f670bb1719be6c0aca14a4ab450041d0017abd41a094b1da ./web/js/navigation.js
|
||||
e3d146a683b8beb332c752e533826160c2cf2236bc269ced7a4dd1249871027b ./web/js/realtime.js
|
||||
abdedaca20ecad50ae2b5c7f6081c80a142112bd867cda19949d5253e609f1b4 ./web/js/router.js
|
||||
3426ec31011173172199bff7deacbef1d5eb0121012b5ea0e53b7a72a674a853 ./web/js/settings-ui.js
|
||||
13806217b2312dff3bd6cc2b16f75d9bdffc7a587215ae9f862706cd7699673f ./web/js/settings.js
|
||||
55c163fce9520979d380739546ef3036186297466d5fc3ac8cb0680408fb444e ./web/js/settings-ui.js
|
||||
29e6ca03eb62c6158de9ab0b9e74100129478794ae5c697321e217fb1b69ff23 ./web/js/settings.js
|
||||
3815fea7ae1694568c2f249bc8460e0563ced6bde89214f74f21b41fe8cad9a6 ./web/lang-init.js
|
||||
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
|
||||
10a47e3645316103b8ad85a8635ec6305da9717226568e76db58e2d77e894e8f ./web/styles.css
|
||||
3274600badfc59219db1d76e7767d7448e4cf1a9aedee94e90435c906c8269b3 ./web/styles.css
|
||||
fefdf12cbbb0a5d8a2cb89275cae28455aa42aaf179542305cc8f5e0f4825898 ./web/sw.js
|
||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
||||
|
||||
@@ -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.1**
|
||||
**Version: 0.14.2**
|
||||
|
||||
Version 0.14.1 builds on the GREE Cloud and energy support from 0.14.0 with split/multisplit installation groups, shared energy and outdoor-temperature sources, multi-series energy comparison, cleaner Cloud status/alerts and safer MQTT cleanup after device removal. Local and Cloud entries remain deliberately independent, even when they represent the same physical MAC.
|
||||
Version 0.14.2 adds separate vertical and horizontal swing control across manual unit control, thermostat views, legacy automations, Visual Flow and Home Assistant zone thermostats. It keeps the split/multisplit energy and outdoor-temperature improvements from 0.14.1, while preserving independent Local and Cloud device entries.
|
||||
|
||||
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
|
||||
|
||||
|
||||
+35
-35
@@ -1,13 +1,13 @@
|
||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
1baa24e6934a05f48ea7f289ce5f8804fc0981236a2b6c468539a052c0d4e583 ./Cargo.lock
|
||||
f3204447421f334110e1a626ec29fdd8ee88bb2b9d1e9e1a391c2720c9af7de9 ./Cargo.toml
|
||||
59fe6d7de7f780481d0a7f614d4325f905a8aee8d971717566fc8889340223b3 ./Cargo.lock
|
||||
97b67fd716185b18bb12d9170b502f81c6ffaf099816c893305c9f837048cb1f ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
a4f7dc180e422d6de9d78b1235206ebc3b0436d6041730df8ca065532c36a939 ./README.md
|
||||
de56b105caea31afd80b4051912329a0c1c03233979fd710732cb606acae1dbd ./README.md
|
||||
bb549919b96b0413152dd905aec8640bb53301f07ec7d0b1be48ccbd37936fef ./build.rs
|
||||
50a46eac5dfcdc80c5db068f0eb1ed7d696a2b6070e2ddff808f2bb2a33a913b ./docs/API.md
|
||||
2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md
|
||||
919d54a6825385fdb4e9b8f07db756640d512b2c21d2d33f407f850680285ed0 ./docs/openapi.json
|
||||
0eb39057762c51718e7e7ba220ae1e3a729409c3d91736afa3baa88c64d3a8d6 ./docs/API.md
|
||||
ff213ad348e7f8b5cae7fe4adcad84d72db693765d424f270faa1acca9c23ce7 ./docs/FLOW.md
|
||||
52181be4d4a161710bbb5e7cde1b3061976e41691d9e10bf99f0c224c65732d1 ./docs/openapi.json
|
||||
c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
|
||||
a18d2460340264d34c267fa704ee51a42e59bcc1783ee4c0dbb2a8ef42fa0f16 ./ha-addon/Dockerfile
|
||||
0417c88a990c72aecf071fba64d5d9da65dcda4d396cd7565477f7a4532c8c5e ./ha-addon/README.md
|
||||
@@ -15,13 +15,13 @@ cd7fd4b937a1d6138699f7effd184e3c077912b881e9c73c37b90bbafba68477 ./ha-addon/bui
|
||||
fd041032f353ceb0a2b02005f5a78d74a460b12e4e78de13dcb60129859f8c4e ./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
|
||||
021a235617852c3adc4990936c3daf497309ea9a18574223805ae7496b1366c3 ./ha-addon/home-assistant/custom_components/gree_controller/climate.py
|
||||
0c008329aad15f5af6328380c1afe1f49db186b37b3d1cafec4b6f4088386e8e ./ha-addon/home-assistant/custom_components/gree_controller/climate.py
|
||||
5e4aef2143e81bedb5a15dd4be5c71b64a3ab448ec6d33a20851edd098e5f529 ./ha-addon/home-assistant/custom_components/gree_controller/config_flow.py
|
||||
b7f0873109c52be9d7f09bea3dffc416103c50085e1f0680d11661a969479898 ./ha-addon/home-assistant/custom_components/gree_controller/const.py
|
||||
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
|
||||
5a7a8cf9545c9346d9bcc92e16265fa2ef63567fa28abe153a2e361cc279c082 ./ha-addon/home-assistant/custom_components/gree_controller/manifest.json
|
||||
25c5ffb84460bf81f0c54abfc912e5bfd85535d3558df7a10e1de9541dd2afa3 ./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
|
||||
131509e7dfefcca4ad61a3a060bc0a33ed2f731141496ba99a3cb81a563399b1 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
e561712958ce5ef1068989af0515687358adc69b1d92af29d6b576d46a182859 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
3c06bd11671452ea6472afd5543fb91a39aba73a1d2d95c7762c04f94ab64030 ./ha-addon/repository/gree-controller/DOCS.md
|
||||
c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md
|
||||
09e38d1d821a6c318152e5a023954310e9f361985cfddc456abfcc0b88d79c62 ./ha-addon/repository/gree-controller/config.yaml
|
||||
a3a74b2aaa20fd39a050f8f360eec543d70b3fd43271617a60a0b95fd0cdf2ca ./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
|
||||
301df7a84ce546db1c2e25da841aa0fc9696435d7bb4db358e3fb082a723afd9 ./lang/en.json
|
||||
c01e081782251122fec4fb50500a5f3ee4d865a2213af89cc77f3f3661606dbd ./lang/pl.json
|
||||
4bbfa7cfb7644b6949aa8bc212154fce4dbf1f8bc5c7c56863a30fa63a0efd80 ./lang/en.json
|
||||
bd35f928fbca252a90057a04dc1d363c85bd844b29076238e00751d8ecf196cc ./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
|
||||
3461b89eff40737f5f250b50bcccadb882bb0b5afbd96542d9d67184142191f8 ./scripts/api_dev_test.py
|
||||
506942d9c96b586b7f265742421fcea45c8d7e86824ad8934147a4388873c3cc ./scripts/api_dev_test.py
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
|
||||
@@ -101,12 +101,12 @@ e89b3503b838670f3213e696395024e74d91c718bc67a4cb295077e944ea4c6c ./src/api.rs
|
||||
3dfd1e053cbfa0d684dd88cf99b244a96a36e2ff23d2a0715b8327ab62258254 ./src/api/assets.rs
|
||||
05c255919f7b3d81188fc5f1410be6a0037471dfc44e93bffb009f29a1e6a3fc ./src/api/auth.rs
|
||||
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
|
||||
be865a0be2167131a3778d7ceb825d9a65991a7c063fffadbe7188eac459d078 ./src/api/configuration.rs
|
||||
a9e8b74c9ba3ae723d36d2d87c0b1a72138f9ad2fed8e62dc5ad442278a1986d ./src/api/configuration.rs
|
||||
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
|
||||
32ade46ec45d6faf7852242b9f5ba1be6eacbacbeb33d2deabfab59bf1610ce0 ./src/api/device_groups.rs
|
||||
fcbb3da0538aab451f71810f829cc248d6bb67e586dc418d71a572980583823d ./src/api/devices.rs
|
||||
4b5b784670ae761a6848b9a33fe3fb391bebd39d65e87936907428711e96ca44 ./src/api/events.rs
|
||||
44b74965f3256c2972d6b3cd384a63c68e3b422f414a0902c085bc92e17369bf ./src/api/flows.rs
|
||||
d59b080b392b574ec94276c7b485b455d0d1abacc7de2ef29dcbfcf7d86dbdb0 ./src/api/flows.rs
|
||||
2cfee7029f065e81628a8942e3dad00a03c3a5cf4f8492d07f12b75d05c07359 ./src/api/gree_cloud.rs
|
||||
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
|
||||
63733cf1fd70b9c2cb1bde3c9304365e8940af4a7524bfc82b47444458a56f4a ./src/api/history.rs
|
||||
@@ -117,7 +117,7 @@ e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/hous
|
||||
744ce18aa9e0227c6cdfb6980063d0d738d74c5865d45c20fd20026d4742bb3e ./src/api/public_settings.rs
|
||||
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs
|
||||
e69df8a5f612cd37d25fbf9f8e824c5a7d655ac5a6376ec3b31addf1fe8597e2 ./src/api/settings.rs
|
||||
264f884e2fdef24303d441ce9cfe11190ed8f2bba8ca5782c1d13c2ee30e6a7f ./src/api/system.rs
|
||||
1c1fe39aec55d10608ca3db83827855b07249cd1a81ae054eb1ee6464514092b ./src/api/system.rs
|
||||
9118a5a3a381b010e22d646498ff56d2e05f51656789682cc4371b09c384aa98 ./src/api/websocket.rs
|
||||
455c7238c9788d378e909a4fbc94d5390b67edd92e127f48b4b913ddcbe82fb0 ./src/api/zones.rs
|
||||
f4feaff281163eda543bf094d0fe9158dc6662be5f76f39c81570da7ed0355e7 ./src/config.rs
|
||||
@@ -133,14 +133,14 @@ bb5048ee60053f39ce3c1203dc1f24c7047310538d2357f105a052c016d21ec4 ./src/db/flows
|
||||
d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs
|
||||
a76938b2e49719b97f8abf40a5cce03df6740ce2e8ac138b179c761619b32ce6 ./src/db/tests.rs
|
||||
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
|
||||
3c97e04e93d14f37d39ee9f8f53257101517a3d90a0dfeba81ca3dcd5e39ff31 ./src/engine.rs
|
||||
6046bdbc9c02ac74f47068229c59af69abc8bfa46cfeff6fe7ba573b493e128c ./src/engine/automations.rs
|
||||
a66b4c2f4f62bd4a981f57ad574c797b73d8400f3492f8ff69126f74b5010899 ./src/engine.rs
|
||||
ed5c9a0c61c6e597acb33f58593ebe02b562ddc68fa61271e1c1cd436c1003d3 ./src/engine/automations.rs
|
||||
0bf7fe7d28b4749e53ec25d71effbb78bc8f70c716714f838a4a2d8f20627d63 ./src/engine/commands.rs
|
||||
925135985f640ba9ae0a6109d908187b659341a91451e91d27e533adfd610b76 ./src/engine/control_plan.rs
|
||||
7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs
|
||||
9a51cc46b8befb992d7f7dd3dbe5025cd739755a684ed36013be78eaaf1a1743 ./src/engine/energy.rs
|
||||
67ddb482ad08608e799a88faa6183d9bb65f62ca8ac138c6314cab18bc033033 ./src/engine/energy.rs
|
||||
e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs
|
||||
0e78e8d1c31503b313926af025281714a6e22a23f46af081b3da8e94a2d7315a ./src/engine/history.rs
|
||||
9f650203959acebc72f3ee87faaeebe4c5c8beee7d77d2039d56fa2c0277aab4 ./src/engine/history.rs
|
||||
41b7045877771b7c352328998cb45ce2433e7f9631fad3eb1a882ab4897dc31f ./src/engine/local_thermostat.rs
|
||||
99527adab9fea1caede9ebb24faa6a63d5cef4810c64c06306b571cf5cbbb0da ./src/engine/ownership.rs
|
||||
1a29b7fd8de77f0a5e5964ca05e41cc5ea78855c1ab961750d40461faec305e5 ./src/engine/polling.rs
|
||||
@@ -149,9 +149,9 @@ c1e8de5392681ba1255e3e2d871a6ee860fb8ca987a0a8f973d9ded3977a68fb ./src/engine/s
|
||||
b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs
|
||||
682c8b3956c657b655746cbd19ee97ee2f4cf953bc9de67d6d70a1b14f3dd746 ./src/engine/temperature.rs
|
||||
833b72907375e75c3f42b3921ad70a8e298f5a93743bf0d0c19059aa272462d1 ./src/engine/temporary_thermostat.rs
|
||||
338161bd94c868be6155efabdc63e81e9a572d559cc00493d6c69f15cc1612a3 ./src/engine/tests.rs
|
||||
47330f0d8ff170ea91ba37d019c0d5577151c87f0474bcc2a287850415f6b953 ./src/engine/tests.rs
|
||||
c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/zone_actions.rs
|
||||
6db07dc9c3003c97b80d3955761329ffea9876279e7afc0644bfba8878e41209 ./src/engine/zone_control.rs
|
||||
28571e25e539fb1fe5e77e8bd003caf2da9e84fc34d156fd5f3e65e805798ca0 ./src/engine/zone_control.rs
|
||||
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
|
||||
ea553a1d8b44c74b300d7c4a76dcd3b370524eb57766730a1376d1ab3079913b ./src/home_assistant.rs
|
||||
b43d95ac42f5edc1a38323e47626b6c0ec6cbbda3c30ad6f245d23865cd1993e ./src/influxdb.rs
|
||||
@@ -198,25 +198,25 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
6cd0fa21dcc1b5170c4375d7fb70b72fc753f6ac80fbd1f6a91ba0e52ccca7e4 ./web/index.html
|
||||
54350093b25e335f7e2bcf99fd0e17ba6df9035a0711c02bcbe0c4dd612c62e5 ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
|
||||
dfea965a2093f9f85744b50993c867118afc7a65d2c5d3abd975d4b6f4acc6f2 ./web/js/bootstrap.js
|
||||
761ad87ed20a819d8d87bc260bcd7f5b39f19582c346a69c8b717c94e7e94302 ./web/js/charts.js
|
||||
e6c459f92f18d72f11ffaead5caede0b9c8e6b6a1c9cf1a6e97e6b07d5425ab5 ./web/js/core.js
|
||||
a183492c9cf91d1318675f242c57ed30c11661ffc8a5a52f03e5aaeea060ef24 ./web/js/bootstrap.js
|
||||
e987328623938637135c8a8d3952043fe7ae5d8568eff12bf4d95a601409f1df ./web/js/charts.js
|
||||
3c61d86420d1f3765c9ccc184553425132fc432fd60d93763da4669d113b449b ./web/js/core.js
|
||||
d880ffdac6aa07cd10d79fbacf12f87eb4c69b48a80564bbfdd0248abe8884d5 ./web/js/dashboard.js
|
||||
83189a47d114e7b911c9cbf368bf80232a7a481d416297db5fc451c12bb710c7 ./web/js/entities.js
|
||||
1ff265beab8eb15ced854b5da260fd3952add3acc6b290ea73c76b1a54853165 ./web/js/events.js
|
||||
2e822a00b5a6941b6a251eaf2c54ca95a53399b5d1056f3d8240e11f82d6065f ./web/js/flows.js
|
||||
ed31601df41cc82032709f960a2b4061966abd58cde456ef7dda05e42eb91432 ./web/js/entities.js
|
||||
424b52fa05cc6f45ab3bcac0f5f1fd0ac2102d840334fc3d762fd208816565a8 ./web/js/events.js
|
||||
8c156909d2d2e59b7d7dc969b9520c978141a448a6cc5aae8fe74a8dc124f2bc ./web/js/flows.js
|
||||
9e2209d41df7624c29741f03f12ed8e83b47cee2891c5a80cea8ca1bdacf1d5a ./web/js/forms.js
|
||||
6d2c2fc497f0ea4e3ef4a35e6518c9e4be680ace9100ed09a327f9dbe4261d0b ./web/js/history.js
|
||||
688390149f023b4f7e8701ca30ea5cb56c27692dc5083344739411405e320bb1 ./web/js/history.js
|
||||
0dc879c0056251618633c9efd188fc1593eeae6965ad1bc993132dea6e171b44 ./web/js/main.js
|
||||
341c4bc441782bf60bd92c899154ca565ac1ba884a6f3a1ea89aae58f5d0322f ./web/js/navigation.js
|
||||
d105fb2a3b4000a9f22ed23c7b030c1018fee4d2a78dba3ad96b3d9d95689f2a ./web/js/realtime.js
|
||||
419e0859d1553972f670bb1719be6c0aca14a4ab450041d0017abd41a094b1da ./web/js/navigation.js
|
||||
e3d146a683b8beb332c752e533826160c2cf2236bc269ced7a4dd1249871027b ./web/js/realtime.js
|
||||
abdedaca20ecad50ae2b5c7f6081c80a142112bd867cda19949d5253e609f1b4 ./web/js/router.js
|
||||
3426ec31011173172199bff7deacbef1d5eb0121012b5ea0e53b7a72a674a853 ./web/js/settings-ui.js
|
||||
13806217b2312dff3bd6cc2b16f75d9bdffc7a587215ae9f862706cd7699673f ./web/js/settings.js
|
||||
55c163fce9520979d380739546ef3036186297466d5fc3ac8cb0680408fb444e ./web/js/settings-ui.js
|
||||
29e6ca03eb62c6158de9ab0b9e74100129478794ae5c697321e217fb1b69ff23 ./web/js/settings.js
|
||||
3815fea7ae1694568c2f249bc8460e0563ced6bde89214f74f21b41fe8cad9a6 ./web/lang-init.js
|
||||
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
|
||||
10a47e3645316103b8ad85a8635ec6305da9717226568e76db58e2d77e894e8f ./web/styles.css
|
||||
3274600badfc59219db1d76e7767d7448e4cf1a9aedee94e90435c906c8269b3 ./web/styles.css
|
||||
fefdf12cbbb0a5d8a2cb89275cae28455aa42aaf179542305cc8f5e0f4825898 ./web/sw.js
|
||||
d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js
|
||||
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
# GREE Controller API reference
|
||||
|
||||
HTTP and WebSocket API for GREE Controller **0.14.1**.
|
||||
HTTP and WebSocket API for GREE Controller **0.14.2**.
|
||||
|
||||
[← Main documentation](../README.md)
|
||||
|
||||
@@ -232,7 +232,7 @@ Response:
|
||||
{
|
||||
"status": "ok",
|
||||
"name": "gree-controller",
|
||||
"version": "0.14.1",
|
||||
"version": "0.14.2",
|
||||
"uptime_seconds": 1234,
|
||||
"control_ready": true,
|
||||
"time": "2026-08-30T06:54:00Z"
|
||||
@@ -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.1",
|
||||
"version": "0.14.2",
|
||||
"uptime_seconds": 1234,
|
||||
"auth_required": false,
|
||||
"control_ready": true,
|
||||
@@ -1654,6 +1654,12 @@ Additional Flow condition blocks are `house_mode`, `device_state`, `zone_state`,
|
||||
|
||||
|
||||
|
||||
## Swing control across manual, thermostat and automation surfaces — 0.14.2
|
||||
|
||||
Local device control exposes vertical (`swing_vertical`) and horizontal (`swing_horizontal`) swing independently. Thermostat zone UI and the bundled Home Assistant zone climate entity expose the same auxiliary controls when the assigned device reports support; changing swing does not hand thermostat ownership to manual control.
|
||||
|
||||
Legacy direct-device automations accept optional `swing_vertical` and `swing_horizontal` action fields. Visual Flow exposes both axes in `device_action` and `zone_thermostat` actions and in device-state conditions. A `zone_thermostat` Flow using swing compiles to an automation rather than a native schedule so the auxiliary command is preserved. Swing remains outside the thermostat's continuously regulated fan/temperature outputs.
|
||||
|
||||
## Split/multisplit installations and energy comparison — 0.14.1
|
||||
|
||||
Installation configuration endpoints:
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ Flow is an authoring/orchestration layer. It does not replace the thermostat, sc
|
||||
|
||||
## Compilation
|
||||
|
||||
- `weekday + time_range -> zone_thermostat` with schedule-compatible settings compiles to a native `Schedule`.
|
||||
- `weekday + time_range -> zone_thermostat` with schedule-compatible settings compiles to a native `Schedule`; selecting vertical or horizontal swing makes the action compile as an `Automation` so the swing command is not lost.
|
||||
- Richer DAGs compile to native `Automation` rows with `trigger_kind = "flow"`.
|
||||
- Generated rows keep `flow_id` and `flow_node_id`, use stable `flow-<stable-unique-id>` names, and are read-only in the legacy editors.
|
||||
- The Flow source graph plus all of its generated schedule/automation rows are replaced in one SQLite transaction.
|
||||
@@ -14,7 +14,7 @@ Flow is an authoring/orchestration layer. It does not replace the thermostat, sc
|
||||
|
||||
## Existing control domains remain authoritative
|
||||
|
||||
- `zone_thermostat` writes the existing persistent Zone intent and wakes the normal thermostat cycle.
|
||||
- `zone_thermostat` writes the existing persistent Zone intent and wakes the normal thermostat cycle. It can also issue one-shot vertical/horizontal swing commands to the assigned unit; these auxiliary fields do not take over thermostat ownership.
|
||||
- `group_action` calls the existing `control_group` path, including custom group targets.
|
||||
- `device_action` uses the existing `DeviceCommand` / automatic-device path.
|
||||
- Thermostat hysteresis, compressor protection, schedule hand-back, manual/local ownership and Temporary Quick Thermostat keep their existing priority.
|
||||
|
||||
+3
-3
@@ -2,9 +2,9 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "GREE Controller API",
|
||||
"version": "0.14.1",
|
||||
"version": "0.14.2",
|
||||
"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.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.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"
|
||||
}
|
||||
@@ -5391,7 +5391,7 @@
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"example": "0.14.1"
|
||||
"example": "0.14.2"
|
||||
},
|
||||
"uptime_seconds": {
|
||||
"type": "integer",
|
||||
|
||||
@@ -266,12 +266,8 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl
|
||||
_attr_target_temperature_step = 0.5
|
||||
_attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO, HVACMode.COOL, HVACMode.HEAT]
|
||||
_attr_preset_modes = ["auto", "comfort", "sleep", "away"]
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
)
|
||||
_attr_swing_modes = [SWING_OFF, SWING_ON]
|
||||
_attr_swing_horizontal_modes = [SWING_OFF, SWING_ON]
|
||||
|
||||
def __init__(self, coordinator: GreeControllerCoordinator, zone_id: str) -> None:
|
||||
super().__init__(coordinator)
|
||||
@@ -285,10 +281,30 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl
|
||||
return zone
|
||||
return {}
|
||||
|
||||
@property
|
||||
def _device(self) -> dict[str, Any]:
|
||||
device_id = str(self._zone.get("device_id") or "").strip()
|
||||
return self.coordinator.data.get(device_id, {}) if device_id else {}
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return super().available and bool(self._zone)
|
||||
|
||||
@property
|
||||
def supported_features(self) -> ClimateEntityFeature:
|
||||
features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
)
|
||||
capabilities = self._device.get("capabilities") or {}
|
||||
if capabilities.get("vertical_swing", True) is not False:
|
||||
features |= ClimateEntityFeature.SWING_MODE
|
||||
if capabilities.get("horizontal_swing", True) is not False:
|
||||
features |= ClimateEntityFeature.SWING_HORIZONTAL_MODE
|
||||
return features
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
zone = self._zone
|
||||
@@ -325,6 +341,14 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl
|
||||
def preset_mode(self) -> str:
|
||||
return str(self._zone.get("preset_override") or "auto")
|
||||
|
||||
@property
|
||||
def swing_mode(self) -> str:
|
||||
return SWING_ON if self._device.get("swing_vertical", False) else SWING_OFF
|
||||
|
||||
@property
|
||||
def swing_horizontal_mode(self) -> str:
|
||||
return SWING_ON if self._device.get("swing_horizontal", False) else SWING_OFF
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
zone = self._zone
|
||||
@@ -380,6 +404,20 @@ class GreeControllerZoneClimate(CoordinatorEntity[GreeControllerCoordinator], Cl
|
||||
return
|
||||
await self.coordinator.async_zone_control(self._zone_id, {"preset": preset_mode})
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
device_id = str(self._zone.get("device_id") or "").strip()
|
||||
if device_id:
|
||||
await self.coordinator.async_device_command(
|
||||
device_id, {"swing_vertical": swing_mode == SWING_ON}
|
||||
)
|
||||
|
||||
async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
|
||||
device_id = str(self._zone.get("device_id") or "").strip()
|
||||
if device_id:
|
||||
await self.coordinator.async_device_command(
|
||||
device_id, {"swing_horizontal": swing_horizontal_mode == SWING_ON}
|
||||
)
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
await self.coordinator.async_zone_control(self._zone_id, {"power": True})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "gree_controller",
|
||||
"name": "GREE Controller",
|
||||
"version": "0.14.1",
|
||||
"version": "0.14.2",
|
||||
"config_flow": true,
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## 0.14.2
|
||||
|
||||
- Adds separate vertical and horizontal swing controls for local units in Manual Control, respecting per-device capabilities.
|
||||
- Adds vertical and horizontal swing controls to thermostat zone cards without switching the zone into manual device ownership.
|
||||
- Adds optional `swing_vertical` and `swing_horizontal` actions to legacy direct-device automations.
|
||||
- Extends Visual Flow so both `device_action` and `zone_thermostat` can control vertical/horizontal swing, and device-state selectors use readable swing labels.
|
||||
- Prevents swing-enabled thermostat Flows from being reduced to native schedules, preserving the auxiliary swing action at runtime.
|
||||
- Exposes vertical and horizontal swing on Home Assistant zone thermostat climate entities when supported by the assigned unit.
|
||||
|
||||
## 0.14.1
|
||||
|
||||
- Adds split and multisplit installation groups in the Devices section, including assignment of indoor units to a shared outdoor installation.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: "GREE Controller"
|
||||
version: "0.14.1"
|
||||
version: "0.14.2"
|
||||
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/"
|
||||
|
||||
+15
-2
@@ -1286,6 +1286,19 @@
|
||||
"energy.comparisonPeriod": "Comparison period",
|
||||
"energy.groupSource": "Group source",
|
||||
"energy.selectAtLeastOne": "Select at least one device or installation.",
|
||||
"energy.multiselectHint": "You can select several items (Ctrl/Cmd or Shift)."
|
||||
"energy.multiselectHint": "Select up to 8 items.",
|
||||
"energy.totalMeter": "Total consumption",
|
||||
"energy.lastReading": "Last reading",
|
||||
"devices.sharedOutdoorMetric": "Shared outdoor temperature",
|
||||
"history.sharedOutdoor": "shared outdoor temperature",
|
||||
"energy.chooseTargets": "Choose devices / installations",
|
||||
"energy.selectedCount": "Selected: {count}",
|
||||
"energy.maxTargets": "You can select up to 8 devices or installations.",
|
||||
"energy.period": "Data period",
|
||||
"energy.periodHint": "Time range used for energy bars and totals.",
|
||||
"devices.swingVertical": "Vertical swing",
|
||||
"devices.swingHorizontal": "Horizontal swing",
|
||||
"flow.thermostatDeviceOptions": "Unit swing control",
|
||||
"flow.thermostatDeviceOptionsHint": "Vertical and horizontal swing are sent to the unit as independent settings and do not change thermostat logic."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-2
@@ -1286,6 +1286,19 @@
|
||||
"energy.comparisonPeriod": "Okres porównawczy",
|
||||
"energy.groupSource": "Źródło grupy",
|
||||
"energy.selectAtLeastOne": "Wybierz co najmniej jedno urządzenie lub instalację.",
|
||||
"energy.multiselectHint": "Możesz zaznaczyć kilka pozycji (Ctrl/Cmd lub Shift)."
|
||||
"energy.multiselectHint": "Wybierz do 8 pozycji.",
|
||||
"energy.totalMeter": "Całkowite zużycie",
|
||||
"energy.lastReading": "Ostatni odczyt",
|
||||
"devices.sharedOutdoorMetric": "Wspólna temperatura zewnętrzna",
|
||||
"history.sharedOutdoor": "wspólna temperatura zewnętrzna",
|
||||
"energy.chooseTargets": "Wybierz urządzenia / instalacje",
|
||||
"energy.selectedCount": "Wybrano: {count}",
|
||||
"energy.maxTargets": "Możesz wybrać maksymalnie 8 urządzeń lub instalacji.",
|
||||
"energy.period": "Okres danych",
|
||||
"energy.periodHint": "Zakres czasu używany do słupków i sum energii.",
|
||||
"devices.swingVertical": "Swing pionowy",
|
||||
"devices.swingHorizontal": "Swing poziomy",
|
||||
"flow.thermostatDeviceOptions": "Sterowanie żaluzjami jednostki",
|
||||
"flow.thermostatDeviceOptionsHint": "Swing pionowy i poziomy są wysyłane do jednostki jako niezależne ustawienia i nie zmieniają logiki termostatu."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Developer smoke/integration tests for GREE Controller API 0.14.1.
|
||||
"""Developer smoke/integration tests for GREE Controller API 0.14.2.
|
||||
|
||||
Default mode is read-only and safe to run against a real controller.
|
||||
Use --settings-write to additionally round-trip all split settings resources and
|
||||
@@ -26,7 +26,7 @@ from typing import Any, Callable, Iterable, Optional
|
||||
|
||||
DEFAULT_BASE_URL = os.environ.get("GREE_API_URL", "http://127.0.0.1:8787")
|
||||
DEFAULT_TOKEN = os.environ.get("GREE_API_TOKEN", "")
|
||||
DEFAULT_EXPECTED_VERSION = "0.14.1"
|
||||
DEFAULT_EXPECTED_VERSION = "0.14.2"
|
||||
|
||||
SETTINGS_PATHS: dict[str, set[str]] = {
|
||||
"/api/settings/application": {"simulator_enabled"},
|
||||
@@ -571,7 +571,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.1 contract", lambda: test_openapi(client, args.expected_version))
|
||||
runner.run("OpenAPI 0.14.2 contract", lambda: test_openapi(client, args.expected_version))
|
||||
runner.run("bootstrap snapshot contract", lambda: test_bootstrap_contract(client))
|
||||
runner.run("protected API requires auth", lambda: test_protected_auth(client))
|
||||
|
||||
@@ -640,7 +640,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.1.",
|
||||
description="Developer smoke/integration tests for GREE Controller API 0.14.2.",
|
||||
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.1".into()));
|
||||
return Err(AppError::BadRequest("unsupported configuration export version; version 3 is required by GREE Controller 0.14.2".into()));
|
||||
}
|
||||
if export.settings.control_strategy != "setpoint" {
|
||||
return Err(AppError::BadRequest(
|
||||
|
||||
+5
-1
@@ -1107,7 +1107,9 @@ fn compile_flow(
|
||||
&& native_time_range
|
||||
&& preset_for_schedule != "auto"
|
||||
&& mode_for_schedule == "auto"
|
||||
&& flow_bool(&action_node.config, "power").is_none();
|
||||
&& flow_bool(&action_node.config, "power").is_none()
|
||||
&& flow_bool(&action_node.config, "swing_vertical").is_none()
|
||||
&& flow_bool(&action_node.config, "swing_horizontal").is_none();
|
||||
if schedule_only {
|
||||
let zone_id = flow_string(&action_node.config, "zone_id")
|
||||
.ok_or_else(|| AppError::BadRequest("thermostat block needs a zone".into()))?;
|
||||
@@ -1285,6 +1287,8 @@ fn compile_flow(
|
||||
}
|
||||
item.action.mode = Some(mode);
|
||||
}
|
||||
item.action.swing_vertical = flow_bool(&action_node.config, "swing_vertical");
|
||||
item.action.swing_horizontal = flow_bool(&action_node.config, "swing_horizontal");
|
||||
}
|
||||
"device_action" => {
|
||||
let id = flow_string(&action_node.config, "device_id")
|
||||
|
||||
+78
-1
@@ -20,12 +20,81 @@ struct SystemInfoResponse {
|
||||
gree_received_frames_by_device: std::collections::HashMap<String, u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
struct DeviceGroupEnergySnapshot {
|
||||
total_kwh: f64,
|
||||
timestamp: Option<chrono::DateTime<Utc>>,
|
||||
source: String,
|
||||
origin: String,
|
||||
}
|
||||
|
||||
fn device_group_energy_snapshot(
|
||||
state: &AppState,
|
||||
group: &DeviceGroup,
|
||||
devices: &[Device],
|
||||
) -> Result<Option<DeviceGroupEnergySnapshot>, AppError> {
|
||||
let selected_source = match group.energy_source {
|
||||
EnergySourcePreference::GreeCloud => Some("gree_cloud"),
|
||||
EnergySourcePreference::HomeAssistant => Some("home_assistant"),
|
||||
EnergySourcePreference::Auto if group.energy_device_id.is_some() => Some("gree_cloud"),
|
||||
EnergySourcePreference::Auto if group.ha_energy_entity_id.is_some() => Some("home_assistant"),
|
||||
EnergySourcePreference::Auto => None,
|
||||
};
|
||||
|
||||
match selected_source {
|
||||
Some("gree_cloud") => {
|
||||
let Some(device_id) = group.energy_device_id.as_deref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(device) = devices.iter().find(|device| device.id == device_id) {
|
||||
if let Some(total_kwh) = device
|
||||
.total_energy_kwh
|
||||
.filter(|value| value.is_finite() && *value >= 0.0)
|
||||
{
|
||||
return Ok(Some(DeviceGroupEnergySnapshot {
|
||||
total_kwh,
|
||||
timestamp: device
|
||||
.last_cloud_sync
|
||||
.clone()
|
||||
.or_else(|| device.last_seen.clone()),
|
||||
source: "gree_cloud".into(),
|
||||
origin: "cloud".into(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(state
|
||||
.db
|
||||
.last_energy_reading(device_id, "gree_cloud")?
|
||||
.map(|reading| DeviceGroupEnergySnapshot {
|
||||
total_kwh: reading.normalized_meter_kwh,
|
||||
timestamp: Some(reading.timestamp),
|
||||
source: "gree_cloud".into(),
|
||||
origin: "database".into(),
|
||||
}))
|
||||
}
|
||||
Some("home_assistant") => {
|
||||
let storage_id = format!("group:{}", group.id);
|
||||
Ok(state
|
||||
.db
|
||||
.last_energy_reading(&storage_id, "home_assistant")?
|
||||
.map(|reading| DeviceGroupEnergySnapshot {
|
||||
total_kwh: reading.normalized_meter_kwh,
|
||||
timestamp: Some(reading.timestamp),
|
||||
source: "home_assistant".into(),
|
||||
origin: "database".into(),
|
||||
}))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct BootstrapResponse {
|
||||
devices: Vec<Device>,
|
||||
zones: Vec<Zone>,
|
||||
groups: Vec<ClimateGroup>,
|
||||
device_groups: Vec<DeviceGroup>,
|
||||
device_group_energy: std::collections::HashMap<String, DeviceGroupEnergySnapshot>,
|
||||
schedules: Vec<Schedule>,
|
||||
automations: Vec<Automation>,
|
||||
flows: Vec<Flow>,
|
||||
@@ -59,6 +128,13 @@ async fn build_bootstrap(state: &AppState) -> Result<BootstrapResponse, AppError
|
||||
(settings_snapshot(&settings), settings.house_mode.clone())
|
||||
};
|
||||
let devices = state.db.list_devices()?;
|
||||
let device_groups = state.db.list_device_groups()?;
|
||||
let mut device_group_energy = std::collections::HashMap::new();
|
||||
for group in &device_groups {
|
||||
if let Some(snapshot) = device_group_energy_snapshot(state, group, &devices)? {
|
||||
device_group_energy.insert(group.id.clone(), snapshot);
|
||||
}
|
||||
}
|
||||
let system = build_system_info(state, &devices);
|
||||
let control_plan = engine::get_control_plan_snapshot(state).await?;
|
||||
|
||||
@@ -66,7 +142,8 @@ async fn build_bootstrap(state: &AppState) -> Result<BootstrapResponse, AppError
|
||||
devices,
|
||||
zones: state.db.list_zones()?,
|
||||
groups: state.db.list_groups()?,
|
||||
device_groups: state.db.list_device_groups()?,
|
||||
device_groups,
|
||||
device_group_energy,
|
||||
schedules: state.db.list_schedules()?,
|
||||
automations: state.db.list_automations()?,
|
||||
flows: state.db.list_flows()?,
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ use crate::{
|
||||
error::AppError,
|
||||
home_assistant, influxdb,
|
||||
models::{
|
||||
Automation, AutomationPlanRule, ClimateGroup, ConnectionStatus, ConnectionType, ControlPlan, ControlPlanEvent, Device, EnergyReading, EnergySourcePreference,
|
||||
Automation, AutomationPlanRule, ClimateGroup, ConnectionStatus, ConnectionType, ControlPlan, ControlPlanEvent, Device, DeviceGroup, EnergyReading, EnergySourcePreference,
|
||||
DeviceCommand, GroupControlPatch, HaReading, NightModeSettings, Reading, RuntimeSettings,
|
||||
Schedule, TemporaryQuickThermostat, Zone, ZoneControlPlan, ZoneReading,
|
||||
},
|
||||
|
||||
@@ -897,13 +897,36 @@ async fn apply_flow_zone_action(state: &AppState, zone_id: &str, preset: Option<
|
||||
zone.control_reason = "Visual Flow automation".into();
|
||||
}
|
||||
refresh_zone_runtime_target(&mut zone, &schedules, &house_mode);
|
||||
state.db.save_zone(&zone)?; state.broadcast("zone.updated", serde_json::to_value(&zone)?); state.wake_zone_control();
|
||||
state.db.save_zone(&zone)?;
|
||||
state.broadcast("zone.updated", serde_json::to_value(&zone)?);
|
||||
|
||||
let swing_command = DeviceCommand {
|
||||
swing_vertical: action.swing_vertical,
|
||||
swing_horizontal: action.swing_horizontal,
|
||||
..Default::default()
|
||||
};
|
||||
if action.power == Some(false) {
|
||||
// Disabled zones are intentionally skipped by the normal thermostat cycle. Perform the
|
||||
// physical OFF under the canonical zone -> device lock order so the durable Flow intent
|
||||
// cannot leave a unit running and polling/manual control cannot interleave with the frame.
|
||||
// Swing can safely share this explicit frame because it is outside thermostat regulation.
|
||||
let _device_guard = state.lock_device_operation(&device_id).await;
|
||||
send_command_locked_forced(state, &device_id, DeviceCommand { power: Some(false), ..Default::default() }).await?;
|
||||
send_command_locked_forced(
|
||||
state,
|
||||
&device_id,
|
||||
DeviceCommand {
|
||||
power: Some(false),
|
||||
swing_vertical: swing_command.swing_vertical,
|
||||
swing_horizontal: swing_command.swing_horizontal,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
} else if !swing_command.is_empty() {
|
||||
// Swing is intentionally a one-shot auxiliary unit setting. It does not participate in
|
||||
// temperature/fan regulation, so the thermostat keeps ownership of the zone.
|
||||
let _ = send_automatic_device_command_if_owned(state, &device_id, swing_command).await?;
|
||||
}
|
||||
state.wake_zone_control();
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -56,6 +56,15 @@ pub(crate) fn record_cumulative_energy_sample(
|
||||
reset_detected,
|
||||
};
|
||||
state.db.add_energy_reading(&reading)?;
|
||||
state.broadcast(
|
||||
"energy.updated",
|
||||
json!({
|
||||
"target_id": reading.device_id.clone(),
|
||||
"source": reading.source.clone(),
|
||||
"total_kwh": reading.normalized_meter_kwh,
|
||||
"timestamp": reading.timestamp,
|
||||
}),
|
||||
);
|
||||
queue_influx_energy(state, reading.clone());
|
||||
Ok(reading)
|
||||
}
|
||||
|
||||
+51
-7
@@ -121,13 +121,7 @@ fn queue_influx_ha(state: &AppState, reading: HaReading) {
|
||||
});
|
||||
}
|
||||
|
||||
fn gree_outdoor_temperature(devices: &[Device]) -> Option<f64> {
|
||||
let mut values: Vec<f64> = devices
|
||||
.iter()
|
||||
.filter(|device| device.enabled && device.online && device.communication_failures == 0)
|
||||
.filter_map(|device| device.outdoor_temperature)
|
||||
.filter(|value| value.is_finite() && (-60.0..=70.0).contains(value))
|
||||
.collect();
|
||||
fn median_outdoor_temperature(mut values: Vec<f64>) -> Option<f64> {
|
||||
if values.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -140,3 +134,53 @@ fn gree_outdoor_temperature(devices: &[Device]) -> Option<f64> {
|
||||
};
|
||||
Some((value * 10.0).round() / 10.0)
|
||||
}
|
||||
|
||||
fn valid_outdoor_temperature(device: &Device) -> Option<f64> {
|
||||
if !device.enabled || !device.online || device.communication_failures != 0 {
|
||||
return None;
|
||||
}
|
||||
device
|
||||
.outdoor_temperature
|
||||
.filter(|value| value.is_finite() && (-60.0..=70.0).contains(value))
|
||||
}
|
||||
|
||||
fn gree_outdoor_temperature(devices: &[Device], device_groups: &[DeviceGroup]) -> Option<f64> {
|
||||
let by_id: HashMap<&str, &Device> = devices
|
||||
.iter()
|
||||
.map(|device| (device.id.as_str(), device))
|
||||
.collect();
|
||||
let mut grouped_ids = HashSet::new();
|
||||
let mut values = Vec::new();
|
||||
|
||||
for group in device_groups {
|
||||
for device_id in &group.device_ids {
|
||||
grouped_ids.insert(device_id.as_str());
|
||||
}
|
||||
let group_value = group
|
||||
.outdoor_temperature_device_id
|
||||
.as_deref()
|
||||
.and_then(|source_id| by_id.get(source_id).copied())
|
||||
.and_then(valid_outdoor_temperature)
|
||||
.or_else(|| {
|
||||
median_outdoor_temperature(
|
||||
group
|
||||
.device_ids
|
||||
.iter()
|
||||
.filter_map(|id| by_id.get(id.as_str()).copied())
|
||||
.filter_map(valid_outdoor_temperature)
|
||||
.collect(),
|
||||
)
|
||||
});
|
||||
if let Some(value) = group_value {
|
||||
values.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
values.extend(
|
||||
devices
|
||||
.iter()
|
||||
.filter(|device| !grouped_ids.contains(device.id.as_str()))
|
||||
.filter_map(valid_outdoor_temperature),
|
||||
);
|
||||
median_outdoor_temperature(values)
|
||||
}
|
||||
|
||||
+31
-1
@@ -352,6 +352,7 @@ mod tests {
|
||||
assert!(automation_action_conflicts_with_thermostat(&DeviceCommand { mode: Some("dry".into()), ..Default::default() }));
|
||||
assert!(!automation_action_conflicts_with_thermostat(&DeviceCommand { mode: Some("heat".into()), ..Default::default() }));
|
||||
assert!(!automation_action_conflicts_with_thermostat(&DeviceCommand { light: Some(false), turbo: Some(true), ..Default::default() }));
|
||||
assert!(!automation_action_conflicts_with_thermostat(&DeviceCommand { swing_vertical: Some(true), swing_horizontal: Some(false), ..Default::default() }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -988,9 +989,38 @@ mod tests {
|
||||
let mut c = Device::simulated_default();
|
||||
c.id = "sim-c".into();
|
||||
c.outdoor_temperature = Some(40.0);
|
||||
assert_eq!(gree_outdoor_temperature(&[a, b, c]), Some(12.0));
|
||||
assert_eq!(gree_outdoor_temperature(&[a, b, c], &[]), Some(12.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gree_outdoor_fallback_counts_multisplit_once() {
|
||||
let mut a = Device::simulated_default();
|
||||
a.id = "a".into();
|
||||
a.outdoor_temperature = Some(10.0);
|
||||
let mut b = Device::simulated_default();
|
||||
b.id = "b".into();
|
||||
b.outdoor_temperature = Some(10.0);
|
||||
let mut c = Device::simulated_default();
|
||||
c.id = "c".into();
|
||||
c.outdoor_temperature = Some(30.0);
|
||||
let now = Utc::now();
|
||||
let group = DeviceGroup {
|
||||
id: "multi".into(),
|
||||
name: "Multi".into(),
|
||||
kind: crate::models::DeviceGroupKind::Multisplit,
|
||||
device_ids: vec!["a".into(), "b".into()],
|
||||
energy_source: EnergySourcePreference::Auto,
|
||||
energy_device_id: None,
|
||||
ha_energy_entity_id: None,
|
||||
ha_energy_unit: None,
|
||||
ha_energy_device_class: None,
|
||||
ha_energy_state_class: None,
|
||||
outdoor_temperature_device_id: Some("a".into()),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
assert_eq!(gree_outdoor_temperature(&[a, b, c], &[group]), Some(20.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_one_shot_queue_does_not_change_zone_ownership() {
|
||||
|
||||
@@ -76,7 +76,8 @@ async fn resolve_cycle_outdoor_temperature(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let temperature = from_home_assistant.or_else(|| gree_outdoor_temperature(devices));
|
||||
let device_groups = state.db.list_device_groups().unwrap_or_default();
|
||||
let temperature = from_home_assistant.or_else(|| gree_outdoor_temperature(devices, &device_groups));
|
||||
let mut current = state.outdoor_temperature.write().await;
|
||||
if *current != temperature {
|
||||
*current = temperature;
|
||||
|
||||
+14
-2
@@ -333,7 +333,7 @@
|
||||
<div class="panel chart-panel history-toolbar-panel">
|
||||
<div class="chart-toolbar">
|
||||
<div id="historyContextControls" class="history-context-controls"></div>
|
||||
<label class="history-range-control"><span data-i18n="history.range">Range</span><select id="historyHours"
|
||||
<label class="history-range-control" id="historyRangeControl"><span data-i18n="history.range">Range</span><select id="historyHours"
|
||||
data-i18n-aria="history.range" aria-label="History range">
|
||||
<option value="6" data-i18n="history.6h">6 hours</option>
|
||||
<option value="24" selected data-i18n="history.24h">24 hours</option>
|
||||
@@ -341,7 +341,7 @@
|
||||
<option value="720" data-i18n="history.30d">30 days</option>
|
||||
<option value="2160" data-i18n="history.90d">90 days</option>
|
||||
<option value="8760" data-i18n="history.1y">1 year</option>
|
||||
</select></label>
|
||||
</select><small id="historyRangeHint" hidden></small></label>
|
||||
<button class="secondary history-refresh-button" id="historyRefresh"
|
||||
data-i18n="actions.refresh">Refresh</button>
|
||||
</div>
|
||||
@@ -1584,6 +1584,18 @@
|
||||
type="text" inputmode="decimal" step="0.1" min="8" max="30"
|
||||
name="action_target_temperature"></label><label><span data-i18n="common.cooldownSeconds">Cooldown
|
||||
(s)</span><input type="number" min="30" name="cooldown_seconds" value="300"></label></div>
|
||||
<div class="two" id="automationDeviceSwingOptions">
|
||||
<label><span data-i18n="devices.swingVertical">Vertical swing</span><select name="action_swing_vertical">
|
||||
<option value="" data-i18n="actions.noChange">No change</option>
|
||||
<option value="true" data-i18n="actions.turnOn">Turn on</option>
|
||||
<option value="false" data-i18n="actions.turnOff">Turn off</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="devices.swingHorizontal">Horizontal swing</span><select name="action_swing_horizontal">
|
||||
<option value="" data-i18n="actions.noChange">No change</option>
|
||||
<option value="true" data-i18n="actions.turnOn">Turn on</option>
|
||||
<option value="false" data-i18n="actions.turnOff">Turn off</option>
|
||||
</select></label>
|
||||
</div>
|
||||
<p class="field-note" id="automationGroupHint" hidden data-i18n="automations.groupHint">For a group, Auto mode
|
||||
means follow the whole-house heating/cooling mode. Group presets are applied to all member zones.</p>
|
||||
<label class="check"><input type="checkbox" name="enabled" checked> <span
|
||||
|
||||
Vendored
+1
@@ -35,6 +35,7 @@ function settingsFromSnapshot(sections, houseMode = app.settings?.house_mode ||
|
||||
function applyBootstrapSnapshot(data) {
|
||||
app.devices = data.devices || [];
|
||||
app.deviceGroups = data.device_groups || [];
|
||||
app.deviceGroupEnergy = data.device_group_energy || {};
|
||||
app.zones = data.zones || [];
|
||||
app.groups = data.groups || [];
|
||||
app.schedules = data.schedules || [];
|
||||
|
||||
+97
-4
@@ -51,9 +51,45 @@ function normalizeEnergyHistoryTargets() {
|
||||
return available;
|
||||
}
|
||||
|
||||
function energyTargetPickerSummary(targets) {
|
||||
const selected = targets.filter(target => app.historyEnergyTargets.includes(target.id));
|
||||
if (!selected.length) return tr('energy.chooseTargets');
|
||||
if (selected.length === 1) return selected[0].label;
|
||||
return tr('energy.selectedCount', { count: selected.length });
|
||||
}
|
||||
|
||||
function resetHistoryRangeControl(host) {
|
||||
const range = $('#historyRangeControl');
|
||||
const toolbar = host?.closest('.chart-toolbar');
|
||||
const refresh = $('#historyRefresh');
|
||||
if (range && toolbar && range.parentElement === host) toolbar.insertBefore(range, refresh);
|
||||
}
|
||||
|
||||
function configureHistoryRangeControl(tab, host) {
|
||||
const range = $('#historyRangeControl');
|
||||
if (!range) return;
|
||||
const toolbar = range.closest('.chart-toolbar') || host?.closest('.chart-toolbar');
|
||||
const label = range.querySelector('span');
|
||||
const hint = $('#historyRangeHint');
|
||||
const sixHours = range.querySelector('option[value="6"]');
|
||||
const energy = tab === 'energy';
|
||||
const rangeLabel = tr(energy ? 'energy.period' : 'history.range');
|
||||
if (label) label.textContent = rangeLabel;
|
||||
range.setAttribute('aria-label', rangeLabel);
|
||||
if (hint) { hint.textContent = energy ? tr('energy.periodHint') : ''; hint.hidden = !energy; }
|
||||
if (sixHours) { sixHours.hidden = energy; sixHours.disabled = energy; }
|
||||
if (energy && $('#historyHours')?.value === '6') $('#historyHours').value = '24';
|
||||
range.classList.toggle('energy-period-control', energy);
|
||||
if (toolbar) toolbar.classList.toggle('energy-toolbar', energy);
|
||||
if (energy && host) host.appendChild(range);
|
||||
}
|
||||
|
||||
function renderHistoryNavigation() {
|
||||
$$('#historyTabs [data-history-tab]').forEach(button => button.classList.toggle('active', button.dataset.historyTab === app.historyTab));
|
||||
const energyPickerWasOpen = $('#historyEnergyTargetPicker')?.open === true;
|
||||
const host = $('#historyContextControls'); if (!host) return;
|
||||
resetHistoryRangeControl(host);
|
||||
host.classList.toggle('energy-context-controls', app.historyTab === 'energy');
|
||||
const options = historyEntityOptions();
|
||||
if (app.historyTab === 'zones') {
|
||||
host.innerHTML = `<label><span>${esc(tr('common.zone'))}</span><select id="historyZoneSelect"><option value="all">${esc(tr('history.allZones'))}</option>${options.zones}</select></label>`;
|
||||
@@ -63,14 +99,13 @@ function renderHistoryNavigation() {
|
||||
const select = $('#historyDeviceSelect'); if ([...select.options].some(option => option.value === app.historyDevice)) select.value = app.historyDevice;
|
||||
} else if (app.historyTab === 'energy') {
|
||||
const targets = normalizeEnergyHistoryTargets();
|
||||
const targetOptions = targets.map(target => `<option value="${esc(target.id)}">${esc(target.label)}</option>`).join('');
|
||||
const targetOptions = targets.map(target => `<label class="history-energy-option"><input type="checkbox" data-history-energy-target="${esc(target.id)}" ${app.historyEnergyTargets.includes(target.id) ? 'checked' : ''}><span>${esc(target.label)}</span></label>`).join('');
|
||||
host.innerHTML = targets.length
|
||||
? `<label class="history-energy-targets"><span>${esc(tr('energy.targets'))}</span><select id="historyEnergyTargetSelect" multiple size="${Math.min(6, Math.max(2, targets.length))}">${targetOptions}</select><small>${esc(tr('energy.multiselectHint'))}</small></label><label><span>${esc(tr('history.bucket'))}</span><select id="historyEnergyInterval"><option value="hourly">${esc(tr('energy.hourly'))}</option><option value="daily">${esc(tr('energy.daily'))}</option><option value="weekly">${esc(tr('energy.weekly'))}</option><option value="monthly">${esc(tr('energy.monthly'))}</option></select></label><label><span>${esc(tr('energy.compare'))}</span><select id="historyEnergyCompare"><option value="none">${esc(tr('energy.compareNone'))}</option><option value="previous_day">${esc(tr('energy.comparePreviousDay'))}</option><option value="previous_period">${esc(tr('energy.comparePreviousPeriod'))}</option><option value="previous_year">${esc(tr('energy.comparePreviousYear'))}</option></select></label>`
|
||||
? `<div class="history-energy-targets"><span class="history-control-label">${esc(tr('energy.targets'))}</span><details class="history-energy-picker" id="historyEnergyTargetPicker"><summary><span>${esc(energyTargetPickerSummary(targets))}</span><b>${app.historyEnergyTargets.length}/8</b></summary><div class="history-energy-options">${targetOptions}</div></details><small>${esc(tr('energy.multiselectHint'))}</small></div><label><span>${esc(tr('history.bucket'))}</span><select id="historyEnergyInterval"><option value="hourly">${esc(tr('energy.hourly'))}</option><option value="daily">${esc(tr('energy.daily'))}</option><option value="weekly">${esc(tr('energy.weekly'))}</option><option value="monthly">${esc(tr('energy.monthly'))}</option></select></label><label><span>${esc(tr('energy.compare'))}</span><select id="historyEnergyCompare"><option value="none">${esc(tr('energy.compareNone'))}</option><option value="previous_day">${esc(tr('energy.comparePreviousDay'))}</option><option value="previous_period">${esc(tr('energy.comparePreviousPeriod'))}</option><option value="previous_year">${esc(tr('energy.comparePreviousYear'))}</option></select></label>`
|
||||
: `<span class="history-context-hint">${esc(tr('energy.noData'))}</span>`;
|
||||
const targetSelect = $('#historyEnergyTargetSelect');
|
||||
if (targetSelect) [...targetSelect.options].forEach(option => { option.selected = app.historyEnergyTargets.includes(option.value); });
|
||||
const intervalSelect = $('#historyEnergyInterval'); if (intervalSelect) intervalSelect.value = app.historyEnergyInterval;
|
||||
const compareSelect = $('#historyEnergyCompare'); if (compareSelect) compareSelect.value = app.historyEnergyCompare;
|
||||
if (energyPickerWasOpen && $('#historyEnergyTargetPicker')) $('#historyEnergyTargetPicker').open = true;
|
||||
} else if (app.historyTab === 'sensors') {
|
||||
host.innerHTML = `<label><span>${esc(tr('history.haSensor'))}</span><select id="historySensorSelect"><option value="all">${esc(tr('history.allSensors'))}</option>${options.sensors}</select></label>`;
|
||||
const select = $('#historySensorSelect'); if ([...select.options].some(option => option.value === app.historySensor)) select.value = app.historySensor;
|
||||
@@ -79,6 +114,7 @@ function renderHistoryNavigation() {
|
||||
} else {
|
||||
host.innerHTML = `<span class="history-context-hint">${esc(tr('history.overviewHint'))}</span>`;
|
||||
}
|
||||
configureHistoryRangeControl(app.historyTab, host);
|
||||
}
|
||||
|
||||
async function loadHistory() {
|
||||
@@ -475,6 +511,62 @@ function energyBucketLabel(timestamp, interval) {
|
||||
return new Intl.DateTimeFormat(locale(), { day: '2-digit', month: '2-digit', ...(interval === 'weekly' ? { year: '2-digit' } : {}) }).format(date);
|
||||
}
|
||||
|
||||
function energyBucketTooltipLabel(timestamp, interval) {
|
||||
const date = new Date(timestamp);
|
||||
if (interval === 'hourly') return chartPreciseTime(timestamp);
|
||||
if (interval === 'monthly') return new Intl.DateTimeFormat(locale(), { month: 'long', year: 'numeric' }).format(date);
|
||||
if (interval === 'weekly') return new Intl.DateTimeFormat(locale(), { day: '2-digit', month: '2-digit', year: 'numeric' }).format(date);
|
||||
return new Intl.DateTimeFormat(locale(), { day: '2-digit', month: '2-digit', year: 'numeric' }).format(date);
|
||||
}
|
||||
|
||||
function bindEnergyChartTooltip(canvas, series, starts, maps, geometry) {
|
||||
const wrap = canvas?.parentElement;
|
||||
const tooltip = wrap?.querySelector('.chart-tooltip');
|
||||
const line = wrap?.querySelector('.chart-hover-line');
|
||||
if (!canvas || !wrap || !tooltip || !line || !starts.length) return;
|
||||
const { pad, width, height, slot, interval } = geometry;
|
||||
let activeIndex = -1;
|
||||
|
||||
const hide = () => { tooltip.hidden = true; line.hidden = true; activeIndex = -1; };
|
||||
const showIndex = index => {
|
||||
index = clamp(index, 0, starts.length - 1);
|
||||
const start = starts[index];
|
||||
const px = pad.left + slot * index + slot / 2;
|
||||
const rows = series.map((item, seriesIndex) => ({ item, value: maps[seriesIndex].get(start) || 0 }));
|
||||
$$('.chart-tooltip').forEach(node => { if (node !== tooltip) node.hidden = true; });
|
||||
$$('.chart-hover-line').forEach(node => { if (node !== line) node.hidden = true; });
|
||||
tooltip.innerHTML = `<strong class="chart-tooltip-time">${esc(energyBucketTooltipLabel(start, interval))}</strong><div class="chart-tooltip-values">${rows.map(({ item, value }) => `<div class="chart-tooltip-row"><span class="chart-tooltip-label"><i style="--tooltip-color:${esc(item.color)}"></i><span>${esc(item.label)}</span></span><span class="chart-tooltip-value">${esc(Number(value).toLocaleString(locale(), { minimumFractionDigits: value > 0 && value < 0.1 ? 3 : 2, maximumFractionDigits: 3 }))} kWh</span></div>`).join('')}</div>`;
|
||||
tooltip.hidden = false;
|
||||
line.hidden = false;
|
||||
line.style.left = `${px}px`;
|
||||
line.style.top = `${pad.top}px`;
|
||||
line.style.height = `${Math.max(0, height - pad.top - pad.bottom)}px`;
|
||||
const minLeft = wrap.scrollLeft + 8;
|
||||
const maxLeft = wrap.scrollLeft + wrap.clientWidth - tooltip.offsetWidth - 8;
|
||||
let left = px + 12;
|
||||
if (left + tooltip.offsetWidth > wrap.scrollLeft + wrap.clientWidth - 8) left = px - tooltip.offsetWidth - 12;
|
||||
tooltip.style.left = `${Math.max(minLeft, Math.min(left, Math.max(minLeft, maxLeft)))}px`;
|
||||
tooltip.style.top = `${pad.top + 8}px`;
|
||||
activeIndex = index;
|
||||
};
|
||||
const indexFromClientX = clientX => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = clamp(clientX - rect.left, pad.left, width - pad.right - 1);
|
||||
return clamp(Math.floor((x - pad.left) / Math.max(1, slot)), 0, starts.length - 1);
|
||||
};
|
||||
|
||||
canvas.onpointermove = event => { if (event.pointerType !== 'touch') showIndex(indexFromClientX(event.clientX)); };
|
||||
canvas.onpointerleave = event => { if (event.pointerType !== 'touch') hide(); };
|
||||
canvas.onpointerdown = null; canvas.onpointerup = null; canvas.onpointercancel = null;
|
||||
canvas.onkeydown = event => {
|
||||
if (!['ArrowLeft', 'ArrowRight'].includes(event.key)) return;
|
||||
event.preventDefault();
|
||||
if (activeIndex < 0) activeIndex = event.key === 'ArrowRight' ? 0 : starts.length - 1;
|
||||
else activeIndex = clamp(activeIndex + (event.key === 'ArrowRight' ? 1 : -1), 0, starts.length - 1);
|
||||
showIndex(activeIndex);
|
||||
};
|
||||
}
|
||||
|
||||
function drawEnergyBarChart(canvas, seriesInput, { height = 340, interval = app.historyEnergyInterval } = {}) {
|
||||
if (!canvas) return;
|
||||
const series = Array.isArray(seriesInput) && seriesInput.length && Array.isArray(seriesInput[0]?.buckets)
|
||||
@@ -532,6 +624,7 @@ function drawEnergyBarChart(canvas, seriesInput, { height = 340, interval = app.
|
||||
ctx.fillText(energyBucketLabel(starts[index], interval), x, height - 18);
|
||||
}
|
||||
ctx.textAlign = 'left'; ctx.fillText('kWh', 8, pad.top + 4);
|
||||
bindEnergyChartTooltip(canvas, visibleSeries, starts, maps, { pad, width, height, slot, interval });
|
||||
renderLegend(document.getElementById(`${canvas.id}Legend`), series);
|
||||
updateChartZoomControls(canvas.id);
|
||||
}
|
||||
|
||||
+16
-1
@@ -25,7 +25,7 @@ const preferredLanguage = getCookie('gree_controller_language') || DEFAULT_LANGU
|
||||
const preferredTheme = ['system', 'light', 'dark'].includes(getCookie('gree_controller_theme')) ? getCookie('gree_controller_theme') : 'system';
|
||||
|
||||
const app = {
|
||||
devices: [], zones: [], groups: [], deviceGroups: [], schedules: [], automations: [], flows: [], accessTokens: [], settings: null, system: {}, outdoorTemperature: null,
|
||||
devices: [], zones: [], groups: [], deviceGroups: [], deviceGroupEnergy: {}, schedules: [], automations: [], flows: [], accessTokens: [], settings: null, system: {}, outdoorTemperature: null,
|
||||
token: localStorage.getItem('gree_controller_token') || '', ws: null, wsTimer: null,
|
||||
currentView: 'dashboard', loading: false, bootstrapReloadPending: false, language: preferredLanguage, theme: preferredTheme, connectionStatus: 'connecting',
|
||||
languages: [], translations: {}, locales: {},
|
||||
@@ -59,6 +59,21 @@ const historyNumber = value => value === null || value === undefined || value ==
|
||||
const modeLabel = mode => tr(`mode.${mode}`) === `mode.${mode}` ? mode : tr(`mode.${mode}`);
|
||||
const houseModeLabel = mode => mode === 'off' ? tr('house.noControl') : modeLabel(mode);
|
||||
const fanLabel = value => ({ 0: 'fan.auto', 1: 'fan.low', 2: 'fan.mediumLow', 3: 'fan.medium', 4: 'fan.mediumHigh', 5: 'fan.high' }[value] ? tr({ 0: 'fan.auto', 1: 'fan.low', 2: 'fan.mediumLow', 3: 'fan.medium', 4: 'fan.mediumHigh', 5: 'fan.high' }[value]) : value);
|
||||
const deviceCommandFieldLabel = key => {
|
||||
const translationKey = {
|
||||
fan_speed: 'common.fan',
|
||||
swing_vertical: 'devices.swingVertical',
|
||||
swing_horizontal: 'devices.swingHorizontal',
|
||||
quiet: 'devices.quiet',
|
||||
turbo: 'devices.turbo',
|
||||
light: 'devices.light',
|
||||
air: 'devices.air',
|
||||
xfan: 'devices.xfan',
|
||||
health: 'devices.health',
|
||||
sleep: 'devices.sleep',
|
||||
}[key];
|
||||
return translationKey ? tr(translationKey) : key;
|
||||
};
|
||||
const dateTime = value => value ? new Intl.DateTimeFormat(locale(), { dateStyle: 'short', timeStyle: 'short' }).format(new Date(value)) : '—';
|
||||
const localResumeSeconds = value => {
|
||||
const timestamp = value ? new Date(value).getTime() : NaN;
|
||||
|
||||
+48
-6
@@ -53,6 +53,36 @@ function effectiveDeviceOutdoorTemperature(device) {
|
||||
return device?.outdoor_temperature;
|
||||
}
|
||||
|
||||
function installationEnergySnapshot(group) {
|
||||
if (!group) return null;
|
||||
const selectedSource = group.energy_source === 'gree_cloud' || (group.energy_source === 'auto' && !!group.energy_device_id)
|
||||
? 'gree_cloud'
|
||||
: (group.energy_source === 'home_assistant' || (group.energy_source === 'auto' && !!group.ha_energy_entity_id) ? 'home_assistant' : '');
|
||||
if (selectedSource === 'gree_cloud' && group.energy_device_id) {
|
||||
const device = app.devices.find(item => item.id === group.energy_device_id);
|
||||
const live = device?.total_energy_kwh == null ? NaN : Number(device.total_energy_kwh);
|
||||
if (Number.isFinite(live) && live >= 0) {
|
||||
return { total_kwh: live, timestamp: device.last_cloud_sync || device.last_seen || null, source: 'gree_cloud', origin: 'cloud' };
|
||||
}
|
||||
}
|
||||
const cached = app.deviceGroupEnergy?.[group.id] || null;
|
||||
return cached?.source === selectedSource ? cached : null;
|
||||
}
|
||||
|
||||
function formatEnergyMeterTotal(value) {
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number) || number < 0) return tr('common.unavailable');
|
||||
return `${number.toLocaleString(locale(), { maximumFractionDigits: 3 })} kWh`;
|
||||
}
|
||||
|
||||
function installationEnergySourceShortLabel(group, snapshot) {
|
||||
if (snapshot?.source === 'home_assistant') return 'Home Assistant';
|
||||
if (snapshot?.source === 'gree_cloud') return 'GREE Cloud';
|
||||
if (group?.energy_source === 'home_assistant' || (group?.energy_source === 'auto' && group?.ha_energy_entity_id && !group?.energy_device_id)) return 'Home Assistant';
|
||||
if (group?.energy_source === 'gree_cloud' || group?.energy_device_id) return 'GREE Cloud';
|
||||
return tr('energy.auto');
|
||||
}
|
||||
|
||||
function deviceConnectionStatusLabel(device) {
|
||||
const key = {
|
||||
online: 'status.online',
|
||||
@@ -97,6 +127,7 @@ function manualDeviceProblem(device) {
|
||||
}
|
||||
|
||||
function manualLocalDeviceCard(device) {
|
||||
const caps = device.capabilities || {};
|
||||
const modes = ['auto', 'cool', 'dry', 'fan', 'heat'];
|
||||
const fans = [0, 1, 3, 5];
|
||||
const disabledZone = disabledZoneForDevice(device.id);
|
||||
@@ -118,8 +149,9 @@ function manualLocalDeviceCard(device) {
|
||||
${compressorQueuePanel(managedZone)}
|
||||
<div class="mode-row quick-control-row quick-control-row-5">${modes.map(mode => `<button class="${device.mode === mode ? 'active' : ''}" data-action="mode" data-value="${mode}" data-device="${esc(device.id)}">${esc(modeLabel(mode))}</button>`).join('')}</div>
|
||||
<div class="fan-row quick-control-row quick-control-row-4">${fans.map(fan => `<button class="${Number(device.fan_speed) === fan ? 'active' : ''}" data-action="fan" data-value="${fan}" data-device="${esc(device.id)}">${esc(fanLabel(fan))}</button>`).join('')}</div>
|
||||
<div class="device-toggles quick-control-row quick-control-row-3">
|
||||
<button class="${device.swing_vertical ? 'active' : ''}" data-action="toggle" data-field="swing_vertical" data-device="${esc(device.id)}">${esc(tr('devices.swing'))}</button>
|
||||
<div class="device-toggles quick-control-row quick-control-row-4">
|
||||
${caps.vertical_swing === false ? '' : `<button class="${device.swing_vertical ? 'active' : ''}" data-action="toggle" data-field="swing_vertical" data-device="${esc(device.id)}">${esc(tr('devices.swingVertical'))}</button>`}
|
||||
${caps.horizontal_swing === false ? '' : `<button class="${device.swing_horizontal ? 'active' : ''}" data-action="toggle" data-field="swing_horizontal" data-device="${esc(device.id)}">${esc(tr('devices.swingHorizontal'))}</button>`}
|
||||
${device.supports_quiet === false ? '' : `<button class="${device.quiet ? 'active' : ''}" data-action="toggle" data-field="quiet" data-device="${esc(device.id)}">${esc(tr('devices.quiet'))}</button>`}
|
||||
${device.supports_turbo === false ? '' : `<button class="${device.turbo ? 'active' : ''}" data-action="toggle" data-field="turbo" data-device="${esc(device.id)}">${esc(tr('devices.turbo'))}</button>`}
|
||||
</div>
|
||||
@@ -156,8 +188,9 @@ function manualCloudDeviceCard(device) {
|
||||
${compressorQueuePanel(managedZone)}
|
||||
${modes.length ? `<div class="mode-row quick-control-row quick-control-row-5">${modes.map(mode => `<button class="${device.mode === mode ? 'active' : ''}" data-action="mode" data-value="${mode}" data-device="${esc(device.id)}">${esc(modeLabel(mode))}</button>`).join('')}</div>` : ''}
|
||||
${fans.length ? `<div class="fan-row quick-control-row quick-control-row-${Math.min(6, fans.length)}">${fans.map(fan => `<button class="${Number(device.fan_speed) === fan ? 'active' : ''}" data-action="fan" data-value="${fan}" data-device="${esc(device.id)}">${esc(fanLabel(fan))}</button>`).join('')}</div>` : ''}
|
||||
<div class="device-toggles quick-control-row quick-control-row-3">
|
||||
${caps.vertical_swing === false ? '' : `<button class="${device.swing_vertical ? 'active' : ''}" data-action="toggle" data-field="swing_vertical" data-device="${esc(device.id)}">${esc(tr('devices.swing'))}</button>`}
|
||||
<div class="device-toggles quick-control-row quick-control-row-4">
|
||||
${caps.vertical_swing === false ? '' : `<button class="${device.swing_vertical ? 'active' : ''}" data-action="toggle" data-field="swing_vertical" data-device="${esc(device.id)}">${esc(tr('devices.swingVertical'))}</button>`}
|
||||
${caps.horizontal_swing === false ? '' : `<button class="${device.swing_horizontal ? 'active' : ''}" data-action="toggle" data-field="swing_horizontal" data-device="${esc(device.id)}">${esc(tr('devices.swingHorizontal'))}</button>`}
|
||||
${device.supports_quiet === false ? '' : `<button class="${device.quiet ? 'active' : ''}" data-action="toggle" data-field="quiet" data-device="${esc(device.id)}">${esc(tr('devices.quiet'))}</button>`}
|
||||
${device.supports_turbo === false ? '' : `<button class="${device.turbo ? 'active' : ''}" data-action="toggle" data-field="turbo" data-device="${esc(device.id)}">${esc(tr('devices.turbo'))}</button>`}
|
||||
</div>
|
||||
@@ -246,8 +279,16 @@ function renderDeviceInstallationsSummary() {
|
||||
const groups = app.deviceGroups || [];
|
||||
host.innerHTML = groups.length ? groups.map(group => {
|
||||
const members = (group.device_ids || []).map(id => app.devices.find(device => device.id === id)?.name).filter(Boolean);
|
||||
const outdoor = app.devices.find(device => device.id === group.outdoor_temperature_device_id)?.name;
|
||||
return `<article class="installation-summary-card"><div><span class="eyebrow">${esc(deviceInstallationKindLabel(group))}</span><strong>${esc(group.name)}</strong><small>${esc(members.join(' · ') || '—')}</small></div><div><span>${esc(tr('energy.source'))}</span><b>${esc(installationEnergySourceLabel(group))}</b>${outdoor ? `<small>${esc(tr('devices.outdoorSourceDevice'))}: ${esc(outdoor)}</small>` : ''}</div><button type="button" class="secondary" data-action="edit-device-group" data-id="${esc(group.id)}">${esc(tr('actions.edit'))}</button></article>`;
|
||||
const outdoorDevice = app.devices.find(device => device.id === group.outdoor_temperature_device_id);
|
||||
const outdoorValue = outdoorDevice?.outdoor_temperature == null ? NaN : Number(outdoorDevice.outdoor_temperature);
|
||||
const energy = installationEnergySnapshot(group);
|
||||
const sourceTitle = installationEnergySourceLabel(group);
|
||||
const sourceShort = installationEnergySourceShortLabel(group, energy);
|
||||
const lastReading = energy?.timestamp ? `${tr('energy.lastReading')}: ${dateTime(energy.timestamp)}` : '';
|
||||
const outdoor = outdoorDevice
|
||||
? `${tr('devices.sharedOutdoorMetric')}: ${Number.isFinite(outdoorValue) ? fmtTemp(outdoorValue) : tr('common.unavailable')} · ${outdoorDevice.name}`
|
||||
: '';
|
||||
return `<article class="installation-summary-card"><div><span class="eyebrow">${esc(deviceInstallationKindLabel(group))}</span><strong>${esc(group.name)}</strong><small>${esc(members.join(' · ') || '—')}</small></div><div class="installation-energy-metric"><span>${esc(tr('energy.totalMeter'))}</span><b title="${esc(sourceTitle)}">${esc(formatEnergyMeterTotal(energy?.total_kwh))}</b><small>${esc(sourceShort)}${lastReading ? ` · ${esc(lastReading)}` : ''}</small>${outdoor ? `<small>${esc(outdoor)}</small>` : ''}</div><button type="button" class="secondary" data-action="edit-device-group" data-id="${esc(group.id)}">${esc(tr('actions.edit'))}</button></article>`;
|
||||
}).join('') : '';
|
||||
}
|
||||
|
||||
@@ -519,6 +560,7 @@ function zoneCard(zone, detailed = true) {
|
||||
${['auto', 'comfort', 'sleep', 'away'].map(preset => `<button class="${manual === preset ? 'active' : ''}" data-action="zone-preset" data-id="${esc(zone.id)}" data-value="${preset}">${esc(preset === 'sleep' ? tr('zones.sleepNow') : zonePresetLabel(preset))}</button>`).join('')}
|
||||
</div>
|
||||
<div class="mode-row zone-mode-row quick-control-row quick-control-row-3"><button class="${mode === 'house' && globalModeAvailable ? 'active' : ''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="house"${globalModeDisabled}>${esc(tr('zones.followHouseShort'))}</button><button class="${mode === 'heat' ? 'active' : ''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="heat">${esc(modeLabel('heat'))}</button><button class="${mode === 'cool' ? 'active' : ''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="cool">${esc(modeLabel('cool'))}</button></div>
|
||||
${device ? `<div class="device-toggles thermostat-swing-row quick-control-row quick-control-row-2">${device.capabilities?.vertical_swing === false ? '' : `<button class="${device.swing_vertical ? 'active' : ''}" data-action="zone-swing" data-field="swing_vertical" data-device="${esc(device.id)}"${deviceUnavailable ? ' disabled' : ''}>${esc(tr('devices.swingVertical'))}</button>`}${device.capabilities?.horizontal_swing === false ? '' : `<button class="${device.swing_horizontal ? 'active' : ''}" data-action="zone-swing" data-field="swing_horizontal" data-device="${esc(device.id)}"${deviceUnavailable ? ' disabled' : ''}>${esc(tr('devices.swingHorizontal'))}</button>`}</div>` : ''}
|
||||
<div class="zone-state-line"><span class="zone-runtime-status" title="${esc(runtimeStatus)}">${esc(runtimeStatus)}${controlGroup ? ` <b class="group-control-tag">${esc(controlGroup.name)}</b>` : ''}</span><span>${esc(override)}</span></div>
|
||||
<div class="zone-state-line control-owner-line"><span><strong>${esc(tr('zones.controlOwner'))}:</strong> ${esc(zoneControlOwnerLabel(zone))}</span><span>${esc(zoneControlOwnerMeta(zone))}</span></div>
|
||||
${compressorQueuePanel(zone)}
|
||||
|
||||
+2
-1
@@ -179,6 +179,7 @@ document.addEventListener('click', async event => {
|
||||
if (action === 'cancel-compressor-task') return cancelCompressorTask(button.dataset.id);
|
||||
if (action === 'cancel-all-compressor-tasks') { if (!confirm(tr('zones.queueCancelAllConfirm', { count: app.zones.filter(zone => !!zone.compressor_pending_action).length }))) return; return cancelAllCompressorTasks(); }
|
||||
if (action === 'zone-device-power') return sendZoneLocalPower(button.dataset.id, button.dataset.value === 'true');
|
||||
if (action === 'zone-swing' && device) return sendDeviceCommand(device.id, current => ({ [button.dataset.field]: !current?.[button.dataset.field] }));
|
||||
if (action === 'zone-open-temporary') return populateTemporaryThermostat(button.dataset.id);
|
||||
if (action === 'zone-temperature') { const zone = app.zones.find(v => v.id === button.dataset.id); if (zone) { const base = Number(zone.manual_setpoint ?? zone.effective_setpoint ?? zone.setpoint); queueZoneTemperature(zone, base + Number(button.dataset.delta)); } return; }
|
||||
if (action === 'zone-mode') return sendZoneControl(button.dataset.id, { mode: button.dataset.value });
|
||||
@@ -614,7 +615,7 @@ $('#scheduleForm').addEventListener('submit', async event => {
|
||||
$('#automationForm').addEventListener('submit', async event => {
|
||||
event.preventDefault(); const form = event.currentTarget, raw = Object.fromEntries(new FormData(form)); const id = raw.id;
|
||||
const groupTarget = raw.action_target_kind === 'group';
|
||||
const action = {}; if (raw.action_power !== '') action.power = raw.action_power === 'true'; if (raw.action_mode) action.mode = raw.action_mode; if (!groupTarget && raw.action_target_temperature !== '') action.target_temperature = parseDecimal(raw.action_target_temperature);
|
||||
const action = {}; if (raw.action_power !== '') action.power = raw.action_power === 'true'; if (raw.action_mode) action.mode = raw.action_mode; if (!groupTarget && raw.action_target_temperature !== '') action.target_temperature = parseDecimal(raw.action_target_temperature); if (!groupTarget && raw.action_swing_vertical !== '') action.swing_vertical = raw.action_swing_vertical === 'true'; if (!groupTarget && raw.action_swing_horizontal !== '') action.swing_horizontal = raw.action_swing_horizontal === 'true';
|
||||
const body = { name: raw.name, enabled: form.enabled.checked, trigger_kind: raw.trigger_kind, trigger_device_id: raw.trigger_device_id || null, threshold: raw.threshold === '' ? null : parseDecimal(raw.threshold), at_time: raw.at_time || null, action_device_id: groupTarget ? '' : raw.action_device_id, action_group_id: groupTarget ? (raw.action_group_id || null) : null, action_preset: groupTarget ? (raw.action_preset || null) : null, action, cooldown_seconds: Number(raw.cooldown_seconds) };
|
||||
await runFormTask(form, async () => {
|
||||
await api(id ? `/api/automations/${encodeURIComponent(id)}` : '/api/automations', { method: id ? 'PUT' : 'POST', body }); form.closest('dialog').close(); form.reset(); await loadBootstrap(); toast(tr('common.saved'));
|
||||
|
||||
+5
-5
@@ -87,7 +87,7 @@ function flowDefaultConfig(kind) {
|
||||
if (kind === 'night_mode') return {};
|
||||
if (kind === 'constant') return { value: true };
|
||||
if (kind === 'shared_input') return sharedFlowReferenceDefaultConfig(app.flowSharedInputs?.[0]);
|
||||
if (kind === 'zone_thermostat') return { zone_id: app.zones[0]?.id || '', preset: 'comfort', setpoint: 21, mode: 'auto', cooldown_seconds: 60 };
|
||||
if (kind === 'zone_thermostat') return { zone_id: app.zones[0]?.id || '', preset: 'comfort', setpoint: 21, mode: 'auto', power: null, swing_vertical: null, swing_horizontal: null, cooldown_seconds: 60 };
|
||||
if (kind === 'device_action') return { device_id: app.devices[0]?.id || '', power: true, mode: '', target_temperature: null, fan_speed: null, swing_vertical: null, swing_horizontal: null, quiet: null, turbo: null, light: null, air: null, xfan: null, health: null, sleep: null, cooldown_seconds: 60 };
|
||||
if (kind === 'group_action') return { group_id: app.groups[0]?.id || '', power: true, mode: '', preset: '', setpoint: 21, cooldown_seconds: 60 };
|
||||
if (kind === 'ha_service_action') return { domain: 'switch', service: 'turn_on', entity_id: '', data: {}, cooldown_seconds: 60 };
|
||||
@@ -516,7 +516,7 @@ function renderFlowInspector() {
|
||||
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 === '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(v)}</option>`).join('')}</select></label>${flowTextComparisonFields(c)}`;
|
||||
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)}`;
|
||||
else if (node.kind === 'group_state') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-flow-config="group_id">${flowSelectOptions(app.groups, c.group_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-flow-config="field"><option value="power_enabled" selected>power_enabled</option></select></label>${flowTextComparisonFields(c)}`;
|
||||
else if (node.kind === 'night_mode') fields = `<p class="field-note">${esc(tr('flow.nightModeHint'))}</p>`;
|
||||
@@ -530,7 +530,7 @@ function renderFlowInspector() {
|
||||
else if (node.kind === 'logic_and') fields = `<p class="field-note">${esc(tr('flow.andHint'))}</p>`;
|
||||
else if (node.kind === 'logic_or') fields = `<p class="field-note">${esc(tr('flow.orHint'))}</p>`;
|
||||
else if (node.kind === 'logic_not') fields = `<p class="field-note">${esc(tr('flow.notHint'))}</p>`;
|
||||
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><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 === '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>`;
|
||||
@@ -540,7 +540,7 @@ function renderFlowInspector() {
|
||||
function flowTextComparisonFields(c, numeric = false) { const options = numeric ? flowOperatorOptions(c.operator || 'eq') : [['eq', '='], ['neq', '≠']].map(([v, l]) => `<option value="${v}" ${v === (c.operator || 'eq') ? 'selected' : ''}>${l}</option>`).join(''); return `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${options}</select></label><label><span>${esc(tr('flow.value'))}</span><input data-flow-config="value" value="${esc(c.value ?? '')}"></label></div>`; }
|
||||
function flowComparisonFields(c, temperature = true) { return `<div class="two"><label><span>${esc(tr('flow.operator'))}</span><select data-flow-config="operator">${flowOperatorOptions(c.operator || 'lt')}</select></label><label><span>${esc(temperature ? tr('automations.thresholdC') : tr('flow.value'))}</span><input type="number" step="0.1" data-flow-config="value" value="${Number(c.value ?? 0)}"></label></div>`; }
|
||||
function flowOptionalBoolField(c, key) {
|
||||
return `<label><span>${esc(key)}</span><select data-flow-config="${esc(key)}"><option value="" ${c[key] == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option><option value="true" ${c[key] === true ? 'selected' : ''}>${esc(tr('common.on'))}</option><option value="false" ${c[key] === false ? 'selected' : ''}>${esc(tr('common.off'))}</option></select></label>`;
|
||||
return `<label><span>${esc(deviceCommandFieldLabel(key))}</span><select data-flow-config="${esc(key)}"><option value="" ${c[key] == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option><option value="true" ${c[key] === true ? 'selected' : ''}>${esc(tr('common.on'))}</option><option value="false" ${c[key] === false ? 'selected' : ''}>${esc(tr('common.off'))}</option></select></label>`;
|
||||
}
|
||||
function flowActionFields(c, group) {
|
||||
const modes = group ? ['auto', 'house', 'heat', 'cool'] : ['auto', 'cool', 'dry', 'fan', 'heat'];
|
||||
@@ -549,7 +549,7 @@ function flowActionFields(c, group) {
|
||||
const target = group
|
||||
? `<label><span>${esc(tr('groups.profile'))}</span><select data-flow-config="preset"><option value="">${esc(tr('actions.noChange'))}</option>${['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="${c.setpoint ?? 21}"></label>`
|
||||
: `<label><span>${esc(tr('common.targetC'))}</span><input type="number" min="8" max="30" step="0.5" data-flow-config="target_temperature" value="${c.target_temperature ?? ''}"></label>`;
|
||||
const deviceOptions = group ? '' : `<details class="flow-device-options"><summary>${esc(tr('flow.deviceOptions'))}</summary><div class="two"><label><span>fan_speed</span><select data-flow-config="fan_speed"><option value="" ${c.fan_speed == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option>${[0, 1, 2, 3, 4, 5].map(v => `<option value="${v}" ${Number(c.fan_speed) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label>${flowOptionalBoolField(c, 'swing_vertical')}${flowOptionalBoolField(c, 'swing_horizontal')}${flowOptionalBoolField(c, 'quiet')}${flowOptionalBoolField(c, 'turbo')}${flowOptionalBoolField(c, 'light')}${flowOptionalBoolField(c, 'air')}${flowOptionalBoolField(c, 'xfan')}${flowOptionalBoolField(c, 'health')}${flowOptionalBoolField(c, 'sleep')}</div><p class="field-note">${esc(tr('flow.deviceOptionsHint'))}</p></details>`;
|
||||
const deviceOptions = group ? '' : `<details class="flow-device-options"><summary>${esc(tr('flow.deviceOptions'))}</summary><div class="two"><label><span>${esc(deviceCommandFieldLabel('fan_speed'))}</span><select data-flow-config="fan_speed"><option value="" ${c.fan_speed == null ? 'selected' : ''}>${esc(tr('actions.noChange'))}</option>${[0, 1, 2, 3, 4, 5].map(v => `<option value="${v}" ${Number(c.fan_speed) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label>${flowOptionalBoolField(c, 'swing_vertical')}${flowOptionalBoolField(c, 'swing_horizontal')}${flowOptionalBoolField(c, 'quiet')}${flowOptionalBoolField(c, 'turbo')}${flowOptionalBoolField(c, 'light')}${flowOptionalBoolField(c, 'air')}${flowOptionalBoolField(c, 'xfan')}${flowOptionalBoolField(c, 'health')}${flowOptionalBoolField(c, 'sleep')}</div><p class="field-note">${esc(tr('flow.deviceOptionsHint'))}</p></details>`;
|
||||
return `${base}${target}${deviceOptions}<label><span>${esc(tr('common.cooldownSeconds'))}</span><input type="number" min="30" data-flow-config="cooldown_seconds" value="${Number(c.cooldown_seconds || 60)}"></label>`;
|
||||
}
|
||||
|
||||
|
||||
+49
-3
@@ -1,3 +1,31 @@
|
||||
function outdoorHistorySeries(devices, rows) {
|
||||
const selectedIds = new Set((devices || []).map(device => device.id));
|
||||
const handled = new Set();
|
||||
const definitions = [];
|
||||
|
||||
(app.deviceGroups || []).forEach(group => {
|
||||
if (!group.outdoor_temperature_device_id) return;
|
||||
const members = (group.device_ids || []).filter(id => selectedIds.has(id));
|
||||
if (!members.length) return;
|
||||
const representative = members.includes(group.outdoor_temperature_device_id) ? group.outdoor_temperature_device_id : members[0];
|
||||
if (!rows.some(row => row.device_id === representative && Number.isFinite(historyNumber(row.outdoor_temperature)))) return;
|
||||
members.forEach(id => handled.add(id));
|
||||
definitions.push({ representative, label: `${group.name} · ${tr('history.sharedOutdoor')}` });
|
||||
});
|
||||
|
||||
(devices || []).filter(device => !handled.has(device.id)).forEach(device => {
|
||||
if (rows.some(row => row.device_id === device.id && Number.isFinite(historyNumber(row.outdoor_temperature)))) {
|
||||
definitions.push({ representative: device.id, label: device.name });
|
||||
}
|
||||
});
|
||||
|
||||
return definitions.map((item, index) => ({
|
||||
label: item.label,
|
||||
color: historySeriesColor(index),
|
||||
value: row => row.device_id === item.representative ? historyNumber(row.outdoor_temperature) : NaN,
|
||||
}));
|
||||
}
|
||||
|
||||
function renderHistorySummary() {
|
||||
const host = $('#historySummary'); if (!host) return;
|
||||
if (app.historyTab === 'energy') { renderEnergyHistorySummary(); return; }
|
||||
@@ -20,7 +48,7 @@ function renderOverviewHistory() {
|
||||
const indoorSeries = app.devices.map((device, index) => ({ label: device.name, color: historySeriesColor(index), value: row => !row.zone_id && !row.entity_id && row.device_id === device.id ? historyNumber(row.indoor_temperature) : NaN }));
|
||||
drawLineChart($('#overviewIndoorChart'), indoorSeries, deviceRows, { height: 360 }); renderLegend($('#overviewIndoorChartLegend'), indoorSeries);
|
||||
|
||||
const outdoorDeviceSeries = app.devices.filter(device => deviceRows.some(row => row.device_id === device.id && Number.isFinite(historyNumber(row.outdoor_temperature)))).map((device, index) => ({ label: `${device.name} · ${tr('history.greeOutdoor')}`, color: historySeriesColor(index), value: row => !row.zone_id && !row.entity_id && row.device_id === device.id ? historyNumber(row.outdoor_temperature) : NaN }));
|
||||
const outdoorDeviceSeries = outdoorHistorySeries(app.devices, deviceRows).map(item => ({ ...item, label: item.label.includes(' · ') ? item.label : `${item.label} · ${tr('history.greeOutdoor')}` }));
|
||||
const outdoorEntities = [...new Set(app.historyData.sensors.filter(row => row.kind === 'outdoor').map(row => row.entity_id))];
|
||||
const outdoorHaSeries = outdoorEntities.map((entity, index) => ({ label: `HA · ${haSensorLabel(entity)}`, color: historySeriesColor(index + outdoorDeviceSeries.length), dash: [6, 4], value: row => row.entity_id === entity ? historyNumber(row.temperature) : NaN }));
|
||||
const outdoorRows = [...deviceRows, ...app.historyData.sensors.filter(row => row.kind === 'outdoor')];
|
||||
@@ -68,7 +96,7 @@ function renderDeviceHistory() {
|
||||
host.innerHTML = historyChartMarkup('deviceIndoorChart', tr('history.deviceIndoor'), tr('history.deviceIndoorHint')) + historyChartMarkup('deviceOutdoorChart', tr('history.deviceOutdoor'), tr('history.deviceOutdoorHint')) + historyChartMarkup('deviceTargetChart', tr('history.deviceTargets'), tr('history.deviceTargetsHint'), true);
|
||||
const devices = selected === 'all' ? app.devices : app.devices.filter(device => device.id === selected);
|
||||
const indoor = devices.map((device, index) => ({ label: device.name, color: historySeriesColor(index), value: row => row.device_id === device.id ? historyNumber(row.indoor_temperature) : NaN }));
|
||||
const outdoor = devices.filter(device => rows.some(row => row.device_id === device.id && Number.isFinite(historyNumber(row.outdoor_temperature)))).map((device, index) => ({ label: device.name, color: historySeriesColor(index), value: row => row.device_id === device.id ? historyNumber(row.outdoor_temperature) : NaN }));
|
||||
const outdoor = outdoorHistorySeries(devices, rows);
|
||||
const targets = devices.map((device, index) => ({ label: device.name, color: historySeriesColor(index), dash: [6, 4], value: row => row.device_id === device.id ? historyNumber(row.target_temperature) : NaN }));
|
||||
drawLineChart($('#deviceIndoorChart'), indoor, rows, { height: 350 }); renderLegend($('#deviceIndoorChartLegend'), indoor);
|
||||
drawLineChart($('#deviceOutdoorChart'), outdoor, rows, { height: 310 }); renderLegend($('#deviceOutdoorChartLegend'), outdoor);
|
||||
@@ -87,9 +115,19 @@ function renderSensorHistory() {
|
||||
|
||||
function customSeriesOptions() {
|
||||
const items = [];
|
||||
const groupedOutdoorIds = new Set();
|
||||
(app.deviceGroups || []).forEach(group => {
|
||||
if (!group.outdoor_temperature_device_id) return;
|
||||
const representative = (group.device_ids || []).includes(group.outdoor_temperature_device_id)
|
||||
? group.outdoor_temperature_device_id
|
||||
: (group.device_ids || [])[0];
|
||||
if (!representative) return;
|
||||
(group.device_ids || []).forEach(id => groupedOutdoorIds.add(id));
|
||||
items.push([`installation|${group.id}|outdoor`, `${group.name} · ${tr('history.sharedOutdoor')}`]);
|
||||
});
|
||||
app.devices.forEach(device => {
|
||||
items.push([`device|${device.id}|indoor`, `${device.name} · ${tr('history.indoorTemperature')}`]);
|
||||
items.push([`device|${device.id}|outdoor`, `${device.name} · ${tr('history.greeOutdoor')}`]);
|
||||
if (!groupedOutdoorIds.has(device.id)) items.push([`device|${device.id}|outdoor`, `${device.name} · ${tr('history.greeOutdoor')}`]);
|
||||
items.push([`device|${device.id}|target`, `${device.name} · ${tr('history.deviceTarget')}`]);
|
||||
});
|
||||
app.zones.forEach(zone => {
|
||||
@@ -113,6 +151,14 @@ function customSeriesDefinition(key, index = 0) {
|
||||
const fields = { indoor: 'indoor_temperature', outdoor: 'outdoor_temperature', target: 'target_temperature' };
|
||||
return { key, label: `${device.name} · ${labels[field] || field}`, color, dash: field === 'target' ? [6, 4] : [], value: row => !row.zone_id && !row.entity_id && row.device_id === id ? historyNumber(row[fields[field]]) : NaN };
|
||||
}
|
||||
if (kind === 'installation' && field === 'outdoor') {
|
||||
const group = (app.deviceGroups || []).find(item => item.id === id); if (!group) return null;
|
||||
const representative = (group.device_ids || []).includes(group.outdoor_temperature_device_id)
|
||||
? group.outdoor_temperature_device_id
|
||||
: (group.device_ids || [])[0];
|
||||
if (!representative) return null;
|
||||
return { key, label: `${group.name} · ${tr('history.sharedOutdoor')}`, color, value: row => !row.zone_id && !row.entity_id && row.device_id === representative ? historyNumber(row.outdoor_temperature) : NaN };
|
||||
}
|
||||
if (kind === 'zone') {
|
||||
const zone = app.zones.find(item => item.id === id); if (!zone) return null;
|
||||
const fields = { control: ['control_temperature', tr('history.controlTemperature')], gree: ['gree_temperature', tr('history.greeSensor')], external: ['external_temperature', tr('history.roomSensor')], target: ['target_temperature', tr('history.comfortTarget')], device_target: ['device_setpoint', tr('history.deviceSetpoint')], outdoor: ['outdoor_temperature', tr('history.outdoorTemperature')] };
|
||||
|
||||
+10
-1
@@ -540,11 +540,18 @@ function updateAutomationTargetFields() {
|
||||
$('#automationGroupTarget').hidden = !groupTarget;
|
||||
$('#automationGroupPreset').hidden = !groupTarget;
|
||||
$('#automationTargetTemperature').hidden = groupTarget;
|
||||
$('#automationDeviceSwingOptions').hidden = groupTarget;
|
||||
$('#automationGroupHint').hidden = !groupTarget;
|
||||
form.action_device_id.required = !groupTarget;
|
||||
form.action_group_id.required = groupTarget;
|
||||
form.action_target_temperature.disabled = groupTarget;
|
||||
if (groupTarget) form.action_target_temperature.value = '';
|
||||
form.action_swing_vertical.disabled = groupTarget;
|
||||
form.action_swing_horizontal.disabled = groupTarget;
|
||||
if (groupTarget) {
|
||||
form.action_target_temperature.value = '';
|
||||
form.action_swing_vertical.value = '';
|
||||
form.action_swing_horizontal.value = '';
|
||||
}
|
||||
[...form.action_mode.options].forEach(option => {
|
||||
if (!['dry', 'fan'].includes(option.value)) return;
|
||||
option.disabled = groupTarget;
|
||||
@@ -688,6 +695,8 @@ function populateAutomation(id) {
|
||||
form.action_mode.value = item.action.mode || '';
|
||||
form.action_preset.value = item.action_preset || '';
|
||||
form.action_target_temperature.value = item.action.target_temperature ?? '';
|
||||
form.action_swing_vertical.value = item.action.swing_vertical == null ? '' : String(item.action.swing_vertical);
|
||||
form.action_swing_horizontal.value = item.action.swing_horizontal == null ? '' : String(item.action.swing_horizontal);
|
||||
form.enabled.checked = item.enabled;
|
||||
updateAutomationTargetFields();
|
||||
openDialog('automationDialog');
|
||||
|
||||
@@ -48,6 +48,25 @@ async function handleWebSocketMessage(event) {
|
||||
else if (message.event === 'group.deleted') { app.groups = app.groups.filter(v => v.id !== data.id); renderGroups(); fillSelects(); scheduleControlPlanLoad(); }
|
||||
else if (['device_group.updated', 'device_group.created'].includes(message.event)) { const i = app.deviceGroups.findIndex(v => v.id === data.id); if (i >= 0) app.deviceGroups[i] = data; else app.deviceGroups.push(data); renderDevices(); renderHistoryNavigation(); }
|
||||
else if (message.event === 'device_group.deleted') { app.deviceGroups = app.deviceGroups.filter(v => v.id !== data.id); renderDevices(); renderHistoryNavigation(); }
|
||||
else if (message.event === 'energy.updated') {
|
||||
const total = Number(data.total_kwh);
|
||||
if (Number.isFinite(total)) {
|
||||
const snapshot = { total_kwh: total, timestamp: data.timestamp || message.timestamp, source: data.source || '', origin: 'realtime' };
|
||||
const selectedEnergySource = group => group.energy_source === 'gree_cloud' || (group.energy_source === 'auto' && !!group.energy_device_id)
|
||||
? 'gree_cloud'
|
||||
: (group.energy_source === 'home_assistant' || (group.energy_source === 'auto' && !!group.ha_energy_entity_id) ? 'home_assistant' : '');
|
||||
if (String(data.target_id || '').startsWith('group:')) {
|
||||
const groupId = String(data.target_id).slice(6);
|
||||
const group = (app.deviceGroups || []).find(item => item.id === groupId);
|
||||
if (group && selectedEnergySource(group) === snapshot.source) app.deviceGroupEnergy[groupId] = snapshot;
|
||||
} else if (snapshot.source === 'gree_cloud') {
|
||||
(app.deviceGroups || [])
|
||||
.filter(group => group.energy_device_id === data.target_id && selectedEnergySource(group) === 'gree_cloud')
|
||||
.forEach(group => { app.deviceGroupEnergy[group.id] = snapshot; });
|
||||
}
|
||||
renderDevices();
|
||||
}
|
||||
}
|
||||
else if (['schedule.updated', 'schedule.created'].includes(message.event)) { const i = app.schedules.findIndex(v => v.id === data.id); if (i >= 0) app.schedules[i] = data; else app.schedules.push(data); renderSchedules(); fillSelects(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'schedule.deleted') { app.schedules = app.schedules.filter(v => v.id !== data.id); renderSchedules(); fillSelects(); scheduleControlPlanLoad(); }
|
||||
else if (message.event === 'schedule.template_applied') { try { app.schedules = await api('/api/schedules'); renderSchedules(); fillSelects(); } catch (_) { } scheduleControlPlanLoad(); }
|
||||
|
||||
@@ -119,7 +119,7 @@ function renderFlowSharedInputFields(kind, config = {}) {
|
||||
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>`;
|
||||
else if (kind === 'house_mode') fields = `<p class="field-note">${esc(tr('flow.sharedInputSourceOnlyHint'))}</p>`;
|
||||
else if (kind === 'device_state') fields = `<label><span>${esc(tr('common.device'))}</span><select data-shared-config="device_id">${sharedFlowOptions(app.devices, c.device_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-shared-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(v)}</option>`).join('')}</select></label>`;
|
||||
else if (kind === 'device_state') fields = `<label><span>${esc(tr('common.device'))}</span><select data-shared-config="device_id">${sharedFlowOptions(app.devices, c.device_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-shared-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>`;
|
||||
else if (kind === 'zone_state') fields = `<label><span>${esc(tr('common.zone'))}</span><select data-shared-config="zone_id">${sharedFlowOptions(app.zones, c.zone_id)}</select></label><label><span>${esc(tr('flow.field'))}</span><select data-shared-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>`;
|
||||
else if (kind === 'group_state') fields = `<label><span>${esc(tr('groups.group'))}</span><select data-shared-config="group_id">${sharedFlowOptions(app.groups, c.group_id)}</select></label><input type="hidden" data-shared-config="field" value="power_enabled">`;
|
||||
else if (kind === 'night_mode') fields = `<p class="field-note">${esc(tr('flow.nightModeHint'))}</p>`;
|
||||
|
||||
+8
-1
@@ -321,7 +321,14 @@ document.addEventListener('change', event => {
|
||||
const target = event.target;
|
||||
if (target.id === 'historyZoneSelect') { app.historyZone = target.value; updateBrowserUrl(currentHistoryPath()); renderHistoryPage(); }
|
||||
else if (target.id === 'historyDeviceSelect') { app.historyDevice = target.value; updateBrowserUrl(currentHistoryPath()); renderHistoryPage(); }
|
||||
else if (target.id === 'historyEnergyTargetSelect') { app.historyEnergyTargets = [...target.selectedOptions].map(option => option.value).slice(0, 8); app.historyEnergyDevice = app.historyEnergyTargets[0] || ''; updateBrowserUrl(currentHistoryPath()); loadHistory(); }
|
||||
else if (target.matches?.('[data-history-energy-target]')) {
|
||||
const checked = $$('[data-history-energy-target]:checked').map(input => input.dataset.historyEnergyTarget).filter(Boolean);
|
||||
if (checked.length > 8) { target.checked = false; toast(tr('energy.maxTargets'), true); return; }
|
||||
app.historyEnergyTargets = checked;
|
||||
app.historyEnergyDevice = app.historyEnergyTargets[0] || '';
|
||||
updateBrowserUrl(currentHistoryPath());
|
||||
loadHistory();
|
||||
}
|
||||
else if (target.id === 'historyEnergyInterval') { app.historyEnergyInterval = target.value; updateBrowserUrl(currentHistoryPath()); loadHistory(); }
|
||||
else if (target.id === 'historyEnergyCompare') { app.historyEnergyCompare = target.value; updateBrowserUrl(currentHistoryPath()); loadHistory(); }
|
||||
else if (target.id === 'historySensorSelect') { app.historySensor = target.value; updateBrowserUrl(currentHistoryPath()); renderHistoryPage(); }
|
||||
|
||||
+80
-2
@@ -2087,6 +2087,7 @@ legend {
|
||||
|
||||
.history-toolbar-panel {
|
||||
margin-bottom: 14px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.history-context-controls {
|
||||
@@ -2777,6 +2778,10 @@ legend {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.history-toolbar-panel .chart-toolbar.energy-toolbar {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.history-context-controls {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -9518,13 +9523,86 @@ body.flow-editor-open {
|
||||
.installation-device-choices { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; margin-top: 6px; }
|
||||
.installation-device-choices .check { margin: 0; padding: 8px 9px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface); }
|
||||
.installation-device-choices .check.disabled { opacity: .55; }
|
||||
.history-energy-targets select { min-width: min(360px, 72vw); }
|
||||
.history-energy-targets small { display: block; max-width: 360px; color: var(--muted); font-size: 10px; }
|
||||
.installation-energy-metric b {
|
||||
color: var(--text);
|
||||
font-size: 18px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -.01em;
|
||||
}
|
||||
.history-context-controls.energy-context-controls {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 1.5fr) repeat(3, minmax(132px, .8fr));
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
}
|
||||
.history-energy-targets { display: grid; gap: 7px; min-width: 0; color: var(--muted); font-size: 12px; font-weight: 650; }
|
||||
.history-energy-targets small,
|
||||
.history-range-control small { display: block; color: var(--muted); font-size: 10px; font-weight: 550; line-height: 1.35; }
|
||||
.history-energy-picker { position: relative; min-width: 0; }
|
||||
.history-energy-picker > summary {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 45px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--input-bg);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
.history-energy-picker > summary::-webkit-details-marker { display: none; }
|
||||
.history-energy-picker > summary span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.history-energy-picker > summary b {
|
||||
min-width: 34px;
|
||||
padding: 3px 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--surface-muted);
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.history-energy-picker[open] > summary { border-color: var(--accent); outline: 2px solid color-mix(in srgb, var(--accent) 18%, transparent); outline-offset: 1px; }
|
||||
.history-energy-options {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
width: min(420px, calc(100vw - 40px));
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
padding: 7px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--surface);
|
||||
box-shadow: 0 14px 34px rgba(0, 0, 0, .22);
|
||||
}
|
||||
.history-energy-option {
|
||||
display: grid;
|
||||
grid-template-columns: 20px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
min-height: 38px;
|
||||
padding: 7px 8px;
|
||||
border-radius: 8px;
|
||||
color: var(--text-soft);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.history-energy-option:hover { background: var(--surface-muted); }
|
||||
.history-energy-option input { width: 17px; min-height: 17px; margin: 0; padding: 0; accent-color: var(--accent); }
|
||||
.history-energy-option span { overflow-wrap: anywhere; }
|
||||
.history-range-control.energy-period-control { min-width: 0; }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.installation-summary-card,
|
||||
.installation-list-row { grid-template-columns: 1fr; }
|
||||
.installation-list-row > div { justify-content: flex-start; }
|
||||
.installation-device-choices { grid-template-columns: 1fr; }
|
||||
.history-context-controls.energy-context-controls { grid-template-columns: 1fr; }
|
||||
.history-energy-options { width: min(420px, calc(100vw - 40px)); max-height: 46vh; }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user