From c3f6e773f830dc629a0306fc606c57abdb414129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Tue, 15 Sep 2026 10:02:49 +0200 Subject: [PATCH] v0.14.2-connectivity --- FILE_MANIFEST.sha256 | 80 +++--- README.md | 2 +- SOURCE_MANIFEST.sha256 | 78 +++--- docs/API.md | 7 + docs/openapi.json | 239 +++++++++++++++++- .../repository/gree-controller/CHANGELOG.md | 5 + lang/en.json | 21 +- lang/pl.json | 21 +- scripts/api_dev_test.py | 13 +- scripts/smoke.sh | 12 + src/api.rs | 4 +- src/api/configuration.rs | 7 + src/api/history.rs | 103 ++++++++ src/api/settings.rs | 22 +- src/config.rs | 12 + src/db.rs | 3 +- src/db/core_devices.rs | 1 + src/db/network_history.rs | 143 +++++++++++ src/engine.rs | 3 +- src/engine/connectivity.rs | 184 ++++++++++++++ src/engine/history.rs | 13 + src/engine/runtime.rs | 33 +++ src/influxdb.rs | 3 +- src/influxdb/query.rs | 78 ++++++ src/influxdb/write.rs | 46 ++++ src/models/defaults.rs | 10 + src/models/history.rs | 21 ++ src/models/runtime.rs | 17 ++ src/models/settings_api.rs | 15 ++ src/protocol/gree_cloud_mqtt.rs | 31 ++- src/provider.rs | 6 + src/queries/schema.rs | 20 ++ web/index.html | 19 ++ web/js/bootstrap.js | 3 + web/js/charts.js | 31 ++- web/js/core.js | 2 +- web/js/history.js | 63 +++++ web/js/navigation.js | 15 +- web/js/router.js | 4 +- web/js/settings-ui.js | 6 + web/js/settings.js | 7 + 41 files changed, 1295 insertions(+), 108 deletions(-) create mode 100644 src/db/network_history.rs create mode 100644 src/engine/connectivity.rs diff --git a/FILE_MANIFEST.sha256 b/FILE_MANIFEST.sha256 index ffb6c85..5c2cef8 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -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 diff --git a/README.md b/README.md index 4752f63..0b06501 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/SOURCE_MANIFEST.sha256 b/SOURCE_MANIFEST.sha256 index 5dedc45..e34a6f7 100644 --- a/SOURCE_MANIFEST.sha256 +++ b/SOURCE_MANIFEST.sha256 @@ -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 diff --git a/docs/API.md b/docs/API.md index aa890f6..4991c53 100644 --- a/docs/API.md +++ b/docs/API.md @@ -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. diff --git a/docs/openapi.json b/docs/openapi.json index 24a9254..a76cd06 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -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": { diff --git a/ha-addon/repository/gree-controller/CHANGELOG.md b/ha-addon/repository/gree-controller/CHANGELOG.md index 52396bd..681f880 100644 --- a/ha-addon/repository/gree-controller/CHANGELOG.md +++ b/ha-addon/repository/gree-controller/CHANGELOG.md @@ -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. diff --git a/lang/en.json b/lang/en.json index 0fb438f..0a0fccc 100644 --- a/lang/en.json +++ b/lang/en.json @@ -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." } } diff --git a/lang/pl.json b/lang/pl.json index 92ff316..ad8a5b8 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -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." } } diff --git a/scripts/api_dev_test.py b/scripts/api_dev_test.py index 6fcbd84..83b482f 100644 --- a/scripts/api_dev_test.py +++ b/scripts/api_dev_test.py @@ -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) diff --git a/scripts/smoke.sh b/scripts/smoke.sh index a5f18c0..e7af04f 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -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" diff --git a/src/api.rs b/src/api.rs index ba0c482..38d0733 100644 --- a/src/api.rs +++ b/src/api.rs @@ -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( diff --git a/src/api/configuration.rs b/src/api/configuration.rs index 6eed3ac..90494af 100644 --- a/src/api/configuration.rs +++ b/src/api/configuration.rs @@ -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(); } diff --git a/src/api/history.rs b/src/api/history.rs index 582e67e..066a270 100644 --- a/src/api/history.rs +++ b/src/api/history.rs @@ -775,3 +775,106 @@ async fn energy_history( "latest": latest, }))) } + +#[derive(Debug, Deserialize)] +struct NetworkHistoryQuery { + target_id: Option, + hours: Option, + limit: Option, +} + +async fn combined_network_history( + state: &AppState, + target_id: Option<&str>, + since: chrono::DateTime, + bucket_seconds: i64, + limit: u32, +) -> Result<(Vec, String, Option), 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, + Query(query): Query, +) -> Result, 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::::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, + }))) +} diff --git a/src/api/settings.rs b/src/api/settings.rs index bb947ac..4587498 100644 --- a/src/api/settings.rs +++ b/src/api/settings.rs @@ -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 Result { + 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, + bucket_seconds: i64, + limit: u32, + ) -> Result> { + 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, limit: u32) -> Result> { + 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>>().map_err(Into::into) + } + + pub fn delete_network_batch(&self, rows: &[NetworkReading]) -> Result { + 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 { + 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 { + 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 { + 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)?, + }) + } +} diff --git a/src/engine.rs b/src/engine.rs index 7d853e4..f2cdfc4 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -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"); diff --git a/src/engine/connectivity.rs b/src/engine/connectivity.rs new file mode 100644 index 0000000..7634b9b --- /dev/null +++ b/src/engine/connectivity.rs @@ -0,0 +1,184 @@ +fn build_network_reading( + target_id: String, + target_kind: &str, + source: &str, + samples: Vec>, +) -> NetworkReading { + let sample_count = samples.len() as u32; + let successful: Vec = 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::() / 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::>(); + Some(diffs.iter().sum::() / 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::>(); + 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 { + 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; + } + } +} diff --git a/src/engine/history.rs b/src/engine/history.rs index 33fe72b..3e8aaf5 100644 --- a/src/engine/history.rs +++ b/src/engine/history.rs @@ -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) -> Option { if values.is_empty() { return None; diff --git a/src/engine/runtime.rs b/src/engine/runtime.rs index 5fa4888..a8bdff9 100644 --- a/src/engine/runtime.rs +++ b/src/engine/runtime.rs @@ -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, + start: DateTime, + stop: DateTime, + bucket_seconds: i64, + limit: u32, +) -> Result> { + 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) +} diff --git a/src/influxdb/write.rs b/src/influxdb/write.rs index b577605..366a41b 100644 --- a/src/influxdb/write.rs +++ b/src/influxdb/write.rs @@ -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, diff --git a/src/models/defaults.rs b/src/models/defaults.rs index 73c6538..76cb426 100644 --- a/src/models/defaults.rs +++ b/src/models/defaults.rs @@ -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() } diff --git a/src/models/history.rs b/src/models/history.rs index 793c063..0f58675 100644 --- a/src/models/history.rs +++ b/src/models/history.rs @@ -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, + /// Mean successful round-trip latency for this probe batch. + pub latency_ms: Option, + /// Mean absolute difference between consecutive successful RTT samples. + pub jitter_ms: Option, + /// 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, diff --git a/src/models/runtime.rs b/src/models/runtime.rs index d86a382..1c2815f 100644 --- a/src/models/runtime.rs +++ b/src/models/runtime.rs @@ -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, diff --git a/src/models/settings_api.rs b/src/models/settings_api.rs index 69dae59..3b3d8e9 100644 --- a/src/models/settings_api.rs +++ b/src/models/settings_api.rs @@ -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, 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>, diff --git a/src/protocol/gree_cloud_mqtt.rs b/src/protocol/gree_cloud_mqtt.rs index 85bd92a..e5bb585 100644 --- a/src/protocol/gree_cloud_mqtt.rs +++ b/src/protocol/gree_cloud_mqtt.rs @@ -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, connected: Arc, + events: broadcast::Sender, } 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 { + 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. diff --git a/src/provider.rs b/src/provider.rs index c0ec481..387fa2c 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -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 { + 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"); diff --git a/src/queries/schema.rs b/src/queries/schema.rs index 4627cc9..581ee6b 100644 --- a/src/queries/schema.rs +++ b/src/queries/schema.rs @@ -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')); "#; diff --git a/web/index.html b/web/index.html index 1407291..9f39259 100644 --- a/web/index.html +++ b/web/index.html @@ -327,6 +327,7 @@ + @@ -711,6 +712,20 @@ name="discovery_timeout_ms" min="300" max="30000" required> +
+
+
+

Connectivity metrics

+

Periodic GREE UDP round-trip measurements for Local/LAN units. Results are stored in History.

+
+
+
+ + + +

Only Local/LAN installations are probed. Cloud devices are not pinged directly.

+
+
@@ -751,6 +766,10 @@ + + + +

Optional. Disabled by default. Measures REST login response and MQTT PINGRESP round-trip.