v0.14.2-connectivity
This commit is contained in:
+41
-39
@@ -1,14 +1,14 @@
|
||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||
80472072ffaa0e14ef6fd28b02f757d1947fc656687fef19fa96b58b5f13baa5 ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
59fe6d7de7f780481d0a7f614d4325f905a8aee8d971717566fc8889340223b3 ./Cargo.lock
|
||||
97b67fd716185b18bb12d9170b502f81c6ffaf099816c893305c9f837048cb1f ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
de56b105caea31afd80b4051912329a0c1c03233979fd710732cb606acae1dbd ./README.md
|
||||
ebee04f56d144f3d9d8d385715a2a48480ab662d046b675594b3cf5c850a812a ./SOURCE_MANIFEST.sha256
|
||||
ee65b2f48b4cc5553e01689ace0f33151bbb2a2738f07ff7918cbf033b6a0f4d ./README.md
|
||||
581d151a15a9cfc19f350f9059afc00a0533baa6eee9d4f3c993d5390ee2a2a9 ./SOURCE_MANIFEST.sha256
|
||||
bb549919b96b0413152dd905aec8640bb53301f07ec7d0b1be48ccbd37936fef ./build.rs
|
||||
0eb39057762c51718e7e7ba220ae1e3a729409c3d91736afa3baa88c64d3a8d6 ./docs/API.md
|
||||
40199fea4917f9278344780440b0f47ca3ed5c6e7873e56040e16519edac7834 ./docs/API.md
|
||||
ff213ad348e7f8b5cae7fe4adcad84d72db693765d424f270faa1acca9c23ce7 ./docs/FLOW.md
|
||||
52181be4d4a161710bbb5e7cde1b3061976e41691d9e10bf99f0c224c65732d1 ./docs/openapi.json
|
||||
be8879ede6b807450dd79af22ad739f4089d0ce0b04f66a8bc00ada685789506 ./docs/openapi.json
|
||||
c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
|
||||
a18d2460340264d34c267fa704ee51a42e59bcc1783ee4c0dbb2a8ef42fa0f16 ./ha-addon/Dockerfile
|
||||
0417c88a990c72aecf071fba64d5d9da65dcda4d396cd7565477f7a4532c8c5e ./ha-addon/README.md
|
||||
@@ -30,7 +30,7 @@ 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
|
||||
e561712958ce5ef1068989af0515687358adc69b1d92af29d6b576d46a182859 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
da2dec2201336a2fe91604e19b85873729ad0889fc310fa1da48708a6ebc0898 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
3c06bd11671452ea6472afd5543fb91a39aba73a1d2d95c7762c04f94ab64030 ./ha-addon/repository/gree-controller/DOCS.md
|
||||
c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md
|
||||
a3a74b2aaa20fd39a050f8f360eec543d70b3fd43271617a60a0b95fd0cdf2ca ./ha-addon/repository/gree-controller/config.yaml
|
||||
@@ -43,8 +43,8 @@ c123b190219556382f0e7f3058056fb233f6261f257a01eceb96a2c0bbfa12f3 ./ha-addon/run
|
||||
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
|
||||
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
|
||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||
4bbfa7cfb7644b6949aa8bc212154fce4dbf1f8bc5c7c56863a30fa63a0efd80 ./lang/en.json
|
||||
bd35f928fbca252a90057a04dc1d363c85bd844b29076238e00751d8ecf196cc ./lang/pl.json
|
||||
30d389223e9a34dd3520fab356bc8f3145e4120dcfd680a59d443012e7fdd4d3 ./lang/en.json
|
||||
7249dd996133dc1b4261faf195ac31d97ac0079fe0b661b34d4837ee15db0727 ./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
|
||||
506942d9c96b586b7f265742421fcea45c8d7e86824ad8934147a4388873c3cc ./scripts/api_dev_test.py
|
||||
4094c8b169b7c3babc15b7af3d4d6f7fa12239a279dc8c73c546ea671ad1aef8 ./scripts/api_dev_test.py
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
|
||||
@@ -96,13 +96,13 @@ bb7cd2c5b27c9dceec1d1d2846fbad9600df9c08e0ad07d13fcde97af533091c ./scripts/inst
|
||||
e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/network-debug.sh
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||
472db31959be29ba99247f7e9cc07a44bc090c538cd654ad8d16e094726037a3 ./scripts/smoke.sh
|
||||
bd703ef841f9763f13367aaa5764381b9c59ee87210e806c24617f3cc0739a9b ./scripts/smoke.sh
|
||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
||||
e89b3503b838670f3213e696395024e74d91c718bc67a4cb295077e944ea4c6c ./src/api.rs
|
||||
546e2ade8f4b9f6be464cb6d61c7eee9809450c4c4950c480ce056a5d683cc0e ./src/api.rs
|
||||
3dfd1e053cbfa0d684dd88cf99b244a96a36e2ff23d2a0715b8327ab62258254 ./src/api/assets.rs
|
||||
05c255919f7b3d81188fc5f1410be6a0037471dfc44e93bffb009f29a1e6a3fc ./src/api/auth.rs
|
||||
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
|
||||
a9e8b74c9ba3ae723d36d2d87c0b1a72138f9ad2fed8e62dc5ad442278a1986d ./src/api/configuration.rs
|
||||
cc439eeeeb3cb87202ab9f78a9e2e746ce79c15e18647c7f32442b4e9053f160 ./src/api/configuration.rs
|
||||
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
|
||||
32ade46ec45d6faf7852242b9f5ba1be6eacbacbeb33d2deabfab59bf1610ce0 ./src/api/device_groups.rs
|
||||
fcbb3da0538aab451f71810f829cc248d6bb67e586dc418d71a572980583823d ./src/api/devices.rs
|
||||
@@ -110,42 +110,44 @@ fcbb3da0538aab451f71810f829cc248d6bb67e586dc418d71a572980583823d ./src/api/devi
|
||||
d59b080b392b574ec94276c7b485b455d0d1abacc7de2ef29dcbfcf7d86dbdb0 ./src/api/flows.rs
|
||||
2cfee7029f065e81628a8942e3dad00a03c3a5cf4f8492d07f12b75d05c07359 ./src/api/gree_cloud.rs
|
||||
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
|
||||
63733cf1fd70b9c2cb1bde3c9304365e8940af4a7524bfc82b47444458a56f4a ./src/api/history.rs
|
||||
cecf1cc7ebf97b6b2ea4dcad0845b2dd434718c86b2e8300a1a52c2ceaf01611 ./src/api/history.rs
|
||||
e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/house.rs
|
||||
27e85400ccbdb0adcb8f5c1624e7187e9b649237cfb97349fcf1c0d78300540d ./src/api/integrations.rs
|
||||
6bfbe5135d4f93bd8d19a707877b53ed2029fd8490b4b1954a7520888fb1d664 ./src/api/middleware.rs
|
||||
45e34a447ff384d99a3c42c489ca9fa75849c74d4d246caa91c63d927de17396 ./src/api/openapi.rs
|
||||
744ce18aa9e0227c6cdfb6980063d0d738d74c5865d45c20fd20026d4742bb3e ./src/api/public_settings.rs
|
||||
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs
|
||||
e69df8a5f612cd37d25fbf9f8e824c5a7d655ac5a6376ec3b31addf1fe8597e2 ./src/api/settings.rs
|
||||
1602f70f93b8fc5dbbc2d6b370d5a43faae90b13bb38beb043b285a8a121b4e1 ./src/api/settings.rs
|
||||
1c1fe39aec55d10608ca3db83827855b07249cd1a81ae054eb1ee6464514092b ./src/api/system.rs
|
||||
9118a5a3a381b010e22d646498ff56d2e05f51656789682cc4371b09c384aa98 ./src/api/websocket.rs
|
||||
455c7238c9788d378e909a4fbc94d5390b67edd92e127f48b4b913ddcbe82fb0 ./src/api/zones.rs
|
||||
f4feaff281163eda543bf094d0fe9158dc6662be5f76f39c81570da7ed0355e7 ./src/config.rs
|
||||
d067e3048803bb1eeef1892cbae5bf719872daa0c5ccc8575affcc37c6615965 ./src/db.rs
|
||||
788d80732d1266798b0d67ed016ecfcc55161ef90bf240a034d186a0ff186e5b ./src/config.rs
|
||||
f8394e0daca284070ac9ae0844d6b04fed7c30386f2d1ec409ecf07880bb0e6a ./src/db.rs
|
||||
fdc9d019097becc5c36a0560caa1619aa35d802c0d208902b3bfecbe1643e3e8 ./src/db/climate.rs
|
||||
e5d2027d40a99e61c2c572499af617a92de9e554f8916f3ea6f6731d1d6b09b4 ./src/db/configuration.rs
|
||||
0a4b4dbfc582d1c8ee942325ac16aa73d1e64555904ec2f6896f4b552bd5e731 ./src/db/core_devices.rs
|
||||
46213497ae26b2f799364a2e64e41df26c93a39eb4e7167b59c1e42b525f61f7 ./src/db/core_devices.rs
|
||||
4c20289c08b0cb9c3c4324d9dbc6a47c7dd953aff6fd1746f04b6024043d3bc5 ./src/db/device_history.rs
|
||||
f99de2230082f17eafaf03f5361bb67419205a23d64f22480c996f9c32c55ac5 ./src/db/energy_history.rs
|
||||
88cb4b7ff61a8a558949faa7157d4bc2c45facb951c57240412c4c9d3befdb86 ./src/db/events_tokens.rs
|
||||
bb5048ee60053f39ce3c1203dc1f24c7047310538d2357f105a052c016d21ec4 ./src/db/flows.rs
|
||||
5f4b567c6bc866d0674b3b0c66f4aeccf561774623f575b872ff25bf015b91c2 ./src/db/ha_history.rs
|
||||
7840c89515d08a23050e2634dd91bcdbf7aecc0a8a80bf5002f3eb660ca46f20 ./src/db/network_history.rs
|
||||
d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs
|
||||
a76938b2e49719b97f8abf40a5cce03df6740ce2e8ac138b179c761619b32ce6 ./src/db/tests.rs
|
||||
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
|
||||
a66b4c2f4f62bd4a981f57ad574c797b73d8400f3492f8ff69126f74b5010899 ./src/engine.rs
|
||||
4ed54df6321496a6afabf9443beb9fa8d3434c510aa01dade318bbaa44f0f0ac ./src/engine.rs
|
||||
ed5c9a0c61c6e597acb33f58593ebe02b562ddc68fa61271e1c1cd436c1003d3 ./src/engine/automations.rs
|
||||
0bf7fe7d28b4749e53ec25d71effbb78bc8f70c716714f838a4a2d8f20627d63 ./src/engine/commands.rs
|
||||
f7de7b9b756bef9d6cffbc6ffdb7504b3121476f188e6ff5c8077d5fc54be811 ./src/engine/connectivity.rs
|
||||
925135985f640ba9ae0a6109d908187b659341a91451e91d27e533adfd610b76 ./src/engine/control_plan.rs
|
||||
7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs
|
||||
67ddb482ad08608e799a88faa6183d9bb65f62ca8ac138c6314cab18bc033033 ./src/engine/energy.rs
|
||||
e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs
|
||||
9f650203959acebc72f3ee87faaeebe4c5c8beee7d77d2039d56fa2c0277aab4 ./src/engine/history.rs
|
||||
5d8b3c9f2243848502bba5c1e4940f8bf8c5ae4bffd891b43e2e9b4e7dc55b0c ./src/engine/history.rs
|
||||
41b7045877771b7c352328998cb45ce2433e7f9631fad3eb1a882ab4897dc31f ./src/engine/local_thermostat.rs
|
||||
99527adab9fea1caede9ebb24faa6a63d5cef4810c64c06306b571cf5cbbb0da ./src/engine/ownership.rs
|
||||
1a29b7fd8de77f0a5e5964ca05e41cc5ea78855c1ab961750d40461faec305e5 ./src/engine/polling.rs
|
||||
d13497acd807a74d707e85046b87fb010e13428758336f736df4a30a5ef0b32c ./src/engine/runtime.rs
|
||||
d48ae1799e045fb212e73e630a5d26ed3c5e3359a3dac3ebd5638532efdd4285 ./src/engine/runtime.rs
|
||||
c1e8de5392681ba1255e3e2d871a6ee860fb8ca987a0a8f973d9ded3977a68fb ./src/engine/schedules.rs
|
||||
b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs
|
||||
682c8b3956c657b655746cbd19ee97ee2f4cf953bc9de67d6d70a1b14f3dd746 ./src/engine/temperature.rs
|
||||
@@ -155,21 +157,21 @@ c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/z
|
||||
28571e25e539fb1fe5e77e8bd003caf2da9e84fc34d156fd5f3e65e805798ca0 ./src/engine/zone_control.rs
|
||||
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
|
||||
ea553a1d8b44c74b300d7c4a76dcd3b370524eb57766730a1376d1ab3079913b ./src/home_assistant.rs
|
||||
b43d95ac42f5edc1a38323e47626b6c0ec6cbbda3c30ad6f245d23865cd1993e ./src/influxdb.rs
|
||||
e78ca9aff23362971eb6450534ea21bdc15150456c378ef9b226cb93a8dbc612 ./src/influxdb.rs
|
||||
da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb/codec.rs
|
||||
67dad0bfd1e5165cacf9049a49c8038f78a75edfc0a825bc2627b5336ad9ce23 ./src/influxdb/query.rs
|
||||
6a158adeb20ec8d66d88f43a05f8a101cf001a8c79951333c1993948f0f306a3 ./src/influxdb/write.rs
|
||||
b96624fa34b218a6d5f028ba09579633f6f8a04c109f0e94801e8d89ceeeb4b5 ./src/influxdb/query.rs
|
||||
8089b8eca47419bb6b3ee02560e5cc39c00479164bc76d11992679b2cef3258e ./src/influxdb/write.rs
|
||||
059346de38c710fadf301b467c0eb45dcc2f764561b868a155c2e089a5027422 ./src/main.rs
|
||||
23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs
|
||||
920e028d96a14cca9f74a964bb296e68a109cfead314d3164c180201e606d151 ./src/models/automation.rs
|
||||
fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/control_plan.rs
|
||||
f1bd2ae1acdc342f9310d01502f478f74c43a7e3b1d284ca55f97900a151b7cd ./src/models/defaults.rs
|
||||
06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs
|
||||
5e212a072bf74ef32a186b172404844b02a92363d216167f814056e1f2a9a7a8 ./src/models/device.rs
|
||||
aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs
|
||||
b10abd201d84f88bf6d71e6ed24a47946b67ef9de8dd0cda0608eeb663b7b417 ./src/models/history.rs
|
||||
ba5d359e4d6c12d9f62c1b28bdfb40d312737930df5421814f5310f7183e96c1 ./src/models/history.rs
|
||||
14e053b0dbea9ed7e88e8affaa595fa126ff833c18696fa695e923488ec40d70 ./src/models/integrations.rs
|
||||
75ad8aa44c1b4d63e9f203c49067fe19f7d8c5ed7cd24a9fc446de06cda61f1f ./src/models/runtime.rs
|
||||
2811ada38a132eb78ad998b64e8cf318ac816efa4731147213b41a5ded401ff9 ./src/models/settings_api.rs
|
||||
f1359b68f1d041cd441f80429e77893eeb6f6194613b58fd3e7a212fb511db3e ./src/models/runtime.rs
|
||||
4caae5c95404d2ffefb7db9507dcf4ed40a90d54bc6aeb51098e77e544d85c42 ./src/models/settings_api.rs
|
||||
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
|
||||
7d798228a8d4d4e60a112167c001535f4b8a5fa32ec7a33554c73125c42256c2 ./src/models/zone.rs
|
||||
62e696df8b86835df7ad127f03c257a78580769dc82bc66daa5041f71e8440a5 ./src/notifications.rs
|
||||
@@ -185,37 +187,37 @@ c09c931fa8d512a8883fb14f3142be694af3c51290e023d25c7c972e156e4bd2 ./src/protocol
|
||||
50152bcad4a843ae04000b991a2365abb302fc1583ea69f09320e25740f3c38c ./src/protocol/gree/tests.rs
|
||||
3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs
|
||||
49dcca1e4a4ad858b312386e3b729f36da691916d8fe62ad682ddb86d40695b7 ./src/protocol/gree_cloud.rs
|
||||
cd1e880ea947150300f798fa313eb36c57325856c3ceab862e53a7b597b477b1 ./src/protocol/gree_cloud_mqtt.rs
|
||||
d406a2c5a49fe059f3f3a00ea534a32f9bdd972c09cc051cbfc4317f7c91b38c ./src/protocol/gree_cloud_mqtt.rs
|
||||
6258ee3888a5f9a4f8bb6cfbbac97f2621de07f9da6bcb2fa596d452f47f2e49 ./src/protocol/mod.rs
|
||||
858932d98576e1863bb9dfc5a43a0fa72db13d84815595fc998d007571f48a2d ./src/provider.rs
|
||||
823da603035619894d8a0252c6a9782c44fdf8d539f09a0a0c120dc8e4b8e4f2 ./src/provider.rs
|
||||
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
|
||||
7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs
|
||||
580917b91441fe9b3276cf0cfc31536ba7954b6a86f4080347b93f9367a5b1ac ./src/queries/entities.rs
|
||||
2c5fba462b72158b04dbaaa53c8536263f8d72a1338a6c4ff06cb28a36f6ae27 ./src/queries/ha_history.rs
|
||||
d051ea7cef7fc733d5fcb0a9d496c419be7ddbe799fc93842b9389d220837747 ./src/queries/maintenance.rs
|
||||
84c3c149cde1728b20ee5723e02d7a7cbb6db9cee3b71a3d4709d6aaa4230e3e ./src/queries/schema.rs
|
||||
e26437195696a049b50154a30574880584526c32c2a55c31ae15e261871fba05 ./src/queries/schema.rs
|
||||
f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/zone_history.rs
|
||||
7e7037a059b585c5709367b9431a225789e7039796e3ed19fc132776ac234099 ./src/state.rs
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
54350093b25e335f7e2bcf99fd0e17ba6df9035a0711c02bcbe0c4dd612c62e5 ./web/index.html
|
||||
55eb6fdbed4a316a3722a355b8bc8cb9c56d016eccac046b0afb6472fcb6cb43 ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
|
||||
a183492c9cf91d1318675f242c57ed30c11661ffc8a5a52f03e5aaeea060ef24 ./web/js/bootstrap.js
|
||||
e987328623938637135c8a8d3952043fe7ae5d8568eff12bf4d95a601409f1df ./web/js/charts.js
|
||||
3c61d86420d1f3765c9ccc184553425132fc432fd60d93763da4669d113b449b ./web/js/core.js
|
||||
27eff2821c40395369be15e197fa830535d2327fcb27c93a0e8e65a1c62de3ad ./web/js/bootstrap.js
|
||||
f6e2d74ef9e43f2b93bd99acb1dfa5e64d889fb895e588d763c0e98c9d12efa5 ./web/js/charts.js
|
||||
b328a641d739428e3329b93cf8f465cd880d1eeb1625015ba1c19e5f51427a27 ./web/js/core.js
|
||||
d880ffdac6aa07cd10d79fbacf12f87eb4c69b48a80564bbfdd0248abe8884d5 ./web/js/dashboard.js
|
||||
ed31601df41cc82032709f960a2b4061966abd58cde456ef7dda05e42eb91432 ./web/js/entities.js
|
||||
424b52fa05cc6f45ab3bcac0f5f1fd0ac2102d840334fc3d762fd208816565a8 ./web/js/events.js
|
||||
8c156909d2d2e59b7d7dc969b9520c978141a448a6cc5aae8fe74a8dc124f2bc ./web/js/flows.js
|
||||
9e2209d41df7624c29741f03f12ed8e83b47cee2891c5a80cea8ca1bdacf1d5a ./web/js/forms.js
|
||||
688390149f023b4f7e8701ca30ea5cb56c27692dc5083344739411405e320bb1 ./web/js/history.js
|
||||
1aefe416b4d4495b52476f619aaec18bb4dc011b2b94c555f9da35509e0553b0 ./web/js/history.js
|
||||
0dc879c0056251618633c9efd188fc1593eeae6965ad1bc993132dea6e171b44 ./web/js/main.js
|
||||
419e0859d1553972f670bb1719be6c0aca14a4ab450041d0017abd41a094b1da ./web/js/navigation.js
|
||||
53ebdaa64a1dc8327db5cd4e08fb3f551dc2df33c214f218c6bb1ae3f27ad4dd ./web/js/navigation.js
|
||||
e3d146a683b8beb332c752e533826160c2cf2236bc269ced7a4dd1249871027b ./web/js/realtime.js
|
||||
abdedaca20ecad50ae2b5c7f6081c80a142112bd867cda19949d5253e609f1b4 ./web/js/router.js
|
||||
55c163fce9520979d380739546ef3036186297466d5fc3ac8cb0680408fb444e ./web/js/settings-ui.js
|
||||
29e6ca03eb62c6158de9ab0b9e74100129478794ae5c697321e217fb1b69ff23 ./web/js/settings.js
|
||||
9546ca5c5566994aff3c33d92295be46b65d51d874681764abe77470dc9c9c73 ./web/js/router.js
|
||||
adba1970e5d6054d87ced5ed57ab210b9857c1555466c126d07cbd743de1b2e6 ./web/js/settings-ui.js
|
||||
0dde5e1a19ab2384156229fe2a5d708e06bf58a2227f834d215af97d4375a5d3 ./web/js/settings.js
|
||||
3815fea7ae1694568c2f249bc8460e0563ced6bde89214f74f21b41fe8cad9a6 ./web/lang-init.js
|
||||
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
|
||||
e6eb02737426ec855eef4b23f4d6df4f4c9a9ffbdc00f492e21731a249cb8821 ./web/styles.css
|
||||
|
||||
@@ -4,7 +4,7 @@ Self-hosted controller for GREE-compatible air conditioners with a local Web UI,
|
||||
|
||||
**Version: 0.14.2**
|
||||
|
||||
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.
|
||||
Version 0.14.2 adds separate vertical and horizontal swing control plus connectivity history. Local/LAN units can be sampled periodically for response time, jitter and packet loss, while optional GREE Cloud diagnostics measure REST and MQTT responsiveness. It keeps the split/multisplit energy and outdoor-temperature improvements from 0.14.1, while preserving independent Local and Cloud device entries.
|
||||
|
||||
> [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples.
|
||||
|
||||
|
||||
+40
-38
@@ -1,13 +1,13 @@
|
||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||
80472072ffaa0e14ef6fd28b02f757d1947fc656687fef19fa96b58b5f13baa5 ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
59fe6d7de7f780481d0a7f614d4325f905a8aee8d971717566fc8889340223b3 ./Cargo.lock
|
||||
97b67fd716185b18bb12d9170b502f81c6ffaf099816c893305c9f837048cb1f ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
de56b105caea31afd80b4051912329a0c1c03233979fd710732cb606acae1dbd ./README.md
|
||||
ee65b2f48b4cc5553e01689ace0f33151bbb2a2738f07ff7918cbf033b6a0f4d ./README.md
|
||||
bb549919b96b0413152dd905aec8640bb53301f07ec7d0b1be48ccbd37936fef ./build.rs
|
||||
0eb39057762c51718e7e7ba220ae1e3a729409c3d91736afa3baa88c64d3a8d6 ./docs/API.md
|
||||
40199fea4917f9278344780440b0f47ca3ed5c6e7873e56040e16519edac7834 ./docs/API.md
|
||||
ff213ad348e7f8b5cae7fe4adcad84d72db693765d424f270faa1acca9c23ce7 ./docs/FLOW.md
|
||||
52181be4d4a161710bbb5e7cde1b3061976e41691d9e10bf99f0c224c65732d1 ./docs/openapi.json
|
||||
be8879ede6b807450dd79af22ad739f4089d0ce0b04f66a8bc00ada685789506 ./docs/openapi.json
|
||||
c65aa543d2e99f8f7d52736307b07ebd182597d80124661d53a16bb23f6f1118 ./ha-addon/.env.example
|
||||
a18d2460340264d34c267fa704ee51a42e59bcc1783ee4c0dbb2a8ef42fa0f16 ./ha-addon/Dockerfile
|
||||
0417c88a990c72aecf071fba64d5d9da65dcda4d396cd7565477f7a4532c8c5e ./ha-addon/README.md
|
||||
@@ -29,7 +29,7 @@ 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
|
||||
e561712958ce5ef1068989af0515687358adc69b1d92af29d6b576d46a182859 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
da2dec2201336a2fe91604e19b85873729ad0889fc310fa1da48708a6ebc0898 ./ha-addon/repository/gree-controller/CHANGELOG.md
|
||||
3c06bd11671452ea6472afd5543fb91a39aba73a1d2d95c7762c04f94ab64030 ./ha-addon/repository/gree-controller/DOCS.md
|
||||
c3db302f38d26dc9f8caf37a1a8a07a1334cc07ae301d452f2940a5ec5915719 ./ha-addon/repository/gree-controller/README.md
|
||||
a3a74b2aaa20fd39a050f8f360eec543d70b3fd43271617a60a0b95fd0cdf2ca ./ha-addon/repository/gree-controller/config.yaml
|
||||
@@ -42,8 +42,8 @@ c123b190219556382f0e7f3058056fb233f6261f257a01eceb96a2c0bbfa12f3 ./ha-addon/run
|
||||
081e27b6e43070d44f865335c30a42a5a29668eaa6a5ea5ab6967ac4447b8e06 ./ha-addon/sync-repository.sh
|
||||
f9e31de3d109ead32bf531c074d46a087df398d62488caee0b46b5e0a1c027f7 ./ha_addon.md
|
||||
253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md
|
||||
4bbfa7cfb7644b6949aa8bc212154fce4dbf1f8bc5c7c56863a30fa63a0efd80 ./lang/en.json
|
||||
bd35f928fbca252a90057a04dc1d363c85bd844b29076238e00751d8ecf196cc ./lang/pl.json
|
||||
30d389223e9a34dd3520fab356bc8f3145e4120dcfd680a59d443012e7fdd4d3 ./lang/en.json
|
||||
7249dd996133dc1b4261faf195ac31d97ac0079fe0b661b34d4837ee15db0727 ./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
|
||||
506942d9c96b586b7f265742421fcea45c8d7e86824ad8934147a4388873c3cc ./scripts/api_dev_test.py
|
||||
4094c8b169b7c3babc15b7af3d4d6f7fa12239a279dc8c73c546ea671ad1aef8 ./scripts/api_dev_test.py
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
1d6e14e26e49aa9d3527f30a23668bf8d9c48b67e6628ef686c3155c012155de ./scripts/dev.sh
|
||||
@@ -95,13 +95,13 @@ bb7cd2c5b27c9dceec1d1d2846fbad9600df9c08e0ad07d13fcde97af533091c ./scripts/inst
|
||||
e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/network-debug.sh
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||
472db31959be29ba99247f7e9cc07a44bc090c538cd654ad8d16e094726037a3 ./scripts/smoke.sh
|
||||
bd703ef841f9763f13367aaa5764381b9c59ee87210e806c24617f3cc0739a9b ./scripts/smoke.sh
|
||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
||||
e89b3503b838670f3213e696395024e74d91c718bc67a4cb295077e944ea4c6c ./src/api.rs
|
||||
546e2ade8f4b9f6be464cb6d61c7eee9809450c4c4950c480ce056a5d683cc0e ./src/api.rs
|
||||
3dfd1e053cbfa0d684dd88cf99b244a96a36e2ff23d2a0715b8327ab62258254 ./src/api/assets.rs
|
||||
05c255919f7b3d81188fc5f1410be6a0037471dfc44e93bffb009f29a1e6a3fc ./src/api/auth.rs
|
||||
9e4fc675306e111ed3db7af9822e2925d88109c31b9e309851665a975c0bd83c ./src/api/automations.rs
|
||||
a9e8b74c9ba3ae723d36d2d87c0b1a72138f9ad2fed8e62dc5ad442278a1986d ./src/api/configuration.rs
|
||||
cc439eeeeb3cb87202ab9f78a9e2e746ce79c15e18647c7f32442b4e9053f160 ./src/api/configuration.rs
|
||||
19bc141286a336ad67e99079496eb0ae5126f986ab28be62b5f97758e5f6969c ./src/api/debug_tokens.rs
|
||||
32ade46ec45d6faf7852242b9f5ba1be6eacbacbeb33d2deabfab59bf1610ce0 ./src/api/device_groups.rs
|
||||
fcbb3da0538aab451f71810f829cc248d6bb67e586dc418d71a572980583823d ./src/api/devices.rs
|
||||
@@ -109,42 +109,44 @@ fcbb3da0538aab451f71810f829cc248d6bb67e586dc418d71a572980583823d ./src/api/devi
|
||||
d59b080b392b574ec94276c7b485b455d0d1abacc7de2ef29dcbfcf7d86dbdb0 ./src/api/flows.rs
|
||||
2cfee7029f065e81628a8942e3dad00a03c3a5cf4f8492d07f12b75d05c07359 ./src/api/gree_cloud.rs
|
||||
b61b0b4f196c5e72e47cf06ca0c14df3cf8eedbdef0242a9d2af33623663489f ./src/api/groups.rs
|
||||
63733cf1fd70b9c2cb1bde3c9304365e8940af4a7524bfc82b47444458a56f4a ./src/api/history.rs
|
||||
cecf1cc7ebf97b6b2ea4dcad0845b2dd434718c86b2e8300a1a52c2ceaf01611 ./src/api/history.rs
|
||||
e8edfbb725d6de650442444b0a929ad2be0bd2c2d7d227d2b3fdd99d05c94f94 ./src/api/house.rs
|
||||
27e85400ccbdb0adcb8f5c1624e7187e9b649237cfb97349fcf1c0d78300540d ./src/api/integrations.rs
|
||||
6bfbe5135d4f93bd8d19a707877b53ed2029fd8490b4b1954a7520888fb1d664 ./src/api/middleware.rs
|
||||
45e34a447ff384d99a3c42c489ca9fa75849c74d4d246caa91c63d927de17396 ./src/api/openapi.rs
|
||||
744ce18aa9e0227c6cdfb6980063d0d738d74c5865d45c20fd20026d4742bb3e ./src/api/public_settings.rs
|
||||
5b29f2e5aa2c422c5829ef89c7b59cf753883aa7f60661c95ab0f95cf676fa3f ./src/api/schedules.rs
|
||||
e69df8a5f612cd37d25fbf9f8e824c5a7d655ac5a6376ec3b31addf1fe8597e2 ./src/api/settings.rs
|
||||
1602f70f93b8fc5dbbc2d6b370d5a43faae90b13bb38beb043b285a8a121b4e1 ./src/api/settings.rs
|
||||
1c1fe39aec55d10608ca3db83827855b07249cd1a81ae054eb1ee6464514092b ./src/api/system.rs
|
||||
9118a5a3a381b010e22d646498ff56d2e05f51656789682cc4371b09c384aa98 ./src/api/websocket.rs
|
||||
455c7238c9788d378e909a4fbc94d5390b67edd92e127f48b4b913ddcbe82fb0 ./src/api/zones.rs
|
||||
f4feaff281163eda543bf094d0fe9158dc6662be5f76f39c81570da7ed0355e7 ./src/config.rs
|
||||
d067e3048803bb1eeef1892cbae5bf719872daa0c5ccc8575affcc37c6615965 ./src/db.rs
|
||||
788d80732d1266798b0d67ed016ecfcc55161ef90bf240a034d186a0ff186e5b ./src/config.rs
|
||||
f8394e0daca284070ac9ae0844d6b04fed7c30386f2d1ec409ecf07880bb0e6a ./src/db.rs
|
||||
fdc9d019097becc5c36a0560caa1619aa35d802c0d208902b3bfecbe1643e3e8 ./src/db/climate.rs
|
||||
e5d2027d40a99e61c2c572499af617a92de9e554f8916f3ea6f6731d1d6b09b4 ./src/db/configuration.rs
|
||||
0a4b4dbfc582d1c8ee942325ac16aa73d1e64555904ec2f6896f4b552bd5e731 ./src/db/core_devices.rs
|
||||
46213497ae26b2f799364a2e64e41df26c93a39eb4e7167b59c1e42b525f61f7 ./src/db/core_devices.rs
|
||||
4c20289c08b0cb9c3c4324d9dbc6a47c7dd953aff6fd1746f04b6024043d3bc5 ./src/db/device_history.rs
|
||||
f99de2230082f17eafaf03f5361bb67419205a23d64f22480c996f9c32c55ac5 ./src/db/energy_history.rs
|
||||
88cb4b7ff61a8a558949faa7157d4bc2c45facb951c57240412c4c9d3befdb86 ./src/db/events_tokens.rs
|
||||
bb5048ee60053f39ce3c1203dc1f24c7047310538d2357f105a052c016d21ec4 ./src/db/flows.rs
|
||||
5f4b567c6bc866d0674b3b0c66f4aeccf561774623f575b872ff25bf015b91c2 ./src/db/ha_history.rs
|
||||
7840c89515d08a23050e2634dd91bcdbf7aecc0a8a80bf5002f3eb660ca46f20 ./src/db/network_history.rs
|
||||
d6b9b70a04c774173c97f7426487d5e2932d7713e12ae3fb7f64cf2b4e0b3c10 ./src/db/schedules_automations.rs
|
||||
a76938b2e49719b97f8abf40a5cce03df6740ce2e8ac138b179c761619b32ce6 ./src/db/tests.rs
|
||||
f90fbb14f29a20b799b3856bdaa22812a9f0d9dfa46693f1341c160dccda8a97 ./src/db/zone_history.rs
|
||||
a66b4c2f4f62bd4a981f57ad574c797b73d8400f3492f8ff69126f74b5010899 ./src/engine.rs
|
||||
4ed54df6321496a6afabf9443beb9fa8d3434c510aa01dade318bbaa44f0f0ac ./src/engine.rs
|
||||
ed5c9a0c61c6e597acb33f58593ebe02b562ddc68fa61271e1c1cd436c1003d3 ./src/engine/automations.rs
|
||||
0bf7fe7d28b4749e53ec25d71effbb78bc8f70c716714f838a4a2d8f20627d63 ./src/engine/commands.rs
|
||||
f7de7b9b756bef9d6cffbc6ffdb7504b3121476f188e6ff5c8077d5fc54be811 ./src/engine/connectivity.rs
|
||||
925135985f640ba9ae0a6109d908187b659341a91451e91d27e533adfd610b76 ./src/engine/control_plan.rs
|
||||
7b48a33343d519a2ff2d81e9d757eb56a0c1beacfc0b7b8ef61f49c2ad363f9a ./src/engine/deadlines.rs
|
||||
67ddb482ad08608e799a88faa6183d9bb65f62ca8ac138c6314cab18bc033033 ./src/engine/energy.rs
|
||||
e350aa63f3bb14ca887eaaa7555edbeae00fb584cbcaf552cc972f0c8918f5d8 ./src/engine/groups.rs
|
||||
9f650203959acebc72f3ee87faaeebe4c5c8beee7d77d2039d56fa2c0277aab4 ./src/engine/history.rs
|
||||
5d8b3c9f2243848502bba5c1e4940f8bf8c5ae4bffd891b43e2e9b4e7dc55b0c ./src/engine/history.rs
|
||||
41b7045877771b7c352328998cb45ce2433e7f9631fad3eb1a882ab4897dc31f ./src/engine/local_thermostat.rs
|
||||
99527adab9fea1caede9ebb24faa6a63d5cef4810c64c06306b571cf5cbbb0da ./src/engine/ownership.rs
|
||||
1a29b7fd8de77f0a5e5964ca05e41cc5ea78855c1ab961750d40461faec305e5 ./src/engine/polling.rs
|
||||
d13497acd807a74d707e85046b87fb010e13428758336f736df4a30a5ef0b32c ./src/engine/runtime.rs
|
||||
d48ae1799e045fb212e73e630a5d26ed3c5e3359a3dac3ebd5638532efdd4285 ./src/engine/runtime.rs
|
||||
c1e8de5392681ba1255e3e2d871a6ee860fb8ca987a0a8f973d9ded3977a68fb ./src/engine/schedules.rs
|
||||
b0189f28d0f337dfd3ce0934da7a565e4a5535afc1c242214c26ed584b3b7840 ./src/engine/targets.rs
|
||||
682c8b3956c657b655746cbd19ee97ee2f4cf953bc9de67d6d70a1b14f3dd746 ./src/engine/temperature.rs
|
||||
@@ -154,21 +156,21 @@ c89ff36c6a67bf4fbf68c43e18aa0a556de2dc69263fd79db376363afc741a4f ./src/engine/z
|
||||
28571e25e539fb1fe5e77e8bd003caf2da9e84fc34d156fd5f3e65e805798ca0 ./src/engine/zone_control.rs
|
||||
bfac95cec87f0df6ce562153cd3cfe6731359d703e6c592d2c2f7ba01ea62d99 ./src/error.rs
|
||||
ea553a1d8b44c74b300d7c4a76dcd3b370524eb57766730a1376d1ab3079913b ./src/home_assistant.rs
|
||||
b43d95ac42f5edc1a38323e47626b6c0ec6cbbda3c30ad6f245d23865cd1993e ./src/influxdb.rs
|
||||
e78ca9aff23362971eb6450534ea21bdc15150456c378ef9b226cb93a8dbc612 ./src/influxdb.rs
|
||||
da4068295f37c23222bb13bebac022d88ba7729604a49ad25438c55e45458a49 ./src/influxdb/codec.rs
|
||||
67dad0bfd1e5165cacf9049a49c8038f78a75edfc0a825bc2627b5336ad9ce23 ./src/influxdb/query.rs
|
||||
6a158adeb20ec8d66d88f43a05f8a101cf001a8c79951333c1993948f0f306a3 ./src/influxdb/write.rs
|
||||
b96624fa34b218a6d5f028ba09579633f6f8a04c109f0e94801e8d89ceeeb4b5 ./src/influxdb/query.rs
|
||||
8089b8eca47419bb6b3ee02560e5cc39c00479164bc76d11992679b2cef3258e ./src/influxdb/write.rs
|
||||
059346de38c710fadf301b467c0eb45dcc2f764561b868a155c2e089a5027422 ./src/main.rs
|
||||
23688a5cb68efded89471ce959328d27adff370d451bd9f378a283ab51437e76 ./src/models.rs
|
||||
920e028d96a14cca9f74a964bb296e68a109cfead314d3164c180201e606d151 ./src/models/automation.rs
|
||||
fc5f2802a073a175a8985e56bcb87eb1ff481226d5b4eeabad73dbba1839fc45 ./src/models/control_plan.rs
|
||||
f1bd2ae1acdc342f9310d01502f478f74c43a7e3b1d284ca55f97900a151b7cd ./src/models/defaults.rs
|
||||
06357b352ff6494d83e72796b869573a8b8f36b835985a5900ac571c922b8da9 ./src/models/defaults.rs
|
||||
5e212a072bf74ef32a186b172404844b02a92363d216167f814056e1f2a9a7a8 ./src/models/device.rs
|
||||
aeab5b5acf35d38a54fd1d82e964fe56635ade69c0149ad423df317095d01555 ./src/models/flow.rs
|
||||
b10abd201d84f88bf6d71e6ed24a47946b67ef9de8dd0cda0608eeb663b7b417 ./src/models/history.rs
|
||||
ba5d359e4d6c12d9f62c1b28bdfb40d312737930df5421814f5310f7183e96c1 ./src/models/history.rs
|
||||
14e053b0dbea9ed7e88e8affaa595fa126ff833c18696fa695e923488ec40d70 ./src/models/integrations.rs
|
||||
75ad8aa44c1b4d63e9f203c49067fe19f7d8c5ed7cd24a9fc446de06cda61f1f ./src/models/runtime.rs
|
||||
2811ada38a132eb78ad998b64e8cf318ac816efa4731147213b41a5ded401ff9 ./src/models/settings_api.rs
|
||||
f1359b68f1d041cd441f80429e77893eeb6f6194613b58fd3e7a212fb511db3e ./src/models/runtime.rs
|
||||
4caae5c95404d2ffefb7db9507dcf4ed40a90d54bc6aeb51098e77e544d85c42 ./src/models/settings_api.rs
|
||||
c4de6101de85d9d7e6814b2504b87d9216561e3d9e274054f081f505fb152140 ./src/models/temporary_thermostat.rs
|
||||
7d798228a8d4d4e60a112167c001535f4b8a5fa32ec7a33554c73125c42256c2 ./src/models/zone.rs
|
||||
62e696df8b86835df7ad127f03c257a78580769dc82bc66daa5041f71e8440a5 ./src/notifications.rs
|
||||
@@ -184,37 +186,37 @@ c09c931fa8d512a8883fb14f3142be694af3c51290e023d25c7c972e156e4bd2 ./src/protocol
|
||||
50152bcad4a843ae04000b991a2365abb302fc1583ea69f09320e25740f3c38c ./src/protocol/gree/tests.rs
|
||||
3c93e5b254a2a40a07854532cb5fc07e493ad65d9ba2cf28093cff4a4a4e451c ./src/protocol/gree/transport.rs
|
||||
49dcca1e4a4ad858b312386e3b729f36da691916d8fe62ad682ddb86d40695b7 ./src/protocol/gree_cloud.rs
|
||||
cd1e880ea947150300f798fa313eb36c57325856c3ceab862e53a7b597b477b1 ./src/protocol/gree_cloud_mqtt.rs
|
||||
d406a2c5a49fe059f3f3a00ea534a32f9bdd972c09cc051cbfc4317f7c91b38c ./src/protocol/gree_cloud_mqtt.rs
|
||||
6258ee3888a5f9a4f8bb6cfbbac97f2621de07f9da6bcb2fa596d452f47f2e49 ./src/protocol/mod.rs
|
||||
858932d98576e1863bb9dfc5a43a0fa72db13d84815595fc998d007571f48a2d ./src/provider.rs
|
||||
823da603035619894d8a0252c6a9782c44fdf8d539f09a0a0c120dc8e4b8e4f2 ./src/provider.rs
|
||||
830621fa75265da7288b8135704c0ce27e0687f87d55abf3cdbe40a6e2f3d2b1 ./src/queries.rs
|
||||
7fac65a9dcbcecee09816f5447c7792dc94f736b1efb66ac85629db75f6280be ./src/queries/device_history.rs
|
||||
580917b91441fe9b3276cf0cfc31536ba7954b6a86f4080347b93f9367a5b1ac ./src/queries/entities.rs
|
||||
2c5fba462b72158b04dbaaa53c8536263f8d72a1338a6c4ff06cb28a36f6ae27 ./src/queries/ha_history.rs
|
||||
d051ea7cef7fc733d5fcb0a9d496c419be7ddbe799fc93842b9389d220837747 ./src/queries/maintenance.rs
|
||||
84c3c149cde1728b20ee5723e02d7a7cbb6db9cee3b71a3d4709d6aaa4230e3e ./src/queries/schema.rs
|
||||
e26437195696a049b50154a30574880584526c32c2a55c31ae15e261871fba05 ./src/queries/schema.rs
|
||||
f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/zone_history.rs
|
||||
7e7037a059b585c5709367b9431a225789e7039796e3ed19fc132776ac234099 ./src/state.rs
|
||||
b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service
|
||||
544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html
|
||||
e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg
|
||||
54350093b25e335f7e2bcf99fd0e17ba6df9035a0711c02bcbe0c4dd612c62e5 ./web/index.html
|
||||
55eb6fdbed4a316a3722a355b8bc8cb9c56d016eccac046b0afb6472fcb6cb43 ./web/index.html
|
||||
9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md
|
||||
a183492c9cf91d1318675f242c57ed30c11661ffc8a5a52f03e5aaeea060ef24 ./web/js/bootstrap.js
|
||||
e987328623938637135c8a8d3952043fe7ae5d8568eff12bf4d95a601409f1df ./web/js/charts.js
|
||||
3c61d86420d1f3765c9ccc184553425132fc432fd60d93763da4669d113b449b ./web/js/core.js
|
||||
27eff2821c40395369be15e197fa830535d2327fcb27c93a0e8e65a1c62de3ad ./web/js/bootstrap.js
|
||||
f6e2d74ef9e43f2b93bd99acb1dfa5e64d889fb895e588d763c0e98c9d12efa5 ./web/js/charts.js
|
||||
b328a641d739428e3329b93cf8f465cd880d1eeb1625015ba1c19e5f51427a27 ./web/js/core.js
|
||||
d880ffdac6aa07cd10d79fbacf12f87eb4c69b48a80564bbfdd0248abe8884d5 ./web/js/dashboard.js
|
||||
ed31601df41cc82032709f960a2b4061966abd58cde456ef7dda05e42eb91432 ./web/js/entities.js
|
||||
424b52fa05cc6f45ab3bcac0f5f1fd0ac2102d840334fc3d762fd208816565a8 ./web/js/events.js
|
||||
8c156909d2d2e59b7d7dc969b9520c978141a448a6cc5aae8fe74a8dc124f2bc ./web/js/flows.js
|
||||
9e2209d41df7624c29741f03f12ed8e83b47cee2891c5a80cea8ca1bdacf1d5a ./web/js/forms.js
|
||||
688390149f023b4f7e8701ca30ea5cb56c27692dc5083344739411405e320bb1 ./web/js/history.js
|
||||
1aefe416b4d4495b52476f619aaec18bb4dc011b2b94c555f9da35509e0553b0 ./web/js/history.js
|
||||
0dc879c0056251618633c9efd188fc1593eeae6965ad1bc993132dea6e171b44 ./web/js/main.js
|
||||
419e0859d1553972f670bb1719be6c0aca14a4ab450041d0017abd41a094b1da ./web/js/navigation.js
|
||||
53ebdaa64a1dc8327db5cd4e08fb3f551dc2df33c214f218c6bb1ae3f27ad4dd ./web/js/navigation.js
|
||||
e3d146a683b8beb332c752e533826160c2cf2236bc269ced7a4dd1249871027b ./web/js/realtime.js
|
||||
abdedaca20ecad50ae2b5c7f6081c80a142112bd867cda19949d5253e609f1b4 ./web/js/router.js
|
||||
55c163fce9520979d380739546ef3036186297466d5fc3ac8cb0680408fb444e ./web/js/settings-ui.js
|
||||
29e6ca03eb62c6158de9ab0b9e74100129478794ae5c697321e217fb1b69ff23 ./web/js/settings.js
|
||||
9546ca5c5566994aff3c33d92295be46b65d51d874681764abe77470dc9c9c73 ./web/js/router.js
|
||||
adba1970e5d6054d87ced5ed57ab210b9857c1555466c126d07cbd743de1b2e6 ./web/js/settings-ui.js
|
||||
0dde5e1a19ab2384156229fe2a5d708e06bf58a2227f834d215af97d4375a5d3 ./web/js/settings.js
|
||||
3815fea7ae1694568c2f249bc8460e0563ced6bde89214f74f21b41fe8cad9a6 ./web/lang-init.js
|
||||
6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest
|
||||
e6eb02737426ec855eef4b23f4d6df4f4c9a9ffbdc00f492e21731a249cb8821 ./web/styles.css
|
||||
|
||||
@@ -1693,3 +1693,10 @@ Energy endpoints:
|
||||
- `GET /api/history/energy?device_id=...&interval=hourly|daily|weekly|monthly&source=auto|gree_cloud|home_assistant` returns consumption buckets and period summaries in kWh.
|
||||
|
||||
Cloud settings and diagnostics are redacted: passwords, REST/MQTT tokens, Authorization values and device cipher keys are not returned.
|
||||
|
||||
|
||||
## Connectivity history — 0.14.2
|
||||
|
||||
`GET /api/history/network?hours=24&target_id=all` returns connectivity batches for Local/LAN units and, when enabled, controller-level `cloud:rest` and `cloud:mqtt` endpoints. Each reading contains `latency_ms`, `jitter_ms`, `packet_loss_pct`, `sample_count`, `successful_samples`, `target_kind` and `source`.
|
||||
|
||||
Local connectivity sampling is configured through `GET/PUT /api/settings/gree` using `ping_metrics_enabled`, `ping_interval_seconds` and `ping_sample_count`. Direct unit probes are Local/LAN-only. GREE Cloud connectivity sampling is configured through `GET/PUT /api/settings/gree-cloud` using `connectivity_metrics_enabled`, `connectivity_metrics_interval_seconds` and `connectivity_metrics_sample_count`; it is disabled by default and measures REST login response plus MQTT `PINGRESP` round-trip. Connectivity history follows the same SQLite retention/compaction and optional InfluxDB archive policy as other history metrics.
|
||||
|
||||
+235
-4
@@ -4147,7 +4147,7 @@
|
||||
"Settings"
|
||||
],
|
||||
"summary": "Get GREE settings",
|
||||
"description": "Controller identity, polling/discovery and GREE command safety settings.",
|
||||
"description": "Controller identity, polling/discovery, Local/LAN connectivity worker and GREE command safety settings.",
|
||||
"operationId": "getGREESettings",
|
||||
"security": [
|
||||
{
|
||||
@@ -4181,7 +4181,7 @@
|
||||
"Settings"
|
||||
],
|
||||
"summary": "Update GREE settings",
|
||||
"description": "Controller identity, polling/discovery and GREE command safety settings.",
|
||||
"description": "Controller identity, polling/discovery, Local/LAN connectivity worker and GREE command safety settings.",
|
||||
"operationId": "updateGREESettings",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -4887,7 +4887,7 @@
|
||||
"GREE Cloud"
|
||||
],
|
||||
"summary": "Update GREE Cloud settings",
|
||||
"description": "Updates central GREE Cloud account/provider settings. Polling is clamped to 30–3600 seconds; changing account settings drops the existing MQTT session.",
|
||||
"description": "Updates central GREE Cloud account/provider settings. Polling is clamped to 30–3600 seconds. Optional REST/MQTT connectivity metrics are disabled by default and can configure a 30–3600 second interval with 1–10 samples; changing account settings drops the existing MQTT session.",
|
||||
"operationId": "updateGreeCloudSettings",
|
||||
"security": [
|
||||
{
|
||||
@@ -5336,6 +5336,112 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/history/network": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"History"
|
||||
],
|
||||
"summary": "Get connectivity and ping history",
|
||||
"description": "Returns time-bucketed Local/LAN GREE UDP round-trip metrics and optional controller-level GREE Cloud REST/MQTT diagnostics, including latency, jitter and packet loss. Older data follows the same SQLite retention/compaction and optional InfluxDB archive policy as other history metrics.",
|
||||
"operationId": "networkHistory",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "target_id",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Local device id, cloud:rest, cloud:mqtt, or all/omitted for every target.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "all"
|
||||
},
|
||||
{
|
||||
"name": "hours",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Lookback window in hours.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 87600,
|
||||
"default": 24
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Maximum number of returned rows.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 50000,
|
||||
"default": 20000
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"BearerToken": []
|
||||
},
|
||||
{
|
||||
"ApiTokenHeader": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Connectivity history payload with readings, available targets and storage metadata.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"readings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/NetworkReading"
|
||||
}
|
||||
},
|
||||
"targets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"bucket_seconds": {
|
||||
"type": "integer"
|
||||
},
|
||||
"storage": {
|
||||
"type": "string"
|
||||
},
|
||||
"storage_warning": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"readings",
|
||||
"targets",
|
||||
"bucket_seconds",
|
||||
"storage"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"500": {
|
||||
"$ref": "#/components/responses/InternalError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@@ -7981,6 +8087,25 @@
|
||||
"type": "integer",
|
||||
"minimum": 30,
|
||||
"maximum": 1800
|
||||
},
|
||||
"ping_metrics_enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enable periodic Local/LAN connectivity measurements."
|
||||
},
|
||||
"ping_interval_seconds": {
|
||||
"type": "integer",
|
||||
"minimum": 10,
|
||||
"maximum": 3600,
|
||||
"default": 60,
|
||||
"description": "Seconds between Local/LAN measurement batches."
|
||||
},
|
||||
"ping_sample_count": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 10,
|
||||
"default": 3,
|
||||
"description": "Round-trip samples per Local/LAN measurement batch."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -8565,6 +8690,23 @@
|
||||
],
|
||||
"minimum": 0,
|
||||
"description": "Duration in milliseconds of the most recent successful GREE Cloud REST operation."
|
||||
},
|
||||
"connectivity_metrics_enabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Opt-in GREE Cloud REST/MQTT connectivity history. Disabled by default."
|
||||
},
|
||||
"connectivity_metrics_interval_seconds": {
|
||||
"type": "integer",
|
||||
"minimum": 30,
|
||||
"maximum": 3600,
|
||||
"default": 300
|
||||
},
|
||||
"connectivity_metrics_sample_count": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 10,
|
||||
"default": 3
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -8574,7 +8716,10 @@
|
||||
"password_configured",
|
||||
"polling_interval_seconds",
|
||||
"installation_id",
|
||||
"account_id"
|
||||
"account_id",
|
||||
"connectivity_metrics_enabled",
|
||||
"connectivity_metrics_interval_seconds",
|
||||
"connectivity_metrics_sample_count"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
@@ -8602,6 +8747,23 @@
|
||||
"type": "integer",
|
||||
"minimum": 30,
|
||||
"maximum": 3600
|
||||
},
|
||||
"connectivity_metrics_enabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Opt-in GREE Cloud REST/MQTT connectivity history. Disabled by default."
|
||||
},
|
||||
"connectivity_metrics_interval_seconds": {
|
||||
"type": "integer",
|
||||
"minimum": 30,
|
||||
"maximum": 3600,
|
||||
"default": 300
|
||||
},
|
||||
"connectivity_metrics_sample_count": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 10,
|
||||
"default": 3
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -8875,6 +9037,75 @@
|
||||
"runtime"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"NetworkReading": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"target_id": {
|
||||
"type": "string",
|
||||
"description": "Local device id or cloud:rest/cloud:mqtt."
|
||||
},
|
||||
"target_kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"device",
|
||||
"cloud_service"
|
||||
]
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"latency_ms": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
],
|
||||
"minimum": 0
|
||||
},
|
||||
"jitter_ms": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
],
|
||||
"minimum": 0
|
||||
},
|
||||
"packet_loss_pct": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
},
|
||||
"sample_count": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"successful_samples": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"local_udp",
|
||||
"cloud_rest",
|
||||
"cloud_mqtt"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"target_id",
|
||||
"target_kind",
|
||||
"timestamp",
|
||||
"packet_loss_pct",
|
||||
"sample_count",
|
||||
"successful_samples",
|
||||
"source"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
## 0.14.2
|
||||
|
||||
- Adds a configurable background connectivity worker for Local/LAN units with measurement interval and sample-count settings.
|
||||
- Adds a History → Connectivity/Pings view with per-endpoint latency, jitter and packet-loss charts plus latest batch summaries.
|
||||
- Stores connectivity readings in SQLite using the same retention/compaction lifecycle as other history metrics and archives them to InfluxDB when enabled.
|
||||
- Keeps direct unit probing Local/LAN-only; GREE Cloud diagnostics instead measure optional REST response time and MQTT PINGRESP round-trip.
|
||||
- Adds opt-in GREE Cloud connectivity metrics with configurable interval/sample count; Cloud measurements are disabled by default.
|
||||
- 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.
|
||||
|
||||
+20
-1
@@ -1299,6 +1299,25 @@
|
||||
"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."
|
||||
"flow.thermostatDeviceOptionsHint": "Vertical and horizontal swing are sent to the unit as independent settings and do not change thermostat logic.",
|
||||
"history.tabNetwork": "Pings",
|
||||
"history.networkTarget": "Endpoint",
|
||||
"history.allNetworkTargets": "All endpoints",
|
||||
"history.networkLatency": "Latency",
|
||||
"history.networkJitter": "Jitter",
|
||||
"history.networkLoss": "Packet loss",
|
||||
"history.networkLatencyJitter": "Latency and jitter",
|
||||
"history.networkLatencyJitterHint": "Round-trip response time and jitter measured over time.",
|
||||
"history.networkLossHint": "Packet loss percentage for each measurement batch.",
|
||||
"history.networkNoData": "No connectivity metrics yet",
|
||||
"history.networkNoDataHint": "Enable connectivity metrics in Settings and wait for the first measurement cycle.",
|
||||
"settings.connectivityTitle": "Connectivity metrics",
|
||||
"settings.connectivityHint": "Periodic GREE UDP round-trip measurements for Local/LAN units. Results are stored in History.",
|
||||
"settings.connectivityEnable": "Enable local connectivity metrics",
|
||||
"settings.connectivityInterval": "Measurement interval (s)",
|
||||
"settings.connectivitySamples": "Samples per measurement",
|
||||
"settings.connectivityLocalOnly": "Only Local/LAN installations are probed. Cloud devices are not pinged directly.",
|
||||
"settings.cloudConnectivityEnable": "Measure Cloud REST/MQTT connectivity",
|
||||
"settings.cloudConnectivityHint": "Optional and disabled by default. Measures REST login response and MQTT PINGRESP round-trip."
|
||||
}
|
||||
}
|
||||
|
||||
+20
-1
@@ -1299,6 +1299,25 @@
|
||||
"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."
|
||||
"flow.thermostatDeviceOptionsHint": "Swing pionowy i poziomy są wysyłane do jednostki jako niezależne ustawienia i nie zmieniają logiki termostatu.",
|
||||
"history.tabNetwork": "Pingi",
|
||||
"history.networkTarget": "Punkt pomiaru",
|
||||
"history.allNetworkTargets": "Wszystkie punkty",
|
||||
"history.networkLatency": "Opóźnienie",
|
||||
"history.networkJitter": "Jitter",
|
||||
"history.networkLoss": "Straty pakietów",
|
||||
"history.networkLatencyJitter": "Opóźnienie i jitter",
|
||||
"history.networkLatencyJitterHint": "Czas odpowiedzi round-trip i jitter mierzone w czasie.",
|
||||
"history.networkLossHint": "Procent strat dla każdej serii próbek.",
|
||||
"history.networkNoData": "Brak metryk łączności",
|
||||
"history.networkNoDataHint": "Włącz metryki łączności w Ustawieniach i poczekaj na pierwszy cykl pomiarowy.",
|
||||
"settings.connectivityTitle": "Metryki łączności",
|
||||
"settings.connectivityHint": "Okresowe pomiary odpowiedzi GREE UDP dla jednostek Local/LAN. Wyniki są zapisywane w Historii.",
|
||||
"settings.connectivityEnable": "Włącz lokalne metryki łączności",
|
||||
"settings.connectivityInterval": "Interwał pomiaru (s)",
|
||||
"settings.connectivitySamples": "Liczba próbek na pomiar",
|
||||
"settings.connectivityLocalOnly": "Pingowane są tylko instalacje Local/LAN. Jednostki Cloud nie są pingowane bezpośrednio.",
|
||||
"settings.cloudConnectivityEnable": "Mierz łączność GREE Cloud REST/MQTT",
|
||||
"settings.cloudConnectivityHint": "Opcjonalne i domyślnie wyłączone. Mierzy czas odpowiedzi logowania REST oraz round-trip MQTT PINGRESP."
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -36,6 +36,9 @@ SETTINGS_PATHS: dict[str, set[str]] = {
|
||||
"zone_interval_seconds",
|
||||
"discovery_timeout_ms",
|
||||
"discovery_broadcast",
|
||||
"ping_metrics_enabled",
|
||||
"ping_interval_seconds",
|
||||
"ping_sample_count",
|
||||
"suppress_device_beep",
|
||||
"compressor_protection_enabled",
|
||||
"compressor_protection_seconds",
|
||||
@@ -46,9 +49,13 @@ SETTINGS_PATHS: dict[str, set[str]] = {
|
||||
"username",
|
||||
"password_configured",
|
||||
"polling_interval_seconds",
|
||||
"connectivity_metrics_enabled",
|
||||
"connectivity_metrics_interval_seconds",
|
||||
"connectivity_metrics_sample_count",
|
||||
"installation_id",
|
||||
"account_id",
|
||||
"last_successful_contact",
|
||||
"last_rest_response_time_ms",
|
||||
},
|
||||
"/api/settings/history": {"retention_days", "compaction_enabled", "event_retention_days"},
|
||||
"/api/settings/influxdb": {
|
||||
@@ -111,6 +118,7 @@ SAFE_GET_PATHS = [
|
||||
"/api/flows",
|
||||
"/api/readings",
|
||||
"/api/history",
|
||||
"/api/history/network",
|
||||
"/api/control-plan",
|
||||
"/api/events",
|
||||
"/api/access-tokens",
|
||||
@@ -135,8 +143,8 @@ DETAIL_COLLECTIONS = [
|
||||
("/api/flows", "/api/flows/{id}"),
|
||||
]
|
||||
|
||||
# The beta cloud endpoints are not yet regenerated into the static OpenAPI file.
|
||||
EXPECTED_OPENAPI_PATHS = (set(SETTINGS_PATHS) - {"/api/settings/gree-cloud"}) | {
|
||||
EXPECTED_OPENAPI_PATHS = set(SETTINGS_PATHS) | {
|
||||
"/api/history/network",
|
||||
"/api/configuration/export",
|
||||
"/api/configuration/import",
|
||||
"/api/integrations/home-assistant/snapshot",
|
||||
@@ -319,6 +327,7 @@ def convert_settings_view_to_update(path: str, view: dict[str, Any]) -> dict[str
|
||||
payload.pop("installation_id", None)
|
||||
payload.pop("account_id", None)
|
||||
payload.pop("last_successful_contact", None)
|
||||
payload.pop("last_rest_response_time_ms", None)
|
||||
elif path == "/api/settings/notifications":
|
||||
payload.pop("pushover_configured", None)
|
||||
payload.pop("slack_configured", None)
|
||||
|
||||
@@ -58,6 +58,7 @@ assert isinstance(settings, dict), "bootstrap.settings missing"
|
||||
paths = {
|
||||
"application": "/api/settings/application",
|
||||
"gree": "/api/settings/gree",
|
||||
"gree_cloud": "/api/settings/gree-cloud",
|
||||
"history": "/api/settings/history",
|
||||
"influxdb": "/api/settings/influxdb",
|
||||
"notifications": "/api/settings/notifications",
|
||||
@@ -73,6 +74,7 @@ for section, path in paths.items():
|
||||
|
||||
assert "password" not in settings["influxdb"]
|
||||
assert "token" not in settings["influxdb"]
|
||||
assert "password" not in settings["gree_cloud"]
|
||||
assert "token" not in settings["home_assistant"]
|
||||
for secret in ("pushover_app_token", "pushover_user_key", "slack_webhook_url", "discord_webhook_url"):
|
||||
assert secret not in settings["notifications"], f"secret field leaked: {secret}"
|
||||
@@ -88,6 +90,16 @@ curl -fsS -X POST \
|
||||
grep -q '"ok":true' "$TMP/probe.json"
|
||||
grep -q '"response_time_ms":0' "$TMP/probe.json"
|
||||
|
||||
curl -fsS "http://127.0.0.1:$PORT/api/history/network?hours=24&limit=100" >"$TMP/network-history.json"
|
||||
python3 - "$TMP/network-history.json" <<'PYNETWORK'
|
||||
import json, sys
|
||||
data = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
assert isinstance(data.get("readings"), list)
|
||||
assert isinstance(data.get("targets"), list)
|
||||
assert isinstance(data.get("bucket_seconds"), int) and data["bucket_seconds"] > 0
|
||||
assert isinstance(data.get("storage"), str) and data["storage"]
|
||||
PYNETWORK
|
||||
|
||||
curl -fsS -X POST -H 'Content-Type: application/json' \
|
||||
-d '{"name":"Test","device_id":"sim-salon","enabled":true,"mode":"cool","setpoint":23,"hysteresis":0.6,"min_on_seconds":0,"min_off_seconds":0,"sensor_source":"device"}' \
|
||||
"http://127.0.0.1:$PORT/api/zones" >"$TMP/zone.json"
|
||||
|
||||
+3
-1
@@ -10,7 +10,7 @@ use crate::{
|
||||
GroupControlPatch, HaReading, HistorySettings, HomeAssistantSettings,
|
||||
HomeAssistantSettingsUpdate, HomeAssistantSettingsView, InfluxDbSettings,
|
||||
InfluxDbSettingsUpdate, InfluxDbSettingsView, ManualDeviceRequest, NightModeSettings,
|
||||
NotificationSettings, NotificationSettingsUpdate, NotificationSettingsView, Reading,
|
||||
NotificationSettings, NotificationSettingsUpdate, NotificationSettingsView, NetworkReading, Reading,
|
||||
RuntimeSettings, Schedule, SettingsSnapshot, TemporaryQuickThermostat,
|
||||
TemporaryQuickThermostatRequest, Zone,
|
||||
ZoneControlPatch, ZoneReading,
|
||||
@@ -77,6 +77,7 @@ const SPA_ROUTES: &[&str] = &[
|
||||
"/history/zones",
|
||||
"/history/devices",
|
||||
"/history/energy",
|
||||
"/history/network",
|
||||
"/history/sensors",
|
||||
"/history/custom",
|
||||
];
|
||||
@@ -156,6 +157,7 @@ pub fn router(state: AppState) -> Router {
|
||||
.route("/api/readings", get(readings))
|
||||
.route("/api/history", get(history))
|
||||
.route("/api/history/energy", get(energy_history))
|
||||
.route("/api/history/network", get(network_history))
|
||||
.route("/api/control-plan", get(control_plan))
|
||||
.route("/api/events", get(events))
|
||||
.route(
|
||||
|
||||
@@ -628,6 +628,9 @@ fn normalize_imported_runtime_settings(settings: &mut RuntimeSettings) -> Result
|
||||
settings.zone_interval_seconds = gree.zone_interval_seconds;
|
||||
settings.discovery_timeout_ms = gree.discovery_timeout_ms;
|
||||
settings.discovery_broadcast = gree.discovery_broadcast;
|
||||
settings.ping_metrics_enabled = gree.ping_metrics_enabled;
|
||||
settings.ping_interval_seconds = gree.ping_interval_seconds;
|
||||
settings.ping_sample_count = gree.ping_sample_count;
|
||||
settings.suppress_device_beep = gree.suppress_device_beep;
|
||||
settings.compressor_protection_enabled = gree.compressor_protection_enabled;
|
||||
settings.compressor_protection_seconds = gree.compressor_protection_seconds;
|
||||
@@ -637,6 +640,10 @@ fn normalize_imported_runtime_settings(settings: &mut RuntimeSettings) -> Result
|
||||
}
|
||||
settings.gree_cloud.polling_interval_seconds =
|
||||
settings.gree_cloud.polling_interval_seconds.clamp(30, 3600);
|
||||
settings.gree_cloud.connectivity_metrics_interval_seconds =
|
||||
settings.gree_cloud.connectivity_metrics_interval_seconds.clamp(30, 3600);
|
||||
settings.gree_cloud.connectivity_metrics_sample_count =
|
||||
settings.gree_cloud.connectivity_metrics_sample_count.clamp(1, 10);
|
||||
if settings.gree_cloud.account_id.trim().is_empty() {
|
||||
settings.gree_cloud.account_id = "default".into();
|
||||
}
|
||||
|
||||
@@ -775,3 +775,106 @@ async fn energy_history(
|
||||
"latest": latest,
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NetworkHistoryQuery {
|
||||
target_id: Option<String>,
|
||||
hours: Option<i64>,
|
||||
limit: Option<u32>,
|
||||
}
|
||||
|
||||
async fn combined_network_history(
|
||||
state: &AppState,
|
||||
target_id: Option<&str>,
|
||||
since: chrono::DateTime<Utc>,
|
||||
bucket_seconds: i64,
|
||||
limit: u32,
|
||||
) -> Result<(Vec<NetworkReading>, String, Option<String>), AppError> {
|
||||
let influx = state.settings.read().await.influxdb.clone();
|
||||
let cutoff = Utc::now() - ChronoDuration::days(influx.history_threshold_days.max(1) as i64);
|
||||
if !influx.enabled || since >= cutoff {
|
||||
return Ok((
|
||||
state.db.list_network_history(target_id, since, bucket_seconds, limit)?,
|
||||
"sqlite".into(),
|
||||
None,
|
||||
));
|
||||
}
|
||||
let mut warning = None;
|
||||
let mut values = match influxdb::query_network(
|
||||
&state.http,
|
||||
&influx,
|
||||
target_id,
|
||||
since,
|
||||
cutoff,
|
||||
bucket_seconds,
|
||||
limit,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(err) => {
|
||||
warning = Some(err.to_string());
|
||||
state.log(
|
||||
"warn",
|
||||
"influx.query_error",
|
||||
"InfluxDB connectivity history query failed",
|
||||
json!({"target_id": target_id, "error": err.to_string()}),
|
||||
);
|
||||
state.db.list_network_history(target_id, since, bucket_seconds, limit)?
|
||||
}
|
||||
};
|
||||
if warning.is_none() {
|
||||
values.extend(state.db.list_network_history(target_id, cutoff, bucket_seconds, limit)?);
|
||||
}
|
||||
values.sort_by_key(|row| row.timestamp);
|
||||
trim_history(&mut values, limit);
|
||||
Ok((
|
||||
values,
|
||||
if warning.is_some() { "sqlite_fallback".into() } else { "influx+sqlite".into() },
|
||||
warning,
|
||||
))
|
||||
}
|
||||
|
||||
async fn network_history(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<NetworkHistoryQuery>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let hours = query.hours.unwrap_or(24).clamp(1, 24 * 3650);
|
||||
let since = Utc::now() - ChronoDuration::hours(hours);
|
||||
let bucket_seconds = history_bucket_seconds(hours);
|
||||
let limit = query.limit.unwrap_or(20_000).clamp(1, 50_000);
|
||||
let target_id = query
|
||||
.target_id
|
||||
.as_deref()
|
||||
.filter(|value| !value.is_empty() && *value != "all");
|
||||
let (readings, storage, warning) =
|
||||
combined_network_history(&state, target_id, since, bucket_seconds, limit).await?;
|
||||
|
||||
let settings = state.settings.read().await.clone();
|
||||
let mut targets = Vec::<Value>::new();
|
||||
for device in state.db.list_devices()? {
|
||||
if device.enabled && device.connection_type == ConnectionType::Local && !device.simulated {
|
||||
targets.push(json!({
|
||||
"id": device.id,
|
||||
"name": device.name,
|
||||
"kind": "device",
|
||||
"source": "local_udp",
|
||||
}));
|
||||
}
|
||||
}
|
||||
let has_rest = readings.iter().any(|row| row.target_id == "cloud:rest");
|
||||
let has_mqtt = readings.iter().any(|row| row.target_id == "cloud:mqtt");
|
||||
if (settings.gree_cloud.enabled && settings.gree_cloud.connectivity_metrics_enabled) || has_rest {
|
||||
targets.push(json!({"id":"cloud:rest","name":"GREE Cloud REST","kind":"cloud_service","source":"cloud_rest"}));
|
||||
}
|
||||
if (settings.gree_cloud.enabled && settings.gree_cloud.connectivity_metrics_enabled) || has_mqtt {
|
||||
targets.push(json!({"id":"cloud:mqtt","name":"GREE Cloud MQTT","kind":"cloud_service","source":"cloud_mqtt"}));
|
||||
}
|
||||
Ok(Json(json!({
|
||||
"readings": readings,
|
||||
"targets": targets,
|
||||
"bucket_seconds": bucket_seconds,
|
||||
"storage": storage,
|
||||
"storage_warning": warning,
|
||||
})))
|
||||
}
|
||||
|
||||
+21
-1
@@ -11,6 +11,9 @@ fn gree_settings(settings: &RuntimeSettings) -> GreeSettings {
|
||||
zone_interval_seconds: settings.zone_interval_seconds,
|
||||
discovery_timeout_ms: settings.discovery_timeout_ms,
|
||||
discovery_broadcast: settings.discovery_broadcast.clone(),
|
||||
ping_metrics_enabled: settings.ping_metrics_enabled,
|
||||
ping_interval_seconds: settings.ping_interval_seconds,
|
||||
ping_sample_count: settings.ping_sample_count,
|
||||
suppress_device_beep: settings.suppress_device_beep,
|
||||
compressor_protection_enabled: settings.compressor_protection_enabled,
|
||||
compressor_protection_seconds: settings.compressor_protection_seconds,
|
||||
@@ -24,6 +27,9 @@ fn gree_cloud_settings(settings: &RuntimeSettings) -> GreeCloudSettingsView {
|
||||
username: settings.gree_cloud.username.clone(),
|
||||
password_configured: !settings.gree_cloud.password.is_empty(),
|
||||
polling_interval_seconds: settings.gree_cloud.polling_interval_seconds,
|
||||
connectivity_metrics_enabled: settings.gree_cloud.connectivity_metrics_enabled,
|
||||
connectivity_metrics_interval_seconds: settings.gree_cloud.connectivity_metrics_interval_seconds,
|
||||
connectivity_metrics_sample_count: settings.gree_cloud.connectivity_metrics_sample_count,
|
||||
installation_id: settings.gree_cloud.installation_id.clone(),
|
||||
account_id: settings.gree_cloud.account_id.clone(),
|
||||
last_successful_contact: settings.gree_cloud.last_successful_contact,
|
||||
@@ -138,6 +144,8 @@ fn normalize_gree_settings(mut input: GreeSettings) -> Result<GreeSettings, AppE
|
||||
input.poll_interval_seconds = input.poll_interval_seconds.clamp(2, 3600);
|
||||
input.zone_interval_seconds = input.zone_interval_seconds.clamp(2, 3600);
|
||||
input.discovery_timeout_ms = input.discovery_timeout_ms.clamp(300, 30_000);
|
||||
input.ping_interval_seconds = input.ping_interval_seconds.clamp(10, 3600);
|
||||
input.ping_sample_count = input.ping_sample_count.clamp(1, 10);
|
||||
input.compressor_protection_seconds = input.compressor_protection_seconds.clamp(30, 1800);
|
||||
if !(input.discovery_broadcast.eq_ignore_ascii_case("auto")
|
||||
|| input
|
||||
@@ -201,6 +209,9 @@ async fn update_gree_settings(
|
||||
settings.zone_interval_seconds = input.zone_interval_seconds;
|
||||
settings.discovery_timeout_ms = input.discovery_timeout_ms;
|
||||
settings.discovery_broadcast = input.discovery_broadcast;
|
||||
settings.ping_metrics_enabled = input.ping_metrics_enabled;
|
||||
settings.ping_interval_seconds = input.ping_interval_seconds;
|
||||
settings.ping_sample_count = input.ping_sample_count;
|
||||
settings.suppress_device_beep = input.suppress_device_beep;
|
||||
settings.compressor_protection_enabled = input.compressor_protection_enabled;
|
||||
settings.compressor_protection_seconds = input.compressor_protection_seconds;
|
||||
@@ -216,7 +227,10 @@ async fn update_gree_settings(
|
||||
"GREE settings updated",
|
||||
json!({
|
||||
"compressor_protection_enabled": payload.compressor_protection_enabled,
|
||||
"compressor_protection_seconds": payload.compressor_protection_seconds
|
||||
"compressor_protection_seconds": payload.compressor_protection_seconds,
|
||||
"ping_metrics_enabled": payload.ping_metrics_enabled,
|
||||
"ping_interval_seconds": payload.ping_interval_seconds,
|
||||
"ping_sample_count": payload.ping_sample_count
|
||||
}),
|
||||
);
|
||||
state.broadcast("settings.gree.updated", serde_json::to_value(&payload)?);
|
||||
@@ -246,6 +260,9 @@ fn apply_gree_cloud_update(
|
||||
next.region = input.region.trim().to_string();
|
||||
next.username = input.username.trim().to_string();
|
||||
next.polling_interval_seconds = input.polling_interval_seconds.clamp(30, 3600);
|
||||
next.connectivity_metrics_enabled = input.connectivity_metrics_enabled;
|
||||
next.connectivity_metrics_interval_seconds = input.connectivity_metrics_interval_seconds.clamp(30, 3600);
|
||||
next.connectivity_metrics_sample_count = input.connectivity_metrics_sample_count.clamp(1, 10);
|
||||
if let Some(password) = input.password {
|
||||
next.password = password;
|
||||
}
|
||||
@@ -292,6 +309,9 @@ async fn update_gree_cloud_settings(
|
||||
"enabled": payload.enabled,
|
||||
"region": payload.region,
|
||||
"polling_interval_seconds": payload.polling_interval_seconds,
|
||||
"connectivity_metrics_enabled": payload.connectivity_metrics_enabled,
|
||||
"connectivity_metrics_interval_seconds": payload.connectivity_metrics_interval_seconds,
|
||||
"connectivity_metrics_sample_count": payload.connectivity_metrics_sample_count,
|
||||
"password_configured": payload.password_configured
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -76,6 +76,9 @@ impl Config {
|
||||
zone_interval_seconds: self.zone_interval_seconds.max(2),
|
||||
discovery_timeout_ms: self.discovery_timeout_ms.clamp(300, 30_000),
|
||||
discovery_broadcast: self.discovery_broadcast.clone(),
|
||||
ping_metrics_enabled: env_bool("GREE_CONTROLLER_PING_METRICS_ENABLED").unwrap_or(true),
|
||||
ping_interval_seconds: env_u64("GREE_CONTROLLER_PING_INTERVAL_SECONDS").unwrap_or(60).clamp(10, 3600),
|
||||
ping_sample_count: env_u32("GREE_CONTROLLER_PING_SAMPLE_COUNT").unwrap_or(3).clamp(1, 10),
|
||||
house_mode: env::var("GREE_CONTROLLER_HOUSE_MODE").unwrap_or_else(|_| "cool".into()),
|
||||
control_strategy: "setpoint".into(),
|
||||
outdoor_assist_enabled: env_bool("GREE_CONTROLLER_OUTDOOR_ASSIST_ENABLED")
|
||||
@@ -137,6 +140,15 @@ impl Config {
|
||||
|
||||
/// Environment values explicitly supplied by the service override persisted runtime values.
|
||||
pub fn apply_runtime_env_overrides(&self, settings: &mut RuntimeSettings) {
|
||||
if let Some(value) = env_bool("GREE_CONTROLLER_PING_METRICS_ENABLED") {
|
||||
settings.ping_metrics_enabled = value;
|
||||
}
|
||||
if let Some(value) = env_u64("GREE_CONTROLLER_PING_INTERVAL_SECONDS") {
|
||||
settings.ping_interval_seconds = value.clamp(10, 3600);
|
||||
}
|
||||
if let Some(value) = env_u32("GREE_CONTROLLER_PING_SAMPLE_COUNT") {
|
||||
settings.ping_sample_count = value.clamp(1, 10);
|
||||
}
|
||||
if env::var_os("GREE_CONTROLLER_HISTORY_RETENTION_DAYS").is_some() {
|
||||
settings.history_retention_days = env_u32("GREE_CONTROLLER_HISTORY_RETENTION_DAYS")
|
||||
.unwrap_or(settings.history_retention_days)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
models::{
|
||||
ApiTokenInfo, Automation, ClimateGroup, ConfigurationExport, ConnectionType, Device, DeviceGroup, EnergySourcePreference, EventLog, Flow,
|
||||
EnergyReading, HaReading, Reading, RuntimeSettings, Schedule, Zone, ZoneReading,
|
||||
EnergyReading, HaReading, NetworkReading, Reading, RuntimeSettings, Schedule, Zone, ZoneReading,
|
||||
},
|
||||
queries,
|
||||
};
|
||||
@@ -29,6 +29,7 @@ include!("db/device_history.rs");
|
||||
include!("db/zone_history.rs");
|
||||
include!("db/ha_history.rs");
|
||||
include!("db/energy_history.rs");
|
||||
include!("db/network_history.rs");
|
||||
include!("db/events_tokens.rs");
|
||||
include!("db/configuration.rs");
|
||||
include!("db/tests.rs");
|
||||
|
||||
@@ -110,6 +110,7 @@ impl Db {
|
||||
let tx = conn.transaction()?;
|
||||
tx.execute(queries::DELETE_DEVICE_READINGS, [id])?;
|
||||
tx.execute(queries::DELETE_DEVICE_ENERGY_READINGS, [id])?;
|
||||
tx.execute("DELETE FROM network_readings WHERE target_id=?1", [id])?;
|
||||
tx.execute(queries::DELETE_ZONE_READINGS_BY_DEVICE_ID, [id])?;
|
||||
tx.execute(queries::DELETE_SCHEDULES_BY_DEVICE_ID, [id])?;
|
||||
tx.execute(queries::DELETE_ZONES_BY_DEVICE_ID, [id])?;
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
impl Db {
|
||||
pub fn add_network_reading(&self, reading: &NetworkReading) -> Result<i64> {
|
||||
let conn = self.lock()?;
|
||||
conn.execute(
|
||||
"INSERT INTO network_readings(target_id,target_kind,timestamp,latency_ms,jitter_ms,packet_loss_pct,sample_count,successful_samples,source) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9)",
|
||||
params![
|
||||
reading.target_id,
|
||||
reading.target_kind,
|
||||
reading.timestamp.to_rfc3339(),
|
||||
reading.latency_ms,
|
||||
reading.jitter_ms,
|
||||
reading.packet_loss_pct.clamp(0.0, 100.0),
|
||||
reading.sample_count as i64,
|
||||
reading.successful_samples as i64,
|
||||
reading.source,
|
||||
],
|
||||
)?;
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
pub fn list_network_history(
|
||||
&self,
|
||||
target_id: Option<&str>,
|
||||
since: DateTime<Utc>,
|
||||
bucket_seconds: i64,
|
||||
limit: u32,
|
||||
) -> Result<Vec<NetworkReading>> {
|
||||
let conn = self.lock()?;
|
||||
let bucket_seconds = bucket_seconds.max(1);
|
||||
let limit = limit.clamp(1, 50_000) as i64;
|
||||
let sql_by_target = r#"
|
||||
SELECT MIN(id),target_id,MAX(target_kind),MIN(timestamp),AVG(latency_ms),AVG(jitter_ms),AVG(packet_loss_pct),
|
||||
CAST(ROUND(AVG(sample_count)) AS INTEGER),CAST(ROUND(AVG(successful_samples)) AS INTEGER),MAX(source)
|
||||
FROM network_readings
|
||||
WHERE target_id=?1 AND timestamp>=?2
|
||||
GROUP BY target_id,source,CAST(unixepoch(timestamp)/?3 AS INTEGER)
|
||||
ORDER BY MIN(timestamp) ASC
|
||||
LIMIT ?4
|
||||
"#;
|
||||
let sql_all = r#"
|
||||
SELECT MIN(id),target_id,MAX(target_kind),MIN(timestamp),AVG(latency_ms),AVG(jitter_ms),AVG(packet_loss_pct),
|
||||
CAST(ROUND(AVG(sample_count)) AS INTEGER),CAST(ROUND(AVG(successful_samples)) AS INTEGER),MAX(source)
|
||||
FROM network_readings
|
||||
WHERE timestamp>=?1
|
||||
GROUP BY target_id,source,CAST(unixepoch(timestamp)/?2 AS INTEGER)
|
||||
ORDER BY MIN(timestamp) ASC
|
||||
LIMIT ?3
|
||||
"#;
|
||||
let mut out = Vec::new();
|
||||
if let Some(target_id) = target_id {
|
||||
let mut stmt = conn.prepare(sql_by_target)?;
|
||||
let rows = stmt.query_map(
|
||||
params![target_id, since.to_rfc3339(), bucket_seconds, limit],
|
||||
Self::map_network_reading,
|
||||
)?;
|
||||
for row in rows { out.push(row?); }
|
||||
} else {
|
||||
let mut stmt = conn.prepare(sql_all)?;
|
||||
let rows = stmt.query_map(
|
||||
params![since.to_rfc3339(), bucket_seconds, limit],
|
||||
Self::map_network_reading,
|
||||
)?;
|
||||
for row in rows { out.push(row?); }
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn network_before(&self, before: DateTime<Utc>, limit: u32) -> Result<Vec<NetworkReading>> {
|
||||
let conn = self.lock()?;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id,target_id,target_kind,timestamp,latency_ms,jitter_ms,packet_loss_pct,sample_count,successful_samples,source FROM network_readings WHERE timestamp<?1 ORDER BY timestamp ASC,id ASC LIMIT ?2",
|
||||
)?;
|
||||
let rows = stmt.query_map(
|
||||
params![before.to_rfc3339(), limit.clamp(1, 5000) as i64],
|
||||
Self::map_network_reading,
|
||||
)?;
|
||||
rows.collect::<rusqlite::Result<Vec<_>>>().map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn delete_network_batch(&self, rows: &[NetworkReading]) -> Result<u64> {
|
||||
let mut conn = self.lock()?;
|
||||
let tx = conn.transaction()?;
|
||||
let mut changed = 0_u64;
|
||||
for row in rows {
|
||||
changed += tx.execute("DELETE FROM network_readings WHERE id=?1", [row.id])? as u64;
|
||||
}
|
||||
tx.commit()?;
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
pub fn prune_network_readings(&self, retention_days: i64) -> Result<u64> {
|
||||
let before = Utc::now() - Duration::days(retention_days.max(1));
|
||||
let conn = self.lock()?;
|
||||
Ok(conn.execute("DELETE FROM network_readings WHERE timestamp < ?1", [before.to_rfc3339()])? as u64)
|
||||
}
|
||||
|
||||
pub fn compact_network_history(&self, retention_days: i64) -> Result<u64> {
|
||||
let now = Utc::now();
|
||||
let one_day = now - Duration::days(1);
|
||||
let seven_days = now - Duration::days(7);
|
||||
let retention = now - Duration::days(retention_days.max(1));
|
||||
let conn = self.lock()?;
|
||||
let sql = r#"
|
||||
DELETE FROM network_readings WHERE id IN (
|
||||
SELECT id FROM (
|
||||
SELECT id, ROW_NUMBER() OVER (
|
||||
PARTITION BY target_id,source,CAST(unixepoch(timestamp)/?1 AS INTEGER)
|
||||
ORDER BY timestamp DESC,id DESC
|
||||
) AS rn
|
||||
FROM network_readings WHERE timestamp < ?2 AND timestamp >= ?3
|
||||
) WHERE rn > 1
|
||||
)
|
||||
"#;
|
||||
let mut changed = 0_u64;
|
||||
for (bucket, older_than, newer_than) in [
|
||||
(600_i64, one_day, seven_days),
|
||||
(1800_i64, seven_days, retention),
|
||||
] {
|
||||
if older_than <= newer_than { continue; }
|
||||
changed += conn.execute(sql, params![bucket, older_than.to_rfc3339(), newer_than.to_rfc3339()])? as u64;
|
||||
}
|
||||
conn.execute_batch("PRAGMA optimize;")?;
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
fn map_network_reading(row: &rusqlite::Row<'_>) -> rusqlite::Result<NetworkReading> {
|
||||
let timestamp: String = row.get(3)?;
|
||||
Ok(NetworkReading {
|
||||
id: row.get(0)?,
|
||||
target_id: row.get(1)?,
|
||||
target_kind: row.get(2)?,
|
||||
timestamp: DateTime::parse_from_rfc3339(×tamp)
|
||||
.map(|value| value.with_timezone(&Utc))
|
||||
.unwrap_or_else(|_| Utc::now()),
|
||||
latency_ms: row.get(4)?,
|
||||
jitter_ms: row.get(5)?,
|
||||
packet_loss_pct: row.get::<_, f64>(6)?.clamp(0.0, 100.0),
|
||||
sample_count: row.get::<_, i64>(7)?.max(0) as u32,
|
||||
successful_samples: row.get::<_, i64>(8)?.max(0) as u32,
|
||||
source: row.get(9)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -3,7 +3,7 @@ use crate::{
|
||||
home_assistant, influxdb,
|
||||
models::{
|
||||
Automation, AutomationPlanRule, ClimateGroup, ConnectionStatus, ConnectionType, ControlPlan, ControlPlanEvent, Device, DeviceGroup, EnergyReading, EnergySourcePreference,
|
||||
DeviceCommand, GroupControlPatch, HaReading, NightModeSettings, Reading, RuntimeSettings,
|
||||
DeviceCommand, GroupControlPatch, HaReading, NetworkReading, NightModeSettings, Reading, RuntimeSettings,
|
||||
Schedule, TemporaryQuickThermostat, Zone, ZoneControlPlan, ZoneReading,
|
||||
},
|
||||
state::{AppState, ControlPlanSnapshot, PendingControllerCommand},
|
||||
@@ -30,6 +30,7 @@ include!("engine/groups.rs");
|
||||
include!("engine/zone_actions.rs");
|
||||
include!("engine/zone_control.rs");
|
||||
include!("engine/history.rs");
|
||||
include!("engine/connectivity.rs");
|
||||
include!("engine/energy.rs");
|
||||
include!("engine/temperature.rs");
|
||||
include!("engine/targets.rs");
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
fn build_network_reading(
|
||||
target_id: String,
|
||||
target_kind: &str,
|
||||
source: &str,
|
||||
samples: Vec<Option<u64>>,
|
||||
) -> NetworkReading {
|
||||
let sample_count = samples.len() as u32;
|
||||
let successful: Vec<f64> = samples
|
||||
.iter()
|
||||
.filter_map(|value| value.map(|v| v as f64))
|
||||
.collect();
|
||||
let successful_samples = successful.len() as u32;
|
||||
let latency_ms = if successful.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(successful.iter().sum::<f64>() / successful.len() as f64)
|
||||
};
|
||||
let jitter_ms = if successful.len() < 2 {
|
||||
if successful.is_empty() { None } else { Some(0.0) }
|
||||
} else {
|
||||
let diffs = successful
|
||||
.windows(2)
|
||||
.map(|pair| (pair[1] - pair[0]).abs())
|
||||
.collect::<Vec<_>>();
|
||||
Some(diffs.iter().sum::<f64>() / diffs.len() as f64)
|
||||
};
|
||||
let packet_loss_pct = if sample_count == 0 {
|
||||
0.0
|
||||
} else {
|
||||
((sample_count - successful_samples) as f64 / sample_count as f64) * 100.0
|
||||
};
|
||||
NetworkReading {
|
||||
id: 0,
|
||||
target_id,
|
||||
target_kind: target_kind.into(),
|
||||
timestamp: Utc::now(),
|
||||
latency_ms,
|
||||
jitter_ms,
|
||||
packet_loss_pct,
|
||||
sample_count,
|
||||
successful_samples,
|
||||
source: source.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn save_network_reading(state: &AppState, reading: NetworkReading) {
|
||||
match state.db.add_network_reading(&reading) {
|
||||
Ok(_) => queue_influx_network(state, reading),
|
||||
Err(err) => tracing::warn!(error=?err, target_id=%reading.target_id, "cannot save connectivity metric"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn probe_local_device_batch(state: &AppState, device: &Device, sample_count: u32) {
|
||||
let mut samples = Vec::with_capacity(sample_count as usize);
|
||||
for sample in 0..sample_count {
|
||||
let result = state.providers.local().client().probe(device).await;
|
||||
match result {
|
||||
Ok(ms) => samples.push(Some(ms)),
|
||||
Err(err) => {
|
||||
tracing::debug!(error=?err, device_id=%device.id, "local connectivity probe failed");
|
||||
samples.push(None);
|
||||
}
|
||||
}
|
||||
if sample + 1 < sample_count {
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
save_network_reading(
|
||||
state,
|
||||
build_network_reading(device.id.clone(), "device", "local_udp", samples),
|
||||
);
|
||||
}
|
||||
|
||||
async fn run_local_connectivity_cycle(state: &AppState, sample_count: u32) -> Result<()> {
|
||||
let devices = state
|
||||
.db
|
||||
.list_devices()?
|
||||
.into_iter()
|
||||
.filter(|device| {
|
||||
device.enabled
|
||||
&& !device.simulated
|
||||
&& device.connection_type == ConnectionType::Local
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
futures_util::future::join_all(
|
||||
devices
|
||||
.iter()
|
||||
.map(|device| probe_local_device_batch(state, device, sample_count)),
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cloud_rest_probe(state: &AppState, cloud: &crate::models::GreeCloudSettings) -> Result<u64> {
|
||||
let mut api = crate::protocol::gree_cloud::GreeCloudApi::for_region(
|
||||
state.http.clone(),
|
||||
&cloud.region,
|
||||
&cloud.username,
|
||||
&cloud.password,
|
||||
)?;
|
||||
let started = Instant::now();
|
||||
api.login().await?;
|
||||
Ok(started.elapsed().as_millis().min(u64::MAX as u128) as u64)
|
||||
}
|
||||
|
||||
async fn run_cloud_connectivity_cycle(
|
||||
state: &AppState,
|
||||
cloud: &crate::models::GreeCloudSettings,
|
||||
sample_count: u32,
|
||||
) -> Result<()> {
|
||||
if !cloud.enabled || cloud.username.trim().is_empty() || cloud.password.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let devices = state.db.list_devices()?;
|
||||
let mut rest = Vec::with_capacity(sample_count as usize);
|
||||
let mut mqtt = Vec::with_capacity(sample_count as usize);
|
||||
for sample in 0..sample_count {
|
||||
match cloud_rest_probe(state, cloud).await {
|
||||
Ok(ms) => rest.push(Some(ms)),
|
||||
Err(err) => {
|
||||
tracing::debug!(error=?err, "GREE Cloud REST connectivity probe failed");
|
||||
rest.push(None);
|
||||
}
|
||||
}
|
||||
match state.providers.cloud().probe_mqtt(cloud, &devices).await {
|
||||
Ok(ms) => mqtt.push(Some(ms)),
|
||||
Err(err) => {
|
||||
tracing::debug!(error=?err, "GREE Cloud MQTT connectivity probe failed");
|
||||
mqtt.push(None);
|
||||
}
|
||||
}
|
||||
if sample + 1 < sample_count {
|
||||
sleep(Duration::from_millis(150)).await;
|
||||
}
|
||||
}
|
||||
save_network_reading(
|
||||
state,
|
||||
build_network_reading("cloud:rest".into(), "cloud_service", "cloud_rest", rest),
|
||||
);
|
||||
save_network_reading(
|
||||
state,
|
||||
build_network_reading("cloud:mqtt".into(), "cloud_service", "cloud_mqtt", mqtt),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn local_connectivity_loop(state: AppState) {
|
||||
sleep(Duration::from_secs(5)).await;
|
||||
loop {
|
||||
let settings = state.settings.read().await.clone();
|
||||
if settings.ping_metrics_enabled {
|
||||
if let Err(err) = run_local_connectivity_cycle(&state, settings.ping_sample_count.clamp(1, 10)).await {
|
||||
tracing::warn!(error=?err, "local connectivity metrics cycle failed");
|
||||
}
|
||||
sleep(Duration::from_secs(settings.ping_interval_seconds.clamp(10, 3600))).await;
|
||||
} else {
|
||||
sleep(Duration::from_secs(10)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cloud_connectivity_loop(state: AppState) {
|
||||
sleep(Duration::from_secs(10)).await;
|
||||
loop {
|
||||
let cloud = state.settings.read().await.gree_cloud.clone();
|
||||
if cloud.connectivity_metrics_enabled {
|
||||
if let Err(err) = run_cloud_connectivity_cycle(
|
||||
&state,
|
||||
&cloud,
|
||||
cloud.connectivity_metrics_sample_count.clamp(1, 10),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error=?err, "GREE Cloud connectivity metrics cycle failed");
|
||||
}
|
||||
sleep(Duration::from_secs(
|
||||
cloud.connectivity_metrics_interval_seconds.clamp(30, 3600),
|
||||
))
|
||||
.await;
|
||||
} else {
|
||||
sleep(Duration::from_secs(15)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,19 @@ fn queue_influx_ha(state: &AppState, reading: HaReading) {
|
||||
});
|
||||
}
|
||||
|
||||
fn queue_influx_network(state: &AppState, reading: NetworkReading) {
|
||||
let state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
let settings = state.settings.read().await.influxdb.clone();
|
||||
if !settings.enabled {
|
||||
return;
|
||||
}
|
||||
if let Err(err) = influxdb::write_network(&state.http, &settings, &reading).await {
|
||||
tracing::warn!(error=?err, target_id=%reading.target_id, source=%reading.source, "cannot write connectivity metric to InfluxDB");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn median_outdoor_temperature(mut values: Vec<f64>) -> Option<f64> {
|
||||
if values.is_empty() {
|
||||
return None;
|
||||
|
||||
@@ -46,6 +46,15 @@ pub fn start(state: AppState) {
|
||||
home_assistant_energy_loop(ha_energy_state).await;
|
||||
});
|
||||
|
||||
let local_connectivity_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
local_connectivity_loop(local_connectivity_state).await;
|
||||
});
|
||||
let cloud_connectivity_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
cloud_connectivity_loop(cloud_connectivity_state).await;
|
||||
});
|
||||
|
||||
let control_plan_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
control_plan_cache_loop(control_plan_state).await;
|
||||
@@ -139,6 +148,11 @@ pub fn start(state: AppState) {
|
||||
Ok(_) => {}
|
||||
Err(err) => tracing::warn!(error=?err, "cannot compact history"),
|
||||
}
|
||||
match maintenance_state.db.compact_network_history(compaction_days) {
|
||||
Ok(count) if count > 0 => tracing::info!(count, "connectivity history samples compacted"),
|
||||
Ok(_) => {}
|
||||
Err(err) => tracing::warn!(error=?err, "cannot compact connectivity history"),
|
||||
}
|
||||
}
|
||||
if settings.influxdb.enabled {
|
||||
match archive_old_history(
|
||||
@@ -172,6 +186,13 @@ pub fn start(state: AppState) {
|
||||
Ok(_) => {}
|
||||
Err(err) => tracing::warn!(error=?err, "cannot prune energy readings"),
|
||||
}
|
||||
match maintenance_state.db.prune_network_readings(retention_days) {
|
||||
Ok(count) if count > 0 => {
|
||||
tracing::info!(count, retention_days, "old connectivity readings pruned")
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(err) => tracing::warn!(error=?err, "cannot prune connectivity readings"),
|
||||
}
|
||||
}
|
||||
let event_retention_days = settings.event_log_retention_days.max(1) as i64;
|
||||
match maintenance_state.db.prune_events(event_retention_days) {
|
||||
@@ -216,5 +237,17 @@ async fn archive_old_history(state: &AppState, threshold_days: u32) -> Result<u6
|
||||
break;
|
||||
}
|
||||
}
|
||||
for _ in 0..50 {
|
||||
let network = state.db.network_before(cutoff, 1_000)?;
|
||||
if network.is_empty() {
|
||||
break;
|
||||
}
|
||||
influxdb::write_network_batch(&state.http, &settings, &network).await?;
|
||||
let deleted = state.db.delete_network_batch(&network)?;
|
||||
moved += deleted;
|
||||
if deleted == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(moved)
|
||||
}
|
||||
|
||||
+2
-1
@@ -4,12 +4,13 @@ use reqwest::Client;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::models::{EnergyReading, HaReading, InfluxDbSettings, Reading, ZoneReading};
|
||||
use crate::models::{EnergyReading, HaReading, InfluxDbSettings, NetworkReading, Reading, ZoneReading};
|
||||
|
||||
const DEVICE_MEASUREMENT: &str = "gree_device";
|
||||
const ZONE_MEASUREMENT: &str = "gree_zone";
|
||||
const HA_MEASUREMENT: &str = "gree_ha";
|
||||
const ENERGY_MEASUREMENT: &str = "gree_energy";
|
||||
const NETWORK_MEASUREMENT: &str = "gree_network";
|
||||
|
||||
// Functional source split intentionally keeps items in the existing module namespace.
|
||||
include!("influxdb/write.rs");
|
||||
|
||||
@@ -544,3 +544,81 @@ pub async fn query_energy(
|
||||
out.sort_by_key(|row| row.timestamp);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub async fn query_network(
|
||||
client: &Client,
|
||||
settings: &InfluxDbSettings,
|
||||
target_id: Option<&str>,
|
||||
start: DateTime<Utc>,
|
||||
stop: DateTime<Utc>,
|
||||
bucket_seconds: i64,
|
||||
limit: u32,
|
||||
) -> Result<Vec<NetworkReading>> {
|
||||
if settings.version == "1" {
|
||||
let filter = target_id
|
||||
.map(|id| format!(" AND \"target_id\"='{}'", influxql_string(id)))
|
||||
.unwrap_or_default();
|
||||
let q = format!(
|
||||
"SELECT mean(\"latency_ms\") AS \"latency_ms\",mean(\"jitter_ms\") AS \"jitter_ms\",mean(\"packet_loss_pct\") AS \"packet_loss_pct\",mean(\"sample_count\") AS \"sample_count\",mean(\"successful_samples\") AS \"successful_samples\" FROM \"{NETWORK_MEASUREMENT}\" WHERE time >= '{}' AND time < '{}'{} GROUP BY time({}s),\"target_id\",\"target_kind\",\"source\" fill(none) LIMIT {}",
|
||||
start.to_rfc3339(), stop.to_rfc3339(), filter, bucket_seconds.max(1), limit
|
||||
);
|
||||
let series = query_v1(client, settings, &q).await?;
|
||||
let mut out = Vec::new();
|
||||
for item in series {
|
||||
let id = item.tags.get("target_id").cloned().unwrap_or_default();
|
||||
let kind = item.tags.get("target_kind").cloned().unwrap_or_else(|| "device".into());
|
||||
let source = item.tags.get("source").cloned().unwrap_or_default();
|
||||
for row in item.rows {
|
||||
let Some(timestamp) = row_time(&row) else { continue; };
|
||||
out.push(NetworkReading {
|
||||
id: 0,
|
||||
target_id: id.clone(),
|
||||
target_kind: kind.clone(),
|
||||
timestamp,
|
||||
latency_ms: row_num(&row, "latency_ms"),
|
||||
jitter_ms: row_num(&row, "jitter_ms"),
|
||||
packet_loss_pct: row_num(&row, "packet_loss_pct").unwrap_or(0.0).clamp(0.0, 100.0),
|
||||
sample_count: row_num(&row, "sample_count").unwrap_or(0.0).round().max(0.0) as u32,
|
||||
successful_samples: row_num(&row, "successful_samples").unwrap_or(0.0).round().max(0.0) as u32,
|
||||
source: source.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
out.sort_by_key(|row| row.timestamp);
|
||||
out.truncate(limit as usize);
|
||||
return Ok(out);
|
||||
}
|
||||
|
||||
let tags = target_id
|
||||
.map(|value| format!(" |> filter(fn: (r) => r.target_id == {})", flux_string(value)))
|
||||
.unwrap_or_default();
|
||||
let query = flux_query(
|
||||
settings,
|
||||
NETWORK_MEASUREMENT,
|
||||
&tags,
|
||||
&["target_id", "target_kind", "source"],
|
||||
start,
|
||||
stop,
|
||||
bucket_seconds,
|
||||
);
|
||||
let rows = query_v2(client, settings, &query).await?;
|
||||
let mut out = Vec::new();
|
||||
for row in rows.into_iter().take(limit as usize) {
|
||||
let Some(timestamp) = parse_flux_time(&row) else { continue; };
|
||||
let Some(id) = row.get("target_id").filter(|v| !v.is_empty()) else { continue; };
|
||||
out.push(NetworkReading {
|
||||
id: 0,
|
||||
target_id: id.clone(),
|
||||
target_kind: row.get("target_kind").cloned().unwrap_or_else(|| "device".into()),
|
||||
timestamp,
|
||||
latency_ms: row_f64(&row, "latency_ms"),
|
||||
jitter_ms: row_f64(&row, "jitter_ms"),
|
||||
packet_loss_pct: row_f64(&row, "packet_loss_pct").unwrap_or(0.0).clamp(0.0, 100.0),
|
||||
sample_count: row_f64(&row, "sample_count").unwrap_or(0.0).round().max(0.0) as u32,
|
||||
successful_samples: row_f64(&row, "successful_samples").unwrap_or(0.0).round().max(0.0) as u32,
|
||||
source: row.get("source").cloned().unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
out.sort_by_key(|row| row.timestamp);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,50 @@
|
||||
|
||||
pub async fn write_network(
|
||||
client: &Client,
|
||||
settings: &InfluxDbSettings,
|
||||
reading: &NetworkReading,
|
||||
) -> Result<()> {
|
||||
if !settings.enabled { return Ok(()); }
|
||||
let mut fields = Vec::new();
|
||||
push_float(&mut fields, "latency_ms", reading.latency_ms);
|
||||
push_float(&mut fields, "jitter_ms", reading.jitter_ms);
|
||||
push_float(&mut fields, "packet_loss_pct", Some(reading.packet_loss_pct.clamp(0.0, 100.0)));
|
||||
push_int(&mut fields, "sample_count", reading.sample_count as i64);
|
||||
push_int(&mut fields, "successful_samples", reading.successful_samples as i64);
|
||||
let line = line_protocol(
|
||||
NETWORK_MEASUREMENT,
|
||||
&[("target_id", &reading.target_id), ("target_kind", &reading.target_kind), ("source", &reading.source)],
|
||||
fields,
|
||||
reading.timestamp,
|
||||
)?;
|
||||
write_line(client, settings, line).await
|
||||
}
|
||||
|
||||
pub async fn write_network_batch(
|
||||
client: &Client,
|
||||
settings: &InfluxDbSettings,
|
||||
readings: &[NetworkReading],
|
||||
) -> Result<()> {
|
||||
if !settings.enabled || readings.is_empty() { return Ok(()); }
|
||||
let mut lines = Vec::with_capacity(readings.len());
|
||||
for reading in readings {
|
||||
let mut fields = Vec::new();
|
||||
push_float(&mut fields, "latency_ms", reading.latency_ms);
|
||||
push_float(&mut fields, "jitter_ms", reading.jitter_ms);
|
||||
push_float(&mut fields, "packet_loss_pct", Some(reading.packet_loss_pct.clamp(0.0, 100.0)));
|
||||
push_int(&mut fields, "sample_count", reading.sample_count as i64);
|
||||
push_int(&mut fields, "successful_samples", reading.successful_samples as i64);
|
||||
lines.push(line_protocol(
|
||||
NETWORK_MEASUREMENT,
|
||||
&[("target_id", &reading.target_id), ("target_kind", &reading.target_kind), ("source", &reading.source)],
|
||||
fields,
|
||||
reading.timestamp,
|
||||
)?);
|
||||
}
|
||||
write_lines(client, settings, lines.join("\n")).await
|
||||
}
|
||||
|
||||
|
||||
pub async fn write_energy(
|
||||
client: &Client,
|
||||
settings: &InfluxDbSettings,
|
||||
|
||||
@@ -88,6 +88,16 @@ fn default_cloud_region() -> String {
|
||||
fn default_cloud_poll_interval_seconds() -> u64 {
|
||||
60
|
||||
}
|
||||
|
||||
fn default_ping_interval_seconds() -> u64 {
|
||||
60
|
||||
}
|
||||
fn default_ping_sample_count() -> u32 {
|
||||
3
|
||||
}
|
||||
fn default_cloud_metrics_interval_seconds() -> u64 {
|
||||
300
|
||||
}
|
||||
fn default_influx_version() -> String {
|
||||
"2".into()
|
||||
}
|
||||
|
||||
@@ -40,6 +40,27 @@ pub struct HaReading {
|
||||
pub temperature: f64,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NetworkReading {
|
||||
pub id: i64,
|
||||
/// Stable endpoint id: local device id or a controller-level Cloud service id.
|
||||
pub target_id: String,
|
||||
/// `device` for Local/LAN units, `cloud_service` for REST/MQTT diagnostics.
|
||||
pub target_kind: String,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
/// Mean successful round-trip latency for this probe batch.
|
||||
pub latency_ms: Option<f64>,
|
||||
/// Mean absolute difference between consecutive successful RTT samples.
|
||||
pub jitter_ms: Option<f64>,
|
||||
/// Failed probes divided by all probes, in percent (0..100).
|
||||
pub packet_loss_pct: f64,
|
||||
pub sample_count: u32,
|
||||
pub successful_samples: u32,
|
||||
/// `local_udp`, `cloud_rest` or `cloud_mqtt`.
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EventLog {
|
||||
pub id: i64,
|
||||
|
||||
@@ -10,6 +10,13 @@ pub struct GreeCloudSettings {
|
||||
pub password: String,
|
||||
#[serde(default = "default_cloud_poll_interval_seconds")]
|
||||
pub polling_interval_seconds: u64,
|
||||
/// Periodic Cloud REST/MQTT quality measurements for History. Disabled by default.
|
||||
#[serde(default)]
|
||||
pub connectivity_metrics_enabled: bool,
|
||||
#[serde(default = "default_cloud_metrics_interval_seconds")]
|
||||
pub connectivity_metrics_interval_seconds: u64,
|
||||
#[serde(default = "default_ping_sample_count")]
|
||||
pub connectivity_metrics_sample_count: u32,
|
||||
/// Random, non-personal installation identifier used in the HTTP User-Agent.
|
||||
#[serde(default)]
|
||||
pub installation_id: String,
|
||||
@@ -31,6 +38,9 @@ impl Default for GreeCloudSettings {
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
polling_interval_seconds: default_cloud_poll_interval_seconds(),
|
||||
connectivity_metrics_enabled: false,
|
||||
connectivity_metrics_interval_seconds: default_cloud_metrics_interval_seconds(),
|
||||
connectivity_metrics_sample_count: default_ping_sample_count(),
|
||||
installation_id: String::new(),
|
||||
account_id: "default".into(),
|
||||
last_successful_contact: None,
|
||||
@@ -47,6 +57,13 @@ pub struct RuntimeSettings {
|
||||
pub zone_interval_seconds: u64,
|
||||
pub discovery_timeout_ms: u64,
|
||||
pub discovery_broadcast: String,
|
||||
/// Background Local/LAN round-trip quality worker.
|
||||
#[serde(default = "default_true")]
|
||||
pub ping_metrics_enabled: bool,
|
||||
#[serde(default = "default_ping_interval_seconds")]
|
||||
pub ping_interval_seconds: u64,
|
||||
#[serde(default = "default_ping_sample_count")]
|
||||
pub ping_sample_count: u32,
|
||||
/// Global seasonal mode. Zones follow this by default. Values: cool/heat/off; off pauses house-level thermostat control.
|
||||
#[serde(default = "default_house_mode")]
|
||||
pub house_mode: String,
|
||||
|
||||
@@ -10,6 +10,12 @@ pub struct GreeSettings {
|
||||
pub zone_interval_seconds: u64,
|
||||
pub discovery_timeout_ms: u64,
|
||||
pub discovery_broadcast: String,
|
||||
#[serde(default = "default_true")]
|
||||
pub ping_metrics_enabled: bool,
|
||||
#[serde(default = "default_ping_interval_seconds")]
|
||||
pub ping_interval_seconds: u64,
|
||||
#[serde(default = "default_ping_sample_count")]
|
||||
pub ping_sample_count: u32,
|
||||
pub suppress_device_beep: bool,
|
||||
pub compressor_protection_enabled: bool,
|
||||
pub compressor_protection_seconds: u64,
|
||||
@@ -23,6 +29,12 @@ pub struct GreeCloudSettingsUpdate {
|
||||
#[serde(default)]
|
||||
pub password: Option<String>,
|
||||
pub polling_interval_seconds: u64,
|
||||
#[serde(default)]
|
||||
pub connectivity_metrics_enabled: bool,
|
||||
#[serde(default = "default_cloud_metrics_interval_seconds")]
|
||||
pub connectivity_metrics_interval_seconds: u64,
|
||||
#[serde(default = "default_ping_sample_count")]
|
||||
pub connectivity_metrics_sample_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -32,6 +44,9 @@ pub struct GreeCloudSettingsView {
|
||||
pub username: String,
|
||||
pub password_configured: bool,
|
||||
pub polling_interval_seconds: u64,
|
||||
pub connectivity_metrics_enabled: bool,
|
||||
pub connectivity_metrics_interval_seconds: u64,
|
||||
pub connectivity_metrics_sample_count: u32,
|
||||
pub installation_id: String,
|
||||
pub account_id: String,
|
||||
pub last_successful_contact: Option<DateTime<Utc>>,
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
atomic::{AtomicBool, AtomicU16, AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
|
||||
@@ -81,6 +81,7 @@ enum WireCommand {
|
||||
pub struct MqttConnection {
|
||||
tx: mpsc::Sender<WireCommand>,
|
||||
connected: Arc<AtomicBool>,
|
||||
events: broadcast::Sender<MqttEvent>,
|
||||
}
|
||||
|
||||
impl MqttConnection {
|
||||
@@ -168,6 +169,7 @@ impl MqttConnection {
|
||||
Ok(Self {
|
||||
tx,
|
||||
connected,
|
||||
events,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -211,6 +213,33 @@ impl MqttConnection {
|
||||
.map_err(|_| anyhow!("GREE Cloud MQTT writer stopped"))
|
||||
}
|
||||
|
||||
pub async fn ping_round_trip(&self) -> Result<u64> {
|
||||
if !self.is_connected() {
|
||||
bail!("GREE Cloud MQTT is not connected");
|
||||
}
|
||||
let mut events = self.events.subscribe();
|
||||
let started = Instant::now();
|
||||
timeout(MQTT_QUEUE_TIMEOUT, self.tx.send(WireCommand::Ping))
|
||||
.await
|
||||
.context("GREE Cloud MQTT ping queue timed out")?
|
||||
.map_err(|_| anyhow!("GREE Cloud MQTT writer stopped"))?;
|
||||
let response = timeout(Duration::from_secs(5), async {
|
||||
loop {
|
||||
match events.recv().await {
|
||||
Ok(MqttEvent::Traffic { kind: "PINGRESP" }) => break Ok(()),
|
||||
Ok(MqttEvent::Disconnected { reason }) => break Err(anyhow!("{}", reason)),
|
||||
Ok(_) => {}
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => {}
|
||||
Err(broadcast::error::RecvError::Closed) => break Err(anyhow!("GREE Cloud MQTT event stream closed")),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.context("GREE Cloud MQTT PINGRESP timed out")?;
|
||||
response?;
|
||||
Ok(started.elapsed().as_millis().min(u64::MAX as u128) as u64)
|
||||
}
|
||||
|
||||
pub async fn disconnect(&self) {
|
||||
// Mark disconnected first so no new work can enter the queue while shutdown is in
|
||||
// progress. A wedged/full writer queue must never prevent process termination.
|
||||
|
||||
@@ -614,6 +614,12 @@ impl GreeCloudProvider {
|
||||
.ok_or_else(|| anyhow!("GREE Cloud MQTT is disconnected"))
|
||||
}
|
||||
|
||||
pub async fn probe_mqtt(&self, settings: &GreeCloudSettings, devices: &[Device]) -> Result<u64> {
|
||||
self.ensure_connected(settings, devices).await?;
|
||||
let session = self.current_session().await?;
|
||||
session.mqtt.ping_round_trip().await
|
||||
}
|
||||
|
||||
pub async fn ensure_connected(&self, settings: &GreeCloudSettings, devices: &[Device]) -> Result<()> {
|
||||
if !settings.enabled {
|
||||
bail!("GREE Cloud is disabled");
|
||||
|
||||
@@ -129,6 +129,24 @@ CREATE INDEX IF NOT EXISTS energy_readings_device_time_idx
|
||||
CREATE INDEX IF NOT EXISTS energy_readings_source_time_idx
|
||||
ON energy_readings(device_id, source, timestamp DESC);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS network_readings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
target_id TEXT NOT NULL,
|
||||
target_kind TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
latency_ms REAL,
|
||||
jitter_ms REAL,
|
||||
packet_loss_pct REAL NOT NULL CHECK(packet_loss_pct >= 0 AND packet_loss_pct <= 100),
|
||||
sample_count INTEGER NOT NULL,
|
||||
successful_samples INTEGER NOT NULL,
|
||||
source TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS network_readings_target_time_idx
|
||||
ON network_readings(target_id, timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS network_readings_source_time_idx
|
||||
ON network_readings(source, timestamp DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS event_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp TEXT NOT NULL,
|
||||
@@ -163,5 +181,7 @@ INSERT OR IGNORE INTO schema_migrations(version, applied_at)
|
||||
VALUES (7, strftime('%Y-%m-%dT%H:%M:%fZ','now'));
|
||||
INSERT OR IGNORE INTO schema_migrations(version, applied_at)
|
||||
VALUES (8, strftime('%Y-%m-%dT%H:%M:%fZ','now'));
|
||||
INSERT OR IGNORE INTO schema_migrations(version, applied_at)
|
||||
VALUES (9, strftime('%Y-%m-%dT%H:%M:%fZ','now'));
|
||||
"#;
|
||||
|
||||
|
||||
@@ -327,6 +327,7 @@
|
||||
<button data-history-tab="zones" data-i18n="history.tabZones">Zones</button>
|
||||
<button data-history-tab="devices" data-i18n="history.tabDevices">GREE devices</button>
|
||||
<button data-history-tab="energy" data-i18n="energy.title">Energy</button>
|
||||
<button data-history-tab="network" data-i18n="history.tabNetwork">Pings</button>
|
||||
<button data-history-tab="sensors" data-i18n="history.tabSensors">HA sensors</button>
|
||||
<button data-history-tab="custom" data-i18n="history.tabCustom">Custom chart</button>
|
||||
</div>
|
||||
@@ -711,6 +712,20 @@
|
||||
name="discovery_timeout_ms" min="300" max="30000" required></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<div>
|
||||
<h3 data-i18n="settings.connectivityTitle">Connectivity metrics</h3>
|
||||
<p data-i18n="settings.connectivityHint">Periodic GREE UDP round-trip measurements for Local/LAN units. Results are stored in History.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<label class="check wide"><input type="checkbox" name="ping_metrics_enabled"> <span data-i18n="settings.connectivityEnable">Enable local connectivity metrics</span></label>
|
||||
<label><span data-i18n="settings.connectivityInterval">Measurement interval (s)</span><input type="number" name="ping_interval_seconds" min="10" max="3600" value="60"></label>
|
||||
<label><span data-i18n="settings.connectivitySamples">Samples per measurement</span><input type="number" name="ping_sample_count" min="1" max="10" value="3"></label>
|
||||
<p class="field-note wide" data-i18n="settings.connectivityLocalOnly">Only Local/LAN installations are probed. Cloud devices are not pinged directly.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel settings-block">
|
||||
<div class="settings-block-head">
|
||||
<div>
|
||||
@@ -751,6 +766,10 @@
|
||||
<option>Australia</option><option>China Mainland</option><option>East South Asia</option><option selected>Europe</option><option>India</option><option>Latin American</option><option>Middle East</option><option>North American</option><option>Russia</option><option>South American</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="settings.cloudPolling">Cloud fallback polling interval (s)</span><input type="number" name="gree_cloud_polling_interval_seconds" min="30" max="3600" value="60"></label>
|
||||
<label class="check wide"><input type="checkbox" name="gree_cloud_connectivity_metrics_enabled"> <span data-i18n="settings.cloudConnectivityEnable">Measure Cloud REST/MQTT connectivity</span></label>
|
||||
<label><span data-i18n="settings.connectivityInterval">Measurement interval (s)</span><input type="number" name="gree_cloud_connectivity_metrics_interval_seconds" min="30" max="3600" value="300"></label>
|
||||
<label><span data-i18n="settings.connectivitySamples">Samples per measurement</span><input type="number" name="gree_cloud_connectivity_metrics_sample_count" min="1" max="10" value="3"></label>
|
||||
<p class="field-note wide" data-i18n="settings.cloudConnectivityHint">Optional. Disabled by default. Measures REST login response and MQTT PINGRESP round-trip.</p>
|
||||
<label class="wide"><span data-i18n="settings.cloudLogin">Login / email</span><input name="gree_cloud_username" autocomplete="username"></label>
|
||||
<label class="wide"><span data-i18n="settings.cloudPassword">Password</span><input type="password" name="gree_cloud_password" autocomplete="new-password" data-i18n-placeholder="settings.cloudPasswordPlaceholder" placeholder="Leave empty to keep saved secret"></label>
|
||||
<div class="wide cloud-runtime-summary" aria-live="polite" hidden>
|
||||
|
||||
Vendored
+3
@@ -15,6 +15,9 @@ function settingsFromSnapshot(sections, houseMode = app.settings?.house_mode ||
|
||||
zone_interval_seconds: Number(gree.zone_interval_seconds),
|
||||
discovery_timeout_ms: Number(gree.discovery_timeout_ms),
|
||||
discovery_broadcast: gree.discovery_broadcast,
|
||||
ping_metrics_enabled: gree.ping_metrics_enabled !== false,
|
||||
ping_interval_seconds: Number(gree.ping_interval_seconds || 60),
|
||||
ping_sample_count: Number(gree.ping_sample_count || 3),
|
||||
suppress_device_beep: !!gree.suppress_device_beep,
|
||||
gree_cloud: greeCloud,
|
||||
compressor_protection_enabled: gree.compressor_protection_enabled !== false,
|
||||
|
||||
+23
-8
@@ -106,6 +106,11 @@ function renderHistoryNavigation() {
|
||||
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 === 'network') {
|
||||
const targets = app.historyNetworkTargets || [];
|
||||
const targetOptions = targets.map(target => `<option value="${esc(target.id)}">${esc(target.name)}</option>`).join('');
|
||||
host.innerHTML = `<label><span>${esc(tr('history.networkTarget'))}</span><select id="historyNetworkSelect"><option value="all">${esc(tr('history.allNetworkTargets'))}</option>${targetOptions}</select></label>`;
|
||||
const select = $('#historyNetworkSelect'); if ([...select.options].some(option => option.value === app.historyNetworkTarget)) select.value = app.historyNetworkTarget;
|
||||
} 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;
|
||||
@@ -129,6 +134,16 @@ async function loadHistory() {
|
||||
if (app.currentView === 'history') updateBrowserUrl(currentHistoryPath(), true);
|
||||
return;
|
||||
}
|
||||
if (app.historyTab === 'network') {
|
||||
const target = app.historyNetworkTarget && app.historyNetworkTarget !== 'all' ? `&target_id=${encodeURIComponent(app.historyNetworkTarget)}` : '';
|
||||
const data = await api(`/api/history/network?hours=${encodeURIComponent(hours)}&limit=20000${target}`);
|
||||
app.historyNetwork = data.readings || [];
|
||||
app.historyNetworkTargets = data.targets || [];
|
||||
renderHistoryNavigation();
|
||||
renderHistoryPage();
|
||||
if (app.currentView === 'history') updateBrowserUrl(currentHistoryPath(), true);
|
||||
return;
|
||||
}
|
||||
const data = await api(`/api/history?scope=overview&hours=${encodeURIComponent(hours)}&limit=20000`);
|
||||
app.historyData = { zones: data.zones || [], devices: data.devices || [], sensors: data.sensors || [] };
|
||||
app.historyCounts = data.counts || {};
|
||||
@@ -323,11 +338,11 @@ function nearestPoint(points, targetTs) {
|
||||
return Math.abs(before.ts - targetTs) <= Math.abs(after.ts - targetTs) ? before : after;
|
||||
}
|
||||
|
||||
function formatChartTooltipValue(item, point, binaryLabels) {
|
||||
function formatChartTooltipValue(item, point, binaryLabels, tooltipUnit = ' °C') {
|
||||
if (!point || !Number.isFinite(point.value)) return '—';
|
||||
if (typeof item.tooltipValue === 'function') return String(item.tooltipValue(point.value, point.row));
|
||||
const formatted = Number(point.value).toLocaleString(locale(), { minimumFractionDigits: 1, maximumFractionDigits: 2 });
|
||||
return binaryLabels ? formatted : `${formatted} °C`;
|
||||
return binaryLabels ? formatted : `${formatted}${tooltipUnit}`;
|
||||
}
|
||||
|
||||
function bindChartTooltip(canvas, series, sortedRows, geometry) {
|
||||
@@ -337,7 +352,7 @@ function bindChartTooltip(canvas, series, sortedRows, geometry) {
|
||||
const selection = wrap?.querySelector('.chart-selection');
|
||||
if (!canvas || !wrap || !tooltip || !line) return;
|
||||
|
||||
const { pad, width, height, firstTs, lastTs, binaryLabels } = geometry;
|
||||
const { pad, width, height, firstTs, lastTs, binaryLabels, tooltipUnit } = geometry;
|
||||
const plotWidth = width - pad.left - pad.right;
|
||||
const timestamps = [...new Set(sortedRows.map(row => new Date(row.timestamp).getTime()).filter(Number.isFinite))].sort((a, b) => a - b);
|
||||
const timestampPoints = timestamps.map(ts => ({ ts, value: ts }));
|
||||
@@ -367,7 +382,7 @@ function bindChartTooltip(canvas, series, sortedRows, geometry) {
|
||||
$$('.chart-hover-line').forEach(node => { if (node !== line) node.hidden = true; });
|
||||
tooltip.innerHTML = `<strong class="chart-tooltip-time">${esc(chartPreciseTime(snapped))}</strong><div class="chart-tooltip-values">${values.map(({ item, point }) => {
|
||||
const sampleTime = Math.abs(point.ts - snapped) > 1000 ? `<small class="chart-tooltip-sample-time">${esc(chartPreciseTime(point.ts))}</small>` : '';
|
||||
return `<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(formatChartTooltipValue(item, point, binaryLabels))}${sampleTime}</span></div>`;
|
||||
return `<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(formatChartTooltipValue(item, point, binaryLabels, tooltipUnit))}${sampleTime}</span></div>`;
|
||||
}).join('')}</div>`;
|
||||
tooltip.hidden = false;
|
||||
line.hidden = false;
|
||||
@@ -454,9 +469,9 @@ function bindChartTooltip(canvas, series, sortedRows, geometry) {
|
||||
};
|
||||
}
|
||||
|
||||
function drawLineChart(canvas, series, rows, { height = 340, minValue = null, maxValue = null, binaryLabels = false } = {}) {
|
||||
function drawLineChart(canvas, series, rows, { height = 340, minValue = null, maxValue = null, binaryLabels = false, axisSuffix = '°', tooltipUnit = ' °C', axisDigits = 1 } = {}) {
|
||||
if (!canvas) return;
|
||||
const options = { height, minValue, maxValue, binaryLabels };
|
||||
const options = { height, minValue, maxValue, binaryLabels, axisSuffix, tooltipUnit, axisDigits };
|
||||
if (canvas.id) chartRuntime.set(canvas.id, { series, rows, options });
|
||||
const visibleSeries = series.filter((item, index) => !isChartSeriesHidden(canvas.id, item, index));
|
||||
if (!rows.length || !visibleSeries.length) return drawEmptyChart(canvas, height);
|
||||
@@ -482,7 +497,7 @@ function drawLineChart(canvas, series, rows, { height = 340, minValue = null, ma
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
const value = min + (max - min) * i / 5, py = y(value);
|
||||
ctx.beginPath(); ctx.moveTo(pad.left, py); ctx.lineTo(width - pad.right, py); ctx.stroke();
|
||||
ctx.textAlign = 'right'; ctx.fillText(binaryLabels ? value.toFixed(0) : `${value.toFixed(1)}°`, pad.left - 8, py + 3);
|
||||
ctx.textAlign = 'right'; ctx.fillText(binaryLabels ? value.toFixed(0) : `${value.toFixed(axisDigits)}${axisSuffix}`, pad.left - 8, py + 3);
|
||||
}
|
||||
const ticks = width < 420 ? 2 : width < 620 ? 3 : 5;
|
||||
for (let i = 0; i <= ticks; i++) {
|
||||
@@ -501,7 +516,7 @@ function drawLineChart(canvas, series, rows, { height = 340, minValue = null, ma
|
||||
});
|
||||
ctx.stroke(); ctx.setLineDash([]);
|
||||
});
|
||||
bindChartTooltip(canvas, visibleSeries, sortedRows, { pad, width, height, firstTs, lastTs, binaryLabels });
|
||||
bindChartTooltip(canvas, visibleSeries, sortedRows, { pad, width, height, firstTs, lastTs, binaryLabels, tooltipUnit });
|
||||
}
|
||||
|
||||
function energyBucketLabel(timestamp, interval) {
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ const app = {
|
||||
currentView: 'dashboard', loading: false, bootstrapReloadPending: false, language: preferredLanguage, theme: preferredTheme, connectionStatus: 'connecting',
|
||||
languages: [], translations: {}, locales: {},
|
||||
historyTab: 'overview', historyData: { zones: [], devices: [], sensors: [] }, historyCounts: {},
|
||||
historyZone: 'all', historyDevice: 'all', historySensor: 'all', historyEnergyDevice: '', historyEnergyTargets: [], historyEnergyInterval: 'daily', historyEnergyCompare: 'none', historyEnergy: [], historyLoading: false, historyReloadPending: false,
|
||||
historyZone: 'all', historyDevice: 'all', historySensor: 'all', historyNetworkTarget: 'all', historyNetwork: [], historyNetworkTargets: [], historyEnergyDevice: '', historyEnergyTargets: [], historyEnergyInterval: 'daily', historyEnergyCompare: 'none', historyEnergy: [], historyLoading: false, historyReloadPending: false,
|
||||
customChartSeries: [], savedCharts: [], chartZooms: {}, chartHiddenSeries: {}, zoneControlSeq: {}, groupControlSeq: {}, zoneControlQueue: {}, groupControlQueue: {}, deviceControlQueue: {}, houseControlQueue: null, climateControlQueue: null, groupCustomDrafts: {}, zoneTemperatureTimers: {}, deviceTemperatureDrafts: {},
|
||||
controlPlan: null, controlPlanRevision: null, controlPlanPushReady: false, controlPlanTimer: null, debugLines: [], debugBacklogLoaded: false, debugFilter: 'all', sensorAliases: {}, flowSharedInputs: [], flowSharedInputValueCache: {}, flowSharedInputValueRequests: {}, flowSharedInputValueTimer: null,
|
||||
simulationScope: 'units', simulationTarget: 'all', standaloneSimulation: false, dashboardTab: 'main', settingsTab: 'app', systemSnapshotAt: Date.now(),
|
||||
|
||||
@@ -29,6 +29,7 @@ function outdoorHistorySeries(devices, rows) {
|
||||
function renderHistorySummary() {
|
||||
const host = $('#historySummary'); if (!host) return;
|
||||
if (app.historyTab === 'energy') { renderEnergyHistorySummary(); return; }
|
||||
if (app.historyTab === 'network') { renderNetworkHistorySummary(); return; }
|
||||
const deviceRows = app.historyData.devices, zoneRows = app.historyData.zones, sensorRows = app.historyData.sensors;
|
||||
const cards = [
|
||||
[tr('history.deviceSamples'), app.historyCounts.devices ?? deviceRows.length, tr('history.greeHistory')],
|
||||
@@ -113,6 +114,67 @@ function renderSensorHistory() {
|
||||
drawLineChart($('#haSensorsChart'), series, rows, { height: 370 }); renderLegend($('#haSensorsChartLegend'), series);
|
||||
}
|
||||
|
||||
function networkTargetRows() {
|
||||
const selected = app.historyNetworkTarget || 'all';
|
||||
return selected === 'all' ? (app.historyNetwork || []) : (app.historyNetwork || []).filter(row => row.target_id === selected);
|
||||
}
|
||||
|
||||
function networkTargetLabel(id) {
|
||||
return (app.historyNetworkTargets || []).find(target => target.id === id)?.name || id;
|
||||
}
|
||||
|
||||
function renderNetworkHistorySummary() {
|
||||
const host = $('#historySummary'); if (!host) return;
|
||||
const rows = networkTargetRows();
|
||||
const ids = [...new Set(rows.map(row => row.target_id))];
|
||||
if (!ids.length) { host.innerHTML = ''; return; }
|
||||
host.innerHTML = ids.map(id => {
|
||||
const latest = rows.filter(row => row.target_id === id).sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))[0];
|
||||
const latency = Number.isFinite(Number(latest?.latency_ms)) ? `${Number(latest.latency_ms).toLocaleString(locale(), { maximumFractionDigits: 1 })} ms` : '—';
|
||||
const jitter = Number.isFinite(Number(latest?.jitter_ms)) ? `${Number(latest.jitter_ms).toLocaleString(locale(), { maximumFractionDigits: 1 })} ms` : '—';
|
||||
const loss = `${Number(latest?.packet_loss_pct || 0).toLocaleString(locale(), { maximumFractionDigits: 1 })}%`;
|
||||
return `<div class="history-stat"><small>${esc(networkTargetLabel(id))}</small><strong>${esc(latency)}</strong><span>${esc(tr('history.networkJitter'))}: ${esc(jitter)} · ${esc(tr('history.networkLoss'))}: ${esc(loss)} · ${esc(latest?.successful_samples ?? 0)}/${esc(latest?.sample_count ?? 0)}</span></div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderNetworkHistory() {
|
||||
const host = $('#historyCharts'); if (!host) return;
|
||||
renderNetworkHistorySummary();
|
||||
const rows = networkTargetRows();
|
||||
if (!rows.length) {
|
||||
host.innerHTML = `<div class="empty"><strong>${esc(tr('history.networkNoData'))}</strong><span>${esc(tr('history.networkNoDataHint'))}</span></div>`;
|
||||
return;
|
||||
}
|
||||
const ids = [...new Set(rows.map(row => row.target_id))];
|
||||
const latencySeries = [];
|
||||
const lossSeries = [];
|
||||
ids.forEach((id, index) => {
|
||||
const label = networkTargetLabel(id);
|
||||
const color = historySeriesColor(index);
|
||||
latencySeries.push({
|
||||
key: `${id}:latency`, label: `${label} · ${tr('history.networkLatency')}`, color,
|
||||
value: row => row.target_id === id ? historyNumber(row.latency_ms) : NaN,
|
||||
tooltipValue: value => `${Number(value).toLocaleString(locale(), { maximumFractionDigits: 2 })} ms`,
|
||||
});
|
||||
latencySeries.push({
|
||||
key: `${id}:jitter`, label: `${label} · ${tr('history.networkJitter')}`, color, dash: [5, 4],
|
||||
value: row => row.target_id === id ? historyNumber(row.jitter_ms) : NaN,
|
||||
tooltipValue: value => `${Number(value).toLocaleString(locale(), { maximumFractionDigits: 2 })} ms`,
|
||||
});
|
||||
lossSeries.push({
|
||||
key: `${id}:loss`, label, color,
|
||||
value: row => row.target_id === id ? historyNumber(row.packet_loss_pct) : NaN,
|
||||
tooltipValue: value => `${Number(value).toLocaleString(locale(), { maximumFractionDigits: 2 })}%`,
|
||||
});
|
||||
});
|
||||
host.innerHTML = historyChartMarkup('networkLatencyChart', tr('history.networkLatencyJitter'), tr('history.networkLatencyJitterHint'))
|
||||
+ historyChartMarkup('networkLossChart', tr('history.networkLoss'), tr('history.networkLossHint'));
|
||||
drawLineChart($('#networkLatencyChart'), latencySeries, rows, { height: 360, minValue: 0, axisSuffix: ' ms', tooltipUnit: ' ms', axisDigits: 0 });
|
||||
renderLegend($('#networkLatencyChartLegend'), latencySeries);
|
||||
drawLineChart($('#networkLossChart'), lossSeries, rows, { height: 300, minValue: 0, maxValue: 100, axisSuffix: '%', tooltipUnit: '%', axisDigits: 0 });
|
||||
renderLegend($('#networkLossChartLegend'), lossSeries);
|
||||
}
|
||||
|
||||
function customSeriesOptions() {
|
||||
const items = [];
|
||||
const groupedOutdoorIds = new Set();
|
||||
@@ -203,6 +265,7 @@ function renderHistoryPage() {
|
||||
else if (app.historyTab === 'zones') renderZoneHistory();
|
||||
else if (app.historyTab === 'devices') renderDeviceHistory();
|
||||
else if (app.historyTab === 'energy') renderEnergyHistory();
|
||||
else if (app.historyTab === 'network') renderNetworkHistory();
|
||||
else if (app.historyTab === 'sensors') renderSensorHistory();
|
||||
else renderCustomHistory();
|
||||
}
|
||||
|
||||
@@ -127,14 +127,15 @@ function renderPingDialog() {
|
||||
const toggle = $('#pingToggleButton');
|
||||
const grid = $('#pingLiveGrid');
|
||||
if (!dialog || !select || !all || !toggle || !grid) return;
|
||||
if (!app.pingMonitor.targetId || !app.devices.some(device => device.id === app.pingMonitor.targetId)) app.pingMonitor.targetId = app.devices[0]?.id || '';
|
||||
select.innerHTML = app.devices.map(device => `<option value="${esc(device.id)}" ${device.id === app.pingMonitor.targetId ? 'selected' : ''}>${esc(device.name)}</option>`).join('');
|
||||
const localDevices = app.devices.filter(device => device.connection_type !== 'gree_cloud');
|
||||
if (!app.pingMonitor.targetId || !localDevices.some(device => device.id === app.pingMonitor.targetId)) app.pingMonitor.targetId = localDevices[0]?.id || '';
|
||||
select.innerHTML = localDevices.map(device => `<option value="${esc(device.id)}" ${device.id === app.pingMonitor.targetId ? 'selected' : ''}>${esc(device.name)}</option>`).join('');
|
||||
select.disabled = app.pingMonitor.all;
|
||||
all.checked = app.pingMonitor.all;
|
||||
toggle.textContent = tr(app.pingMonitor.running ? 'devices.pingStop' : 'devices.pingStart');
|
||||
toggle.classList.toggle('primary', app.pingMonitor.running);
|
||||
toggle.classList.toggle('secondary', !app.pingMonitor.running);
|
||||
const devices = app.pingMonitor.all ? app.devices : app.devices.filter(device => device.id === app.pingMonitor.targetId);
|
||||
const devices = app.pingMonitor.all ? localDevices : localDevices.filter(device => device.id === app.pingMonitor.targetId);
|
||||
grid.innerHTML = devices.length ? devices.map(device => {
|
||||
const samples = pingSamples(device.id);
|
||||
const stats = pingStats(samples);
|
||||
@@ -168,7 +169,8 @@ function schedulePingCycle(delay = null) {
|
||||
|
||||
async function runPingCycle() {
|
||||
if (!app.pingMonitor.running || !$('#pingDialog')?.open || app.pingMonitor.inFlight) return;
|
||||
const targets = app.pingMonitor.all ? [...app.devices] : app.devices.filter(device => device.id === app.pingMonitor.targetId);
|
||||
const localDevices = app.devices.filter(device => device.connection_type !== 'gree_cloud');
|
||||
const targets = app.pingMonitor.all ? [...localDevices] : localDevices.filter(device => device.id === app.pingMonitor.targetId);
|
||||
if (!targets.length) { renderPingDialog(); schedulePingCycle(); return; }
|
||||
app.pingMonitor.inFlight = true;
|
||||
await Promise.allSettled(targets.map(async (device, index) => {
|
||||
@@ -190,7 +192,7 @@ async function runPingCycle() {
|
||||
}
|
||||
|
||||
function startPingMonitor() {
|
||||
if (!app.devices.length) return;
|
||||
if (!app.devices.some(device => device.connection_type !== 'gree_cloud')) return;
|
||||
app.pingMonitor.running = true;
|
||||
renderPingDialog();
|
||||
clearTimeout(app.pingMonitor.timer);
|
||||
@@ -206,7 +208,8 @@ function stopPingMonitor() {
|
||||
}
|
||||
|
||||
function openDevicePing(id) {
|
||||
app.pingMonitor.targetId = id || app.devices[0]?.id || '';
|
||||
const localDevices = app.devices.filter(device => device.connection_type !== 'gree_cloud');
|
||||
app.pingMonitor.targetId = (id && localDevices.some(device => device.id === id)) ? id : (localDevices[0]?.id || '');
|
||||
app.pingMonitor.all = false;
|
||||
openDialog('pingDialog');
|
||||
renderPingDialog();
|
||||
|
||||
+3
-1
@@ -13,7 +13,7 @@ const VIEW_ROUTES = Object.freeze({
|
||||
logs: '/events',
|
||||
});
|
||||
const ROUTE_VIEWS = Object.freeze(Object.fromEntries(Object.entries(VIEW_ROUTES).map(([view, path]) => [path, view])));
|
||||
const HISTORY_TABS = Object.freeze(['overview', 'zones', 'devices', 'energy', 'sensors', 'custom']);
|
||||
const HISTORY_TABS = Object.freeze(['overview', 'zones', 'devices', 'energy', 'network', 'sensors', 'custom']);
|
||||
let routerInitialized = false;
|
||||
|
||||
function currentHistoryPath() {
|
||||
@@ -26,6 +26,7 @@ function currentHistoryPath() {
|
||||
if (tab === 'energy' && app.historyEnergyTargets?.length) params.set('targets', app.historyEnergyTargets.join(','));
|
||||
if (tab === 'energy' && app.historyEnergyInterval !== 'daily') params.set('interval', app.historyEnergyInterval);
|
||||
if (tab === 'energy' && app.historyEnergyCompare !== 'none') params.set('compare', app.historyEnergyCompare);
|
||||
if (tab === 'network' && app.historyNetworkTarget !== 'all') params.set('target', app.historyNetworkTarget);
|
||||
if (tab === 'sensors' && app.historySensor !== 'all') params.set('sensor', app.historySensor);
|
||||
if (tab === 'custom' && app.customChartSeries.length) params.set('chart', encodeChartSpec(app.customChartSeries));
|
||||
const query = params.toString();
|
||||
@@ -81,6 +82,7 @@ function applyRouteFromLocation() {
|
||||
if (['hourly', 'daily', 'weekly', 'monthly'].includes(params.get('interval'))) app.historyEnergyInterval = params.get('interval');
|
||||
if (['none', 'previous_day', 'previous_period', 'previous_year'].includes(params.get('compare'))) app.historyEnergyCompare = params.get('compare');
|
||||
}
|
||||
app.historyNetworkTarget = params.get('target') || 'all';
|
||||
app.historySensor = params.get('sensor') || 'all';
|
||||
const hours = params.get('hours');
|
||||
if (hours && ['6', '24', '168', '720', '2160', '8760'].includes(hours) && $('#historyHours')) $('#historyHours').value = hours;
|
||||
|
||||
@@ -173,6 +173,9 @@ function renderSettings() {
|
||||
form.zone_interval_seconds.value = app.settings.zone_interval_seconds || 5;
|
||||
form.discovery_broadcast.value = app.settings.discovery_broadcast || '255.255.255.255:7000';
|
||||
form.discovery_timeout_ms.value = app.settings.discovery_timeout_ms || 3000;
|
||||
form.ping_metrics_enabled.checked = app.settings.ping_metrics_enabled !== false;
|
||||
form.ping_interval_seconds.value = Number(app.settings.ping_interval_seconds || 60);
|
||||
form.ping_sample_count.value = Number(app.settings.ping_sample_count || 3);
|
||||
form.simulator_enabled.checked = !!app.settings.simulator_enabled;
|
||||
form.history_retention_days.value = app.settings.history_retention_days || 30;
|
||||
form.event_log_retention_days.value = app.settings.event_log_retention_days || 30;
|
||||
@@ -185,6 +188,9 @@ function renderSettings() {
|
||||
form.gree_cloud_password.value = '';
|
||||
form.gree_cloud_password.placeholder = cloud.password_configured ? tr('settings.secretSaved') : tr('settings.secretKeep');
|
||||
form.gree_cloud_polling_interval_seconds.value = Number(cloud.polling_interval_seconds || 60);
|
||||
form.gree_cloud_connectivity_metrics_enabled.checked = !!cloud.connectivity_metrics_enabled;
|
||||
form.gree_cloud_connectivity_metrics_interval_seconds.value = Number(cloud.connectivity_metrics_interval_seconds || 300);
|
||||
form.gree_cloud_connectivity_metrics_sample_count.value = Number(cloud.connectivity_metrics_sample_count || 3);
|
||||
const cloudInstance = $('#greeCloudInstanceId');
|
||||
if (cloudInstance) cloudInstance.textContent = cloud.installation_id || '—';
|
||||
const cloudLastContact = $('#greeCloudLastContact');
|
||||
|
||||
@@ -40,6 +40,9 @@ function greeSettingsBodyFromForm(form) {
|
||||
zone_interval_seconds: Number(raw.zone_interval_seconds),
|
||||
discovery_timeout_ms: Number(raw.discovery_timeout_ms),
|
||||
discovery_broadcast: raw.discovery_broadcast,
|
||||
ping_metrics_enabled: form.ping_metrics_enabled.checked,
|
||||
ping_interval_seconds: Number(raw.ping_interval_seconds || 60),
|
||||
ping_sample_count: Number(raw.ping_sample_count || 3),
|
||||
suppress_device_beep: form.suppress_device_beep.checked,
|
||||
compressor_protection_enabled: form.compressor_protection_enabled.checked,
|
||||
compressor_protection_seconds: Math.max(30, Math.min(1800, Math.round(Number(raw.compressor_protection_minutes || 3) * 60))),
|
||||
@@ -53,6 +56,9 @@ function greeCloudSettingsBodyFromForm(form) {
|
||||
region: raw.gree_cloud_region || 'Europe',
|
||||
username: raw.gree_cloud_username || '',
|
||||
polling_interval_seconds: Number(raw.gree_cloud_polling_interval_seconds || 60),
|
||||
connectivity_metrics_enabled: form.gree_cloud_connectivity_metrics_enabled.checked,
|
||||
connectivity_metrics_interval_seconds: Number(raw.gree_cloud_connectivity_metrics_interval_seconds || 300),
|
||||
connectivity_metrics_sample_count: Number(raw.gree_cloud_connectivity_metrics_sample_count || 3),
|
||||
};
|
||||
if (raw.gree_cloud_password) body.password = raw.gree_cloud_password;
|
||||
return body;
|
||||
@@ -331,6 +337,7 @@ document.addEventListener('change', event => {
|
||||
}
|
||||
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 === 'historyNetworkSelect') { app.historyNetworkTarget = target.value; updateBrowserUrl(currentHistoryPath()); loadHistory(); }
|
||||
else if (target.id === 'historySensorSelect') { app.historySensor = target.value; updateBrowserUrl(currentHistoryPath()); renderHistoryPage(); }
|
||||
else if (target.name === 'influx_version') updateInfluxFields();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user