v0.14.15
This commit is contained in:
+8
-2
@@ -23,7 +23,7 @@ use axum::{
|
||||
body::Body,
|
||||
extract::{
|
||||
ws::{Message, WebSocket},
|
||||
Path, Query, Request, State, WebSocketUpgrade,
|
||||
ConnectInfo, Path, Query, Request, State, WebSocketUpgrade,
|
||||
},
|
||||
http::{header, HeaderMap, HeaderValue, StatusCode},
|
||||
middleware::{self, Next},
|
||||
@@ -39,7 +39,7 @@ use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
net::IpAddr,
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::atomic::Ordering,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -49,6 +49,8 @@ use uuid::Uuid;
|
||||
mod openapi;
|
||||
|
||||
const INDEX_HTML: &str = include_str!("../web/index.html");
|
||||
const CUSTOM_CHART_HTML: &str = include_str!("../web/custom-chart.html");
|
||||
const CUSTOM_CHART_JS: &str = include_str!("../web/custom-chart.js");
|
||||
const NOT_FOUND_HTML: &str = include_str!("../web/404.html");
|
||||
const APP_JS: &str = include_str!(concat!(env!("OUT_DIR"), "/app.bundle.js"));
|
||||
const THEME_INIT_JS: &str = include_str!("../web/theme-init.js");
|
||||
@@ -156,6 +158,7 @@ pub fn router(state: AppState) -> Router {
|
||||
)
|
||||
.route("/api/readings", get(readings))
|
||||
.route("/api/history", get(history))
|
||||
.route("/api/charts/custom/share", post(create_public_custom_chart))
|
||||
.route("/api/history/energy", get(energy_history))
|
||||
.route("/api/history/network", get(network_history))
|
||||
.route("/api/control-plan", get(control_plan))
|
||||
@@ -300,6 +303,9 @@ pub fn router(state: AppState) -> Router {
|
||||
|
||||
let mut app = Router::new()
|
||||
.route("/api/health", get(health))
|
||||
.route("/api/public/charts/custom/:token", get(public_custom_chart))
|
||||
.route("/charts/custom/:token", get(custom_chart_page))
|
||||
.route("/custom-chart.js", get(custom_chart_js))
|
||||
.route("/ws", get(websocket))
|
||||
.route("/", get(index))
|
||||
.route("/index.html", get(index))
|
||||
|
||||
@@ -29,6 +29,34 @@ async fn not_found(State(state): State<AppState>, headers: HeaderMap) -> Respons
|
||||
response
|
||||
}
|
||||
|
||||
async fn custom_chart_page(State(state): State<AppState>, headers: HeaderMap) -> Response {
|
||||
let base = request_base_path(&state, &headers);
|
||||
let body = CUSTOM_CHART_HTML
|
||||
.replace("__GREE_BASE_PATH__", &base)
|
||||
.replace(
|
||||
"__GREE_THEME_INIT_ASSET__",
|
||||
&format!("{base}{THEME_INIT_ASSET_PATH}"),
|
||||
)
|
||||
.replace(
|
||||
"__GREE_STYLES_ASSET__",
|
||||
&format!("{base}{STYLES_CSS_ASSET_PATH}"),
|
||||
)
|
||||
.replace(
|
||||
"__GREE_CUSTOM_CHART_ASSET__",
|
||||
&format!("{base}/custom-chart.js"),
|
||||
);
|
||||
let mut response = Response::new(Body::from(body));
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("text/html; charset=utf-8"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("public, no-cache"),
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
async fn index(State(state): State<AppState>, headers: HeaderMap) -> Response {
|
||||
let base = request_base_path(&state, &headers);
|
||||
let body = INDEX_HTML
|
||||
@@ -88,6 +116,13 @@ fn forwarded_prefix(headers: &HeaderMap) -> Option<String> {
|
||||
}
|
||||
None
|
||||
}
|
||||
async fn custom_chart_js() -> Response {
|
||||
static_response(
|
||||
CUSTOM_CHART_JS,
|
||||
"application/javascript; charset=utf-8",
|
||||
"public, max-age=300",
|
||||
)
|
||||
}
|
||||
async fn app_js() -> Response {
|
||||
static_response(
|
||||
APP_JS,
|
||||
|
||||
@@ -22,12 +22,36 @@ async fn debug_api_requests(
|
||||
response
|
||||
}
|
||||
|
||||
fn trusted_home_assistant_ingress_parts(headers: &HeaderMap, peer: IpAddr) -> bool {
|
||||
let ingress_request = headers
|
||||
.get("x-ingress-path")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(str::trim)
|
||||
.is_some_and(|path| path.starts_with("/api/hassio_ingress/"));
|
||||
ingress_request && home_assistant::is_supervisor_ingress_peer(peer)
|
||||
}
|
||||
|
||||
fn trusted_home_assistant_ingress(request: &Request) -> bool {
|
||||
request
|
||||
.extensions()
|
||||
.get::<ConnectInfo<SocketAddr>>()
|
||||
.map(|info| trusted_home_assistant_ingress_parts(request.headers(), info.0.ip()))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn auth(
|
||||
State(state): State<AppState>,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, AppError> {
|
||||
if trusted_home_assistant_ingress(&request) {
|
||||
return Ok(next.run(request).await);
|
||||
}
|
||||
|
||||
let expected = state.config.app_token.trim();
|
||||
if home_assistant::supervisor_token_detected() && expected.is_empty() {
|
||||
return Err(AppError::Unauthorized);
|
||||
}
|
||||
if expected.is_empty() {
|
||||
return Ok(next.run(request).await);
|
||||
}
|
||||
|
||||
@@ -495,6 +495,274 @@ async fn history(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CreatePublicCustomChartRequest {
|
||||
title: Option<String>,
|
||||
series: Vec<String>,
|
||||
hours: Option<i64>,
|
||||
lang: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
struct PublicCustomChartShare {
|
||||
title: String,
|
||||
series: Vec<String>,
|
||||
hours: i64,
|
||||
lang: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct PublicChartPoint {
|
||||
timestamp: chrono::DateTime<Utc>,
|
||||
value: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct PublicChartSeries {
|
||||
key: String,
|
||||
label: String,
|
||||
dashed: bool,
|
||||
points: Vec<PublicChartPoint>,
|
||||
}
|
||||
|
||||
fn validate_public_chart_spec(series: &[String]) -> Result<(), AppError> {
|
||||
if series.is_empty()
|
||||
|| series.len() > 16
|
||||
|| series
|
||||
.iter()
|
||||
.any(|item| item.is_empty() || item.len() > 256)
|
||||
{
|
||||
return Err(AppError::BadRequest("invalid custom chart definition".into()));
|
||||
}
|
||||
|
||||
for key in series {
|
||||
let mut parts = key.splitn(3, '|');
|
||||
let kind = parts.next().unwrap_or_default();
|
||||
let id = parts.next().unwrap_or_default();
|
||||
let field = parts.next().unwrap_or_default();
|
||||
if id.trim().is_empty() || public_chart_field_label(kind, field, "en").is_none() {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"unsupported custom chart series: {key}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_public_chart_token() -> String {
|
||||
let mut bytes = [0u8; 32];
|
||||
let mut rng = OsRng;
|
||||
rng.fill_bytes(&mut bytes);
|
||||
format!("chart_{}", URL_SAFE_NO_PAD.encode(bytes))
|
||||
}
|
||||
|
||||
async fn create_public_custom_chart(
|
||||
State(state): State<AppState>,
|
||||
Json(input): Json<CreatePublicCustomChartRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
validate_public_chart_spec(&input.series)?;
|
||||
|
||||
let hours = input.hours.unwrap_or(24).clamp(1, 24 * 3650);
|
||||
let lang = if input.lang.as_deref() == Some("pl") {
|
||||
"pl"
|
||||
} else {
|
||||
"en"
|
||||
};
|
||||
let default_title = if lang == "pl" {
|
||||
"Wykres niestandardowy"
|
||||
} else {
|
||||
"Custom chart"
|
||||
};
|
||||
let title = input
|
||||
.title
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(default_title)
|
||||
.chars()
|
||||
.take(120)
|
||||
.collect::<String>();
|
||||
let share = PublicCustomChartShare {
|
||||
title,
|
||||
series: input.series,
|
||||
hours,
|
||||
lang: lang.to_string(),
|
||||
};
|
||||
|
||||
let token = generate_public_chart_token();
|
||||
let token_hash = hash_token(&token);
|
||||
state
|
||||
.db
|
||||
.save_public_chart_share(&token_hash, &serde_json::to_value(&share)?)?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"path": format!("/charts/custom/{token}")
|
||||
})))
|
||||
}
|
||||
|
||||
fn public_chart_field_label(kind: &str, field: &str, lang: &str) -> Option<&'static str> {
|
||||
let pl = lang == "pl";
|
||||
match (kind, field) {
|
||||
("device", "indoor") => Some(if pl { "Temperatura wewnętrzna" } else { "Indoor temperature" }),
|
||||
("device", "outdoor") => Some(if pl { "Temperatura zewnętrzna GREE" } else { "GREE outdoor temperature" }),
|
||||
("device", "target") => Some(if pl { "Temperatura zadana urządzenia" } else { "Device target" }),
|
||||
("installation", "outdoor") => Some(if pl { "Wspólna temperatura zewnętrzna" } else { "Shared outdoor temperature" }),
|
||||
("zone", "control") => Some(if pl { "Temperatura sterująca" } else { "Control temperature" }),
|
||||
("zone", "gree") => Some(if pl { "Czujnik GREE" } else { "GREE sensor" }),
|
||||
("zone", "external") => Some(if pl { "Czujnik pomieszczenia" } else { "Room sensor" }),
|
||||
("zone", "target") => Some(if pl { "Temperatura docelowa" } else { "Comfort target" }),
|
||||
("zone", "device_target") => Some(if pl { "Nastawa urządzenia" } else { "Device setpoint" }),
|
||||
("zone", "outdoor") => Some(if pl { "Temperatura zewnętrzna" } else { "Outdoor temperature" }),
|
||||
("ha", "temperature") => Some(if pl { "Temperatura" } else { "Temperature" }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn reading_points(rows: Vec<Reading>, field: &str) -> Vec<PublicChartPoint> {
|
||||
rows.into_iter()
|
||||
.filter_map(|row| {
|
||||
let value = match field {
|
||||
"indoor" => row.indoor_temperature,
|
||||
"outdoor" => row.outdoor_temperature,
|
||||
"target" => Some(row.target_temperature),
|
||||
_ => None,
|
||||
}?;
|
||||
value.is_finite().then_some(PublicChartPoint { timestamp: row.timestamp, value })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn zone_reading_points(rows: Vec<ZoneReading>, field: &str) -> Vec<PublicChartPoint> {
|
||||
rows.into_iter()
|
||||
.filter_map(|row| {
|
||||
let value = match field {
|
||||
"control" => row.control_temperature,
|
||||
"gree" => row.gree_temperature,
|
||||
"external" => row.external_temperature,
|
||||
"target" => row.target_temperature,
|
||||
"device_target" => row.device_setpoint,
|
||||
"outdoor" => row.outdoor_temperature,
|
||||
_ => None,
|
||||
}?;
|
||||
value.is_finite().then_some(PublicChartPoint { timestamp: row.timestamp, value })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn public_custom_chart(
|
||||
State(state): State<AppState>,
|
||||
Path(token): Path<String>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
if token.len() < 32 || token.len() > 128 || !token.starts_with("chart_") {
|
||||
return Err(AppError::NotFound("custom chart".into()));
|
||||
}
|
||||
let payload = state
|
||||
.db
|
||||
.get_public_chart_share(&hash_token(&token))?
|
||||
.ok_or_else(|| AppError::NotFound("custom chart".into()))?;
|
||||
let share: PublicCustomChartShare = serde_json::from_value(payload)?;
|
||||
validate_public_chart_spec(&share.series)?;
|
||||
|
||||
let hours = share.hours.clamp(1, 24 * 3650);
|
||||
let lang = if share.lang == "pl" { "pl" } else { "en" };
|
||||
let since = Utc::now() - ChronoDuration::hours(hours);
|
||||
let bucket_seconds = history_bucket_seconds(hours);
|
||||
let limit = 20_000;
|
||||
let ha_settings = state.settings.read().await.home_assistant.clone();
|
||||
let outdoor_entity = ha_settings.outdoor_entity_id.clone();
|
||||
let mut series = Vec::with_capacity(share.series.len());
|
||||
|
||||
for key in share.series {
|
||||
let mut parts = key.splitn(3, '|');
|
||||
let kind = parts.next().unwrap_or_default();
|
||||
let id = parts.next().unwrap_or_default();
|
||||
let field = parts.next().unwrap_or_default();
|
||||
let field_label = public_chart_field_label(kind, field, lang)
|
||||
.ok_or_else(|| AppError::BadRequest(format!("unsupported custom chart series: {key}")))?;
|
||||
|
||||
let item = match kind {
|
||||
"device" => {
|
||||
let device = state.db.get_device(id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("device {id}")))?;
|
||||
let (rows, _, _) = combined_device_history(&state, Some(id), since.clone(), bucket_seconds, limit).await?;
|
||||
PublicChartSeries {
|
||||
key: key.clone(),
|
||||
label: format!("{} · {}", device.name, field_label),
|
||||
dashed: field == "target",
|
||||
points: reading_points(rows, field),
|
||||
}
|
||||
}
|
||||
"installation" => {
|
||||
let group = state.db.get_device_group(id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("device group {id}")))?;
|
||||
let representative = group.outdoor_temperature_device_id.as_deref()
|
||||
.filter(|device_id| group.device_ids.iter().any(|member| member.as_str() == *device_id))
|
||||
.or_else(|| group.device_ids.first().map(String::as_str))
|
||||
.ok_or_else(|| AppError::BadRequest(format!("device group {id} has no devices")))?;
|
||||
let (rows, _, _) = combined_device_history(&state, Some(representative), since.clone(), bucket_seconds, limit).await?;
|
||||
PublicChartSeries {
|
||||
key: key.clone(),
|
||||
label: format!("{} · {}", group.name, field_label),
|
||||
dashed: false,
|
||||
points: reading_points(rows, "outdoor"),
|
||||
}
|
||||
}
|
||||
"zone" => {
|
||||
let zone = state.db.get_zone(id)?
|
||||
.ok_or_else(|| AppError::NotFound(format!("zone {id}")))?;
|
||||
let (rows, _, _) = combined_zone_history(&state, Some(id), since.clone(), bucket_seconds, limit).await?;
|
||||
PublicChartSeries {
|
||||
key: key.clone(),
|
||||
label: format!("{} · {}", zone.name, field_label),
|
||||
dashed: matches!(field, "target" | "device_target" | "outdoor"),
|
||||
points: zone_reading_points(rows, field),
|
||||
}
|
||||
}
|
||||
"ha" => {
|
||||
let (rows, _, _) = combined_sensor_history(
|
||||
&state,
|
||||
Some(id),
|
||||
since.clone(),
|
||||
bucket_seconds,
|
||||
limit,
|
||||
&outdoor_entity,
|
||||
).await?;
|
||||
let alias = ha_settings.sensor_aliases.get(id)
|
||||
.map(String::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or(id);
|
||||
PublicChartSeries {
|
||||
key: key.clone(),
|
||||
label: format!("HA · {} · {}", alias, field_label),
|
||||
dashed: true,
|
||||
points: rows.into_iter()
|
||||
.filter(|row| row.entity_id == id && row.temperature.is_finite())
|
||||
.map(|row| PublicChartPoint { timestamp: row.timestamp, value: row.temperature })
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
_ => return Err(AppError::BadRequest(format!("unsupported custom chart series: {key}"))),
|
||||
};
|
||||
series.push(item);
|
||||
}
|
||||
|
||||
let (hint, no_data_label) = if lang == "pl" {
|
||||
(format!("Ostatnie {hours} h"), "Brak danych")
|
||||
} else {
|
||||
(format!("Last {hours} h"), "No data")
|
||||
};
|
||||
|
||||
Ok(Json(json!({
|
||||
"title": share.title,
|
||||
"hint": hint,
|
||||
"no_data_label": no_data_label,
|
||||
"lang": lang,
|
||||
"hours": hours,
|
||||
"bucket_seconds": bucket_seconds,
|
||||
"series": series,
|
||||
})))
|
||||
}
|
||||
|
||||
async fn control_plan(State(state): State<AppState>) -> Result<Json<Value>, AppError> {
|
||||
let snapshot = engine::get_control_plan_snapshot(&state).await?;
|
||||
Ok(Json(serde_json::to_value(snapshot.plan.as_ref())?))
|
||||
|
||||
@@ -24,7 +24,11 @@ async fn security_headers(request: Request, next: Next) -> Response {
|
||||
);
|
||||
headers.insert(
|
||||
header::HeaderName::from_static("referrer-policy"),
|
||||
HeaderValue::from_static("same-origin"),
|
||||
HeaderValue::from_static(if path.starts_with("/charts/custom/") {
|
||||
"no-referrer"
|
||||
} else {
|
||||
"same-origin"
|
||||
}),
|
||||
);
|
||||
|
||||
if is_html {
|
||||
|
||||
+21
-4
@@ -107,15 +107,32 @@ struct BootstrapResponse {
|
||||
system: SystemInfoResponse,
|
||||
}
|
||||
|
||||
async fn health(State(state): State<AppState>) -> Json<Value> {
|
||||
Json(json!({
|
||||
async fn health(
|
||||
State(state): State<AppState>,
|
||||
request: Request,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
if home_assistant::supervisor_token_detected() {
|
||||
let trusted_supervisor = request
|
||||
.extensions()
|
||||
.get::<ConnectInfo<SocketAddr>>()
|
||||
.map(|info| home_assistant::is_supervisor_ingress_peer(info.0.ip()))
|
||||
.unwrap_or(false);
|
||||
if !trusted_supervisor {
|
||||
let expected = state.config.app_token.trim();
|
||||
if expected.is_empty() || request_token(&request).as_deref() != Some(expected) {
|
||||
return Err(AppError::Unauthorized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"name": "gree-controller",
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"uptime_seconds": state.started.elapsed().as_secs(),
|
||||
"control_ready": state.initial_device_sync_complete.load(Ordering::Acquire),
|
||||
"time": Utc::now(),
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
async fn bootstrap(State(state): State<AppState>) -> Result<Json<BootstrapResponse>, AppError> {
|
||||
@@ -162,7 +179,7 @@ fn build_system_info(state: &AppState, devices: &[Device]) -> SystemInfoResponse
|
||||
SystemInfoResponse {
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
uptime_seconds: state.started.elapsed().as_secs(),
|
||||
auth_required: !state.config.app_token.trim().is_empty(),
|
||||
auth_required: !state.config.app_token.trim().is_empty() || home_assistant::supervisor_token_detected(),
|
||||
control_ready: state.initial_device_sync_complete.load(Ordering::Acquire),
|
||||
database: state.config.database.display().to_string(),
|
||||
device_count: devices.len(),
|
||||
|
||||
+10
-3
@@ -5,11 +5,18 @@ struct WsQuery {
|
||||
async fn websocket(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<WsQuery>,
|
||||
ConnectInfo(peer): ConnectInfo<SocketAddr>,
|
||||
headers: HeaderMap,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> Result<Response, AppError> {
|
||||
let expected = state.config.app_token.trim();
|
||||
if !expected.is_empty() && query.token.as_deref() != Some(expected) {
|
||||
return Err(AppError::Unauthorized);
|
||||
if !trusted_home_assistant_ingress_parts(&headers, peer.ip()) {
|
||||
let expected = state.config.app_token.trim();
|
||||
if home_assistant::supervisor_token_detected() && expected.is_empty() {
|
||||
return Err(AppError::Unauthorized);
|
||||
}
|
||||
if !expected.is_empty() && query.token.as_deref() != Some(expected) {
|
||||
return Err(AppError::Unauthorized);
|
||||
}
|
||||
}
|
||||
Ok(ws.on_upgrade(move |socket| websocket_loop(state, socket)))
|
||||
}
|
||||
|
||||
@@ -109,4 +109,28 @@ impl Db {
|
||||
let conn = self.lock()?;
|
||||
Ok(conn.execute(queries::DELETE_API_TOKEN, [id])? > 0)
|
||||
}
|
||||
|
||||
pub fn save_public_chart_share(&self, token_hash: &str, payload: &Value) -> Result<()> {
|
||||
let conn = self.lock()?;
|
||||
conn.execute(
|
||||
queries::INSERT_PUBLIC_CHART_SHARE,
|
||||
params![
|
||||
token_hash,
|
||||
serde_json::to_string(payload)?,
|
||||
Utc::now().to_rfc3339()
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_public_chart_share(&self, token_hash: &str) -> Result<Option<Value>> {
|
||||
let conn = self.lock()?;
|
||||
let payload: Option<String> = conn
|
||||
.query_row(queries::GET_PUBLIC_CHART_SHARE, [token_hash], |row| row.get(0))
|
||||
.optional()?;
|
||||
payload
|
||||
.map(|value| serde_json::from_str(&value))
|
||||
.transpose()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,19 @@ mod tests {
|
||||
assert!(db.delete_api_token(&access_token.id).unwrap());
|
||||
assert!(!db.api_token_exists("test-hash").unwrap());
|
||||
|
||||
let chart_share = serde_json::json!({
|
||||
"title": "Room temperatures",
|
||||
"series": ["device|dev-1|indoor"],
|
||||
"hours": 24,
|
||||
"lang": "en"
|
||||
});
|
||||
db.save_public_chart_share("chart-hash", &chart_share).unwrap();
|
||||
assert_eq!(
|
||||
db.get_public_chart_share("chart-hash").unwrap(),
|
||||
Some(chart_share)
|
||||
);
|
||||
assert!(db.get_public_chart_share("missing-chart").unwrap().is_none());
|
||||
|
||||
let now = Utc::now();
|
||||
db.add_reading(&Reading {
|
||||
id: 0,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use crate::models::HomeAssistantSettings;
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use serde_json::Value;
|
||||
use std::{env, time::Duration};
|
||||
use std::{
|
||||
env,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
time::Duration,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
const SUPERVISOR_AUTH_ENV: &str = "GREE_CONTROLLER_HA_AUTH";
|
||||
@@ -22,6 +26,10 @@ pub fn supervisor_token_detected() -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn is_supervisor_ingress_peer(peer: IpAddr) -> bool {
|
||||
supervisor_token_detected() && peer == IpAddr::V4(Ipv4Addr::new(172, 30, 32, 2))
|
||||
}
|
||||
|
||||
pub fn uses_supervisor_auth(settings: &HomeAssistantSettings) -> bool {
|
||||
supervisor_detected() && !settings.manual_auth_override
|
||||
}
|
||||
|
||||
+5
-2
@@ -144,7 +144,7 @@ async fn main() -> Result<()> {
|
||||
address = %config.bind,
|
||||
database = %config.database.display(),
|
||||
simulator = config.simulate,
|
||||
auth = !config.app_token.trim().is_empty(),
|
||||
auth = !config.app_token.trim().is_empty() || home_assistant::supervisor_token_detected(),
|
||||
gree_interface = %gree_interface_log,
|
||||
discovery_broadcast = %runtime_settings.discovery_broadcast,
|
||||
"GREE Controller started"
|
||||
@@ -158,7 +158,10 @@ async fn main() -> Result<()> {
|
||||
let _ = shutdown_tx.send(true);
|
||||
});
|
||||
|
||||
let server = axum::serve(listener, app)
|
||||
let server = axum::serve(
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
|
||||
)
|
||||
.with_graceful_shutdown(wait_for_shutdown(shutdown_rx.clone()))
|
||||
.into_future();
|
||||
tokio::pin!(server);
|
||||
|
||||
@@ -64,6 +64,11 @@ pub const INSERT_API_TOKEN: &str =
|
||||
pub const API_TOKEN_EXISTS: &str = "SELECT 1 FROM api_tokens WHERE token_hash=?1 LIMIT 1";
|
||||
pub const DELETE_API_TOKEN: &str = "DELETE FROM api_tokens WHERE id=?1";
|
||||
|
||||
pub const INSERT_PUBLIC_CHART_SHARE: &str =
|
||||
"INSERT INTO public_chart_shares(token_hash,payload,created_at) VALUES(?1,?2,?3)";
|
||||
pub const GET_PUBLIC_CHART_SHARE: &str =
|
||||
"SELECT payload FROM public_chart_shares WHERE token_hash=?1 LIMIT 1";
|
||||
|
||||
pub const LOAD_RUNTIME_SETTINGS: &str = "SELECT value FROM settings WHERE key='runtime'";
|
||||
pub const UPSERT_RUNTIME_SETTINGS: &str = r#"
|
||||
INSERT INTO settings(key,value,updated_at) VALUES('runtime',?1,?2)
|
||||
|
||||
@@ -165,6 +165,12 @@ CREATE TABLE IF NOT EXISTS api_tokens (
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public_chart_shares (
|
||||
token_hash TEXT PRIMARY KEY,
|
||||
payload TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO schema_migrations(version, applied_at)
|
||||
VALUES (1, strftime('%Y-%m-%dT%H:%M:%fZ','now'));
|
||||
INSERT OR IGNORE INTO schema_migrations(version, applied_at)
|
||||
@@ -183,5 +189,7 @@ 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'));
|
||||
INSERT OR IGNORE INTO schema_migrations(version, applied_at)
|
||||
VALUES (10, strftime('%Y-%m-%dT%H:%M:%fZ','now'));
|
||||
"#;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user