first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:29:38 +03:00
commit 427856cd3a
176 changed files with 27613 additions and 0 deletions

View 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"]

View 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
}
}

View 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
}
}

View 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
}
}

View 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>;

View 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;
}

View 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
}
}

View 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),
}