v0.11.6
This commit is contained in:
@@ -28,6 +28,7 @@ tokio = { version = "1", features = ["full"] }
|
||||
tower-http = { version = "0.6", features = ["compression-gzip", "cors", "trace"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||
utoipa-swagger-ui = { version = "=8.0.3", default-features = false, features = ["axum", "debug-embed", "vendored"] }
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example
|
||||
a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore
|
||||
d3c8a734104ca6d68b1a07c41ef6832ca475e3fc257ccbbaa0afc3a7ad85bd17 ./Cargo.lock
|
||||
0c1a0a807005fc51ac02dfa22bb1a1fa4ae372c200a456faa49b18389bc43b15 ./Cargo.toml
|
||||
31eed008472815e59f3a6b7eaa1f9186c407f49a15d50d77d0aaa136d1c3ca82 ./Cargo.toml
|
||||
19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE
|
||||
a9d1837cefe92369e324a2f656cc4ce399884736d13c064eae44dae7e38faa7b ./README.md
|
||||
5467c49d65a990966d3778fb928f6c67cbca7be94d2d54022d479821f0457816 ./README.md
|
||||
41dfdc6d099b54f87d4dd51f696122b3c88d3bf3ddf420bea8f4bc621ba920b0 ./build.rs
|
||||
797eb20f62d4e9a7351dc76989c2f60e1cf52a48a9bac6fa9d3a51f9c5b837e3 ./docs/API.md
|
||||
34a90343baf4d189c58887bc0cf6f65e824614676afc94b5ea8363e9d262ccc6 ./docs/API.md
|
||||
2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md
|
||||
f10b54081d00e6d20aac14d351cad767b874544196628d71631b14125f1e88ba ./docs/openapi.json
|
||||
a0893b2a56eb1523f1a72871842e9be2139a5fafba1f51ae942fc407a6e4ca34 ./home-assistant/README.md
|
||||
f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py
|
||||
6910589f27960a28d4de9735884a7e5376e455cd885f19ce2b55947fcd135114 ./home-assistant/custom_components/gree_controller/api.py
|
||||
@@ -71,7 +72,7 @@ ae21459a261712bcb8d57594528b1648432e8d03a8a38502234829c0dbfef774 ./presets/week
|
||||
fdcd9a5055d08037278b842e7ab69265345c5811f0a06867136c511d140bb191 ./presets/window_available_guard.json
|
||||
804f22123cd3e8db0fac791826c8dd9f758fb866c8e3b5655fb6d25d259dccf1 ./presets/workday_comfort.json
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh
|
||||
ec5301fbad7adf463ee1205539481746388f757173301efb3c1ded6176487944 ./scripts/FILE_MANIFEST.sha256
|
||||
a35385734b1855dbea6d895075d54b17dce1621797cd65581a9404acff2ce645 ./scripts/FILE_MANIFEST.sha256
|
||||
bb89bac237e750e9b1bf73761d7df97a6b81853091615878c03f13d7b6399aa7 ./scripts/README.md
|
||||
5bc736c7bc76ca80aaa406bb171d2aa91baf4c3aa8695dce0e09b888b6ab3146 ./scripts/common.sh
|
||||
6403786610ee6d2f628193c25aee0dd058d62e904aa1a31d5f62fdaae0e94b4f ./scripts/configure-gree-network.sh
|
||||
@@ -82,10 +83,10 @@ bb7cd2c5b27c9dceec1d1d2846fbad9600df9c08e0ad07d13fcde97af533091c ./scripts/inst
|
||||
e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./scripts/network-debug.sh
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./scripts/regenerate-sha.sh
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./scripts/service.sh
|
||||
e16483e9afcea50ca92fa2ccf4ec5e5a2cc46b68b157865d20b09829b5fe9ed3 ./scripts/smoke.sh
|
||||
c714e3cf2fba84ded718d102bd620cf879c1a8f3a5ae8f435be7863d8a0cd8e0 ./scripts/smoke.sh
|
||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./scripts/update.sh
|
||||
4877f9e8217b6a77fb416722c3778373edac874ed3a583bb016ea76d4ffee7d4 ./scripts/verify_flow_logic.py
|
||||
204dc035f6cbc657afb5ff17dfd19d5f567e6fae4318347e65158cacf2d60139 ./src/api.rs
|
||||
3256e9b9a34e4ef9b69c27d630dad4a9b17a29afc894f56c65206e7aa78eab21 ./src/api.rs
|
||||
825370f409124a719043200742c01c3441bc65f483108106140d13fe7bbcba91 ./src/api/assets.rs
|
||||
6c34b1294d76b0eba4c56bfd0bf69bdd502de0e40838104f6ddf2db4e922683d ./src/api/auth.rs
|
||||
63cb241c4536abed402f2a0ad37e7f299343df264eab6a33bb5354c15421e045 ./src/api/automations.rs
|
||||
@@ -98,6 +99,7 @@ a13d4e217fe3ddaa73873ba6e0d1bc93750cf61764d867ff7372274ed21d9599 ./src/api/hist
|
||||
b3ed82de9d885a6325647bff3d0ead6ac46fc74d0250d5e14d75319555ad88d7 ./src/api/house.rs
|
||||
88e04a59bfedd5e6c54e6a2938b1031964be3a1505db2ca67d6290aa042cbe3b ./src/api/integrations.rs
|
||||
bb2a746ecdcc2da5fe54e18b455c7bd19453486dd5c008e71951a2c81d0e7d64 ./src/api/middleware.rs
|
||||
45e34a447ff384d99a3c42c489ca9fa75849c74d4d246caa91c63d927de17396 ./src/api/openapi.rs
|
||||
50cfd47e44e22cc802f22f97267c157fa5a6d19b51be921a6ca6b1ea7c9799d1 ./src/api/public_settings.rs
|
||||
a1d1a4ac071493b052e6bf20b03be67cf75726de95649e9a73aeba8d22395a15 ./src/api/schedules.rs
|
||||
430f446371489a2b2f1532d589c71f60c98c1a92765ccb5e486b08e2da8f31c3 ./src/api/settings.rs
|
||||
|
||||
@@ -375,6 +375,15 @@ Start here:
|
||||
|
||||
**[docs/API.md — complete API reference](docs/API.md)**
|
||||
|
||||
Interactive API documentation is built into the controller:
|
||||
|
||||
- Swagger UI: `http://127.0.0.1:8787/api-docs`
|
||||
- OpenAPI 3.1 JSON: `http://127.0.0.1:8787/api-docs/openapi.json`
|
||||
|
||||
Swagger UI includes endpoint descriptions, authentication schemes, request/response models,
|
||||
examples and documented error responses. If `GREE_CONTROLLER_BASE_PATH` is configured, prefix
|
||||
both paths with that base path.
|
||||
|
||||
Public health check:
|
||||
|
||||
```bash
|
||||
|
||||
+14
@@ -20,6 +20,18 @@ Content-Type: application/json
|
||||
|
||||
If `GREE_CONTROLLER_BASE_PATH=/gree` is configured, every HTTP and WebSocket path below is prefixed with `/gree`.
|
||||
|
||||
Interactive and machine-readable API documentation is served by the application itself:
|
||||
|
||||
```text
|
||||
GET /api-docs Swagger UI
|
||||
GET /api-docs/openapi.json OpenAPI 3.1 document
|
||||
```
|
||||
|
||||
The OpenAPI document contains endpoint descriptions, authentication schemes, parameters,
|
||||
request/response models, examples and common error responses. Its `servers` entry follows the
|
||||
configured `GREE_CONTROLLER_BASE_PATH`, so requests sent from Swagger UI target the current
|
||||
controller instance correctly.
|
||||
|
||||
## Authentication
|
||||
|
||||
There are three access levels.
|
||||
@@ -30,6 +42,8 @@ No token is required for:
|
||||
|
||||
```text
|
||||
GET /api/health
|
||||
GET /api-docs
|
||||
GET /api-docs/openapi.json
|
||||
GET /
|
||||
GET /index.html
|
||||
GET /app.js
|
||||
|
||||
+6905
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,6 @@ bb7cd2c5b27c9dceec1d1d2846fbad9600df9c08e0ad07d13fcde97af533091c ./install.sh
|
||||
e00d211e3885e30d7fed1e43b44e6fdad40a67019060156c0641816a93e3365f ./network-debug.sh
|
||||
01952aa92b217f8eae2493b88870e2dec595100cd15c4d561ff11ae2b936c46f ./regenerate-sha.sh
|
||||
81345b6a0b51736bdbc98fd23199b62e4c721b4e7437e02dab7ea79b97dff29a ./service.sh
|
||||
e16483e9afcea50ca92fa2ccf4ec5e5a2cc46b68b157865d20b09829b5fe9ed3 ./smoke.sh
|
||||
c714e3cf2fba84ded718d102bd620cf879c1a8f3a5ae8f435be7863d8a0cd8e0 ./smoke.sh
|
||||
b50782b3742dfbf8a319c60571c968e93fdf8547db747c759edcffae68cb98bf ./update.sh
|
||||
4877f9e8217b6a77fb416722c3778373edac874ed3a583bb016ea76d4ffee7d4 ./verify_flow_logic.py
|
||||
|
||||
@@ -33,6 +33,19 @@ for _ in $(seq 1 80); do
|
||||
done
|
||||
grep -q '"status":"ok"' "$TMP/health.json"
|
||||
|
||||
curl -fsS "http://127.0.0.1:$PORT/api-docs/openapi.json" >"$TMP/openapi.json"
|
||||
python3 - "$TMP/openapi.json" <<'PYOPENAPI'
|
||||
import json, sys
|
||||
|
||||
doc = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
assert doc["openapi"] == "3.1.0"
|
||||
assert "/api/zones/{id}/control" in doc["paths"]
|
||||
assert "BearerToken" in doc["components"]["securitySchemes"]
|
||||
assert doc["servers"][0]["url"] == "/"
|
||||
PYOPENAPI
|
||||
curl -fsSL "http://127.0.0.1:$PORT/api-docs" >"$TMP/swagger.html"
|
||||
grep -qi 'swagger-ui' "$TMP/swagger.html"
|
||||
|
||||
curl -fsS "http://127.0.0.1:$PORT/api/bootstrap" >"$TMP/bootstrap.json"
|
||||
grep -q 'sim-salon' "$TMP/bootstrap.json"
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ use crate::{
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
mod openapi;
|
||||
|
||||
const INDEX_HTML: &str = include_str!("../web/index.html");
|
||||
const NOT_FOUND_HTML: &str = include_str!("../web/404.html");
|
||||
const APP_JS: &str = include_str!(concat!(env!("OUT_DIR"), "/app.bundle.js"));
|
||||
@@ -139,6 +141,7 @@ pub fn router(state: AppState) -> Router {
|
||||
.route("/lang/:file", get(language_file))
|
||||
.route("/presets/index.json", get(preset_index))
|
||||
.route("/presets/:file", get(preset_file))
|
||||
.merge(openapi::swagger_ui(&state.config.base_path))
|
||||
.merge(protected)
|
||||
.merge(home_assistant_api);
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
use serde_json::{json, Value};
|
||||
use utoipa_swagger_ui::{Config as SwaggerConfig, SwaggerUi};
|
||||
|
||||
const OPENAPI_JSON: &str = include_str!("../../docs/openapi.json");
|
||||
|
||||
pub(super) fn swagger_ui(base_path: &str) -> SwaggerUi {
|
||||
let docs_url = format!("{}/api-docs/openapi.json", normalized_base_path(base_path));
|
||||
SwaggerUi::new("/api-docs")
|
||||
// Keep the route itself relative to the application router so Axum's
|
||||
// outer base-path nesting prefixes it exactly once. Swagger UI may use
|
||||
// the externally visible, base-path-aware URL when fetching the spec.
|
||||
.external_url_unchecked("/api-docs/openapi.json", document(base_path))
|
||||
.config(
|
||||
SwaggerConfig::new([docs_url])
|
||||
.filter(true)
|
||||
.try_it_out_enabled(true)
|
||||
.display_request_duration(true)
|
||||
.persist_authorization(true),
|
||||
)
|
||||
}
|
||||
|
||||
fn document(base_path: &str) -> Value {
|
||||
let mut document: Value = serde_json::from_str(OPENAPI_JSON)
|
||||
.expect("embedded OpenAPI document must contain valid JSON");
|
||||
document["info"]["version"] = Value::String(env!("CARGO_PKG_VERSION").to_string());
|
||||
document["servers"] = json!([{
|
||||
"url": server_base_path(base_path),
|
||||
"description": "This GREE Controller instance"
|
||||
}]);
|
||||
document
|
||||
}
|
||||
|
||||
fn normalized_base_path(base_path: &str) -> String {
|
||||
let base = base_path.trim().trim_end_matches('/');
|
||||
if base.is_empty() || base == "/" {
|
||||
String::new()
|
||||
} else if base.starts_with('/') {
|
||||
base.to_string()
|
||||
} else {
|
||||
format!("/{base}")
|
||||
}
|
||||
}
|
||||
|
||||
fn server_base_path(base_path: &str) -> String {
|
||||
let base = normalized_base_path(base_path);
|
||||
if base.is_empty() {
|
||||
"/".into()
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn openapi_document_is_valid_json_and_version_is_runtime_version() {
|
||||
let document = document("");
|
||||
assert_eq!(document["openapi"], "3.1.0");
|
||||
assert_eq!(document["info"]["version"], env!("CARGO_PKG_VERSION"));
|
||||
assert_eq!(document["servers"][0]["url"], "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openapi_respects_configured_base_path() {
|
||||
let document = document("/gree/");
|
||||
assert_eq!(document["servers"][0]["url"], "/gree");
|
||||
assert_eq!(normalized_base_path("/gree/"), "/gree");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_documented_operation_has_summary_description_and_responses() {
|
||||
let document = document("");
|
||||
let paths = document["paths"].as_object().expect("OpenAPI paths object");
|
||||
for (path, item) in paths {
|
||||
let methods = item.as_object().expect("OpenAPI path item");
|
||||
for (method, operation) in methods {
|
||||
if !matches!(method.as_str(), "get" | "post" | "put" | "patch" | "delete") {
|
||||
continue;
|
||||
}
|
||||
assert!(
|
||||
operation.get("summary").and_then(Value::as_str).is_some(),
|
||||
"{method} {path} is missing summary"
|
||||
);
|
||||
assert!(
|
||||
operation
|
||||
.get("description")
|
||||
.and_then(Value::as_str)
|
||||
.is_some(),
|
||||
"{method} {path} is missing description"
|
||||
);
|
||||
assert!(
|
||||
operation
|
||||
.get("responses")
|
||||
.and_then(Value::as_object)
|
||||
.is_some(),
|
||||
"{method} {path} is missing responses"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user