first commit
This commit is contained in:
34
crates/bifrost-api/Cargo.toml
Normal file
34
crates/bifrost-api/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "bifrost-api"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
rust-version.workspace = true
|
||||
description.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
categories.workspace = true
|
||||
keywords.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
camino = { version = "1.1.9", features = ["serde", "serde1"] }
|
||||
reqwest = { version = "0.12.15", default-features = false, features = ["json"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
thiserror = "2.0.12"
|
||||
url = { version = "2.5.4", features = ["serde"] }
|
||||
uuid = { version = "1.16.0", features = ["serde"] }
|
||||
serde_json = "1.0.140"
|
||||
|
||||
hue = { version = "0.1.0", path = "../hue", default-features = false, features = ["event"] }
|
||||
svc = { version = "0.1.0", path = "../svc", default-features = false }
|
||||
|
||||
mac_address = { version = "1.1.8", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
mac = ["dep:mac_address"]
|
||||
39
crates/bifrost-api/src/backend.rs
Normal file
39
crates/bifrost-api/src/backend.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use hue::api::{
|
||||
GroupedLightUpdate, LightUpdate, ResourceLink, RoomUpdate, Scene, SceneUpdate,
|
||||
ZigbeeDeviceDiscoveryUpdate,
|
||||
};
|
||||
use hue::stream::HueStreamLightsV2;
|
||||
|
||||
use crate::Client;
|
||||
use crate::config::Z2mServer;
|
||||
use crate::error::BifrostResult;
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum BackendRequest {
|
||||
LightUpdate(ResourceLink, LightUpdate),
|
||||
|
||||
SceneCreate(ResourceLink, u32, Scene),
|
||||
SceneUpdate(ResourceLink, SceneUpdate),
|
||||
|
||||
GroupedLightUpdate(ResourceLink, GroupedLightUpdate),
|
||||
|
||||
RoomUpdate(ResourceLink, RoomUpdate),
|
||||
|
||||
Delete(ResourceLink),
|
||||
|
||||
EntertainmentStart(Uuid),
|
||||
EntertainmentFrame(HueStreamLightsV2),
|
||||
EntertainmentStop(),
|
||||
|
||||
ZigbeeDeviceDiscovery(ResourceLink, ZigbeeDeviceDiscoveryUpdate),
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub async fn post_backend(&self, name: &str, backend: Z2mServer) -> BifrostResult<()> {
|
||||
self.post(&format!("backend/z2m/{name}"), backend).await
|
||||
}
|
||||
}
|
||||
62
crates/bifrost-api/src/client.rs
Normal file
62
crates/bifrost-api/src/client.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use reqwest::{Method, Url};
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::error::BifrostResult;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Client {
|
||||
client: reqwest::Client,
|
||||
url: Url,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
#[must_use]
|
||||
pub const fn new(client: reqwest::Client, url: Url) -> Self {
|
||||
Self { client, url }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_url(url: Url) -> Self {
|
||||
Self::new(reqwest::Client::new(), url)
|
||||
}
|
||||
|
||||
pub async fn request<I: Serialize, O: DeserializeOwned>(
|
||||
&self,
|
||||
scope: &str,
|
||||
method: Method,
|
||||
data: Option<I>,
|
||||
) -> BifrostResult<O> {
|
||||
let url = self.url.join(scope)?;
|
||||
|
||||
let mut req = self.client.request(method, url);
|
||||
|
||||
if let Some(data) = data {
|
||||
req = req.json(&data);
|
||||
}
|
||||
|
||||
let response = req.send().await?.error_for_status()?.json().await?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn get<T: DeserializeOwned>(&self, scope: &str) -> BifrostResult<T> {
|
||||
self.request(scope, Method::GET, None::<()>).await
|
||||
}
|
||||
|
||||
pub async fn post<I: Serialize, O: DeserializeOwned>(
|
||||
&self,
|
||||
scope: &str,
|
||||
data: I,
|
||||
) -> BifrostResult<O> {
|
||||
self.request(scope, Method::POST, Some(data)).await
|
||||
}
|
||||
|
||||
pub async fn put<I: Serialize, O: DeserializeOwned>(
|
||||
&self,
|
||||
scope: &str,
|
||||
data: I,
|
||||
) -> BifrostResult<O> {
|
||||
self.request(scope, Method::PUT, Some(data)).await
|
||||
}
|
||||
}
|
||||
120
crates/bifrost-api/src/config.rs
Normal file
120
crates/bifrost-api/src/config.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use std::net::Ipv4Addr;
|
||||
use std::{collections::BTreeMap, num::NonZeroU32};
|
||||
|
||||
use camino::Utf8PathBuf;
|
||||
use hue::api::RoomArchetype;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use crate::{Client, error::BifrostResult};
|
||||
|
||||
#[cfg(feature = "mac")]
|
||||
use mac_address::MacAddress;
|
||||
#[cfg(not(feature = "mac"))]
|
||||
type MacAddress = String;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct BridgeConfig {
|
||||
pub name: String,
|
||||
pub mac: MacAddress,
|
||||
pub ipaddress: Ipv4Addr,
|
||||
pub http_port: u16,
|
||||
pub https_port: u16,
|
||||
pub entm_port: u16,
|
||||
pub netmask: Ipv4Addr,
|
||||
pub gateway: Ipv4Addr,
|
||||
pub timezone: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct BifrostConfig {
|
||||
pub state_file: Utf8PathBuf,
|
||||
pub cert_file: Utf8PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct Z2mConfig {
|
||||
#[serde(flatten)]
|
||||
pub servers: BTreeMap<String, Z2mServer>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct Z2mServer {
|
||||
pub url: Url,
|
||||
pub group_prefix: Option<String>,
|
||||
pub disable_tls_verify: Option<bool>,
|
||||
pub streaming_fps: Option<NonZeroU32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default, Eq, PartialEq)]
|
||||
pub struct RoomConfig {
|
||||
pub name: Option<String>,
|
||||
pub icon: Option<RoomArchetype>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct AppConfig {
|
||||
pub bridge: BridgeConfig,
|
||||
pub z2m: Z2mConfig,
|
||||
pub bifrost: BifrostConfig,
|
||||
#[serde(default)]
|
||||
pub rooms: BTreeMap<String, RoomConfig>,
|
||||
}
|
||||
|
||||
impl Z2mServer {
|
||||
#[must_use]
|
||||
pub fn get_url(&self) -> Url {
|
||||
let mut url = self.url.clone();
|
||||
// z2m version 1.x allows both / and /api as endpoints for the
|
||||
// websocket, but version 2.x only allows /api. By adding /api (if
|
||||
// missing), we ensure compatibility with both versions.
|
||||
if !url.path().ends_with("/api") {
|
||||
if let Ok(mut path) = url.path_segments_mut() {
|
||||
path.push("api");
|
||||
}
|
||||
}
|
||||
|
||||
// z2m version 2.x requires an auth token on the websocket. If one is
|
||||
// not specified in the z2m configuration, the literal string
|
||||
// `your-secret-token` is used!
|
||||
//
|
||||
// To be compatible, we mirror this behavior here. If "token" is set
|
||||
// manually by the user, we do nothing.
|
||||
if !url.query_pairs().any(|(key, _)| key == "token") {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("token", "your-secret-token");
|
||||
}
|
||||
|
||||
url
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
fn sanitize_url(url: &str) -> String {
|
||||
match url.find("token=") {
|
||||
Some(offset) => {
|
||||
let token = &url[offset + "token=".len()..];
|
||||
if token == "your-secret-token" {
|
||||
// this is the standard "blank" token, it's safe to show
|
||||
url.to_string()
|
||||
} else {
|
||||
// this is an actual secret token, blank it out with a
|
||||
// standard-length placeholder.
|
||||
format!("{}token={}", &url[..offset], "<<REDACTED>>")
|
||||
}
|
||||
}
|
||||
None => url.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_sanitized_url(&self) -> String {
|
||||
Self::sanitize_url(self.get_url().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub async fn config(&self) -> BifrostResult<AppConfig> {
|
||||
self.get("config").await
|
||||
}
|
||||
}
|
||||
15
crates/bifrost-api/src/error.rs
Normal file
15
crates/bifrost-api/src/error.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BifrostError {
|
||||
#[error(transparent)]
|
||||
ReqwestError(#[from] reqwest::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
UrlParseError(#[from] url::ParseError),
|
||||
|
||||
#[error("Server error: {0}")]
|
||||
ServerError(String),
|
||||
}
|
||||
|
||||
pub type BifrostResult<T> = Result<T, BifrostError>;
|
||||
13
crates/bifrost-api/src/lib.rs
Normal file
13
crates/bifrost-api/src/lib.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
pub mod backend;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod service;
|
||||
pub mod websocket;
|
||||
|
||||
mod client;
|
||||
pub use client::*;
|
||||
|
||||
pub mod export {
|
||||
pub extern crate hue;
|
||||
pub extern crate svc;
|
||||
}
|
||||
38
crates/bifrost-api/src/service.rs
Normal file
38
crates/bifrost-api/src/service.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use svc::serviceid::ServiceName;
|
||||
use svc::traits::ServiceState;
|
||||
|
||||
use crate::Client;
|
||||
use crate::error::BifrostResult;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct Service {
|
||||
pub id: Uuid,
|
||||
pub name: ServiceName,
|
||||
pub state: ServiceState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default, Eq, PartialEq)]
|
||||
pub struct ServiceList {
|
||||
pub services: BTreeMap<Uuid, Service>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub async fn service_list(&self) -> BifrostResult<ServiceList> {
|
||||
self.get("service").await
|
||||
}
|
||||
|
||||
pub async fn service_stop(&self, id: Uuid) -> BifrostResult<Uuid> {
|
||||
self.put(&format!("service/{id}"), ServiceState::Stopped)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn service_start(&self, id: Uuid) -> BifrostResult<Uuid> {
|
||||
self.put(&format!("service/{id}"), ServiceState::Running)
|
||||
.await
|
||||
}
|
||||
}
|
||||
14
crates/bifrost-api/src/websocket.rs
Normal file
14
crates/bifrost-api/src/websocket.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use hue::event::EventBlock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::backend::BackendRequest;
|
||||
use crate::config::AppConfig;
|
||||
use crate::service::Service;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum Update {
|
||||
AppConfig(AppConfig),
|
||||
HueEvent(EventBlock),
|
||||
BackendRequest(BackendRequest),
|
||||
ServiceUpdate(Service),
|
||||
}
|
||||
Reference in New Issue
Block a user