diff --git a/Cargo.lock b/Cargo.lock index b99536b9..b48fb100 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,6 +279,7 @@ dependencies = [ "bytes", "camino", "chrono", + "chrono-tz", "clap", "clap-stdin", "config", @@ -288,7 +289,6 @@ dependencies = [ "hex", "hue", "hyper", - "iana-time-zone", "itertools", "json_diff_ng", "log", @@ -325,7 +325,6 @@ dependencies = [ "tower 0.5.2", "tower-http", "tracing", - "tzfile", "udp-stream", "url", "uuid", @@ -339,6 +338,7 @@ name = "bifrost-api" version = "0.1.0" dependencies = [ "camino", + "chrono-tz", "hue", "mac_address", "reqwest", @@ -438,6 +438,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "chrono-tz" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", + "serde", +] + +[[package]] +name = "chrono-tz-build" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402" +dependencies = [ + "parse-zoneinfo", + "phf_codegen", +] + [[package]] name = "clap" version = "4.5.29" @@ -1026,8 +1048,8 @@ dependencies = [ "bitflags", "byteorder", "chrono", + "chrono-tz", "hex", - "iana-time-zone", "mac_address", "maplit", "packed_struct", @@ -1681,6 +1703,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "pathdiff" version = "0.2.3" @@ -1702,6 +1733,44 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.9" @@ -2654,16 +2723,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "tzfile" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f59c22c42a2537e4c7ad21a4007273bbc5bebed7f36bc93730a5780e22a4592e" -dependencies = [ - "byteorder", - "chrono", -] - [[package]] name = "udp-stream" version = "0.0.12" diff --git a/Cargo.toml b/Cargo.toml index ce701bf8..18ce71fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,7 +88,6 @@ clap = { version = "4.5.29", features = ["std", "color", "derive", "help", "usag config = { version = "0.15.8", default-features = false, features = ["yaml"] } futures = "0.3.31" hyper = "1.6.0" -iana-time-zone = "0.1.61" log = "0.4.25" mac_address = { version = "1.1.8", features = ["serde"] } mdns-sd = "0.13.2" @@ -134,9 +133,9 @@ tokio-ssdp = { git = "https://github.com/chrivers/tokio-ssdp.git", rev = "00fc29 udp-stream = { git = "https://github.com/chrivers/udp-stream.git", rev = "da6c76bb" } native-tls = "0.2.13" tokio-native-tls = "0.3.1" -tzfile = "0.1.3" bifrost-api = { version = "0.1.0", path = "crates/bifrost-api", features = ["mac"] } nix = { version = "0.30.0", default-features = false, features = ["socket"] } +chrono-tz = "0.10.3" [dev-dependencies] clap-stdin = "0.6.0" diff --git a/crates/bifrost-api/Cargo.toml b/crates/bifrost-api/Cargo.toml index beb8ab1a..710ddaab 100644 --- a/crates/bifrost-api/Cargo.toml +++ b/crates/bifrost-api/Cargo.toml @@ -27,6 +27,7 @@ hue = { version = "0.1.0", path = "../hue", default-features = false, features = svc = { version = "0.1.0", path = "../svc", default-features = false } mac_address = { version = "1.1.8", optional = true } +chrono-tz = { version = "0.10.3", features = ["serde"] } [features] default = [] diff --git a/crates/bifrost-api/src/config.rs b/crates/bifrost-api/src/config.rs index f14197aa..2d0722e5 100644 --- a/crates/bifrost-api/src/config.rs +++ b/crates/bifrost-api/src/config.rs @@ -2,6 +2,7 @@ use std::net::Ipv4Addr; use std::{collections::BTreeMap, num::NonZeroU32}; use camino::Utf8PathBuf; +use chrono_tz::Tz; use hue::api::RoomArchetype; use serde::{Deserialize, Serialize}; use url::Url; @@ -23,7 +24,7 @@ pub struct BridgeConfig { pub entm_port: u16, pub netmask: Ipv4Addr, pub gateway: Ipv4Addr, - pub timezone: String, + pub timezone: Tz, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] diff --git a/crates/hue/Cargo.toml b/crates/hue/Cargo.toml index 53e90b78..d2535065 100644 --- a/crates/hue/Cargo.toml +++ b/crates/hue/Cargo.toml @@ -20,7 +20,6 @@ bitflags = "2.8.0" byteorder = "1.5.0" chrono = { version = "0.4.39", default-features = false, features = ["clock", "std"] } hex = "0.4.3" -iana-time-zone = "0.1.61" packed_struct = "0.10.1" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.140" @@ -30,6 +29,7 @@ uuid = { version = "1.13.1", features = ["serde", "v5"] } mac_address = { version = "1.1.8", features = ["serde"], optional = true } maplit = "1.0.2" +chrono-tz = { version = "0.10.3", features = ["serde"] } [features] default = ["event", "mac", "rng"] diff --git a/crates/hue/src/api/stubs.rs b/crates/hue/src/api/stubs.rs index 092d6b68..b9cb8ea7 100644 --- a/crates/hue/src/api/stubs.rs +++ b/crates/hue/src/api/stubs.rs @@ -1,11 +1,12 @@ use std::collections::BTreeSet; use chrono::{DateTime, Utc}; +use chrono_tz::Tz; use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::api::{DeviceArchetype, LightFunction, ResourceLink, SceneMetadata}; -use crate::{best_guess_timezone, date_format}; +use crate::date_format; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Bridge { @@ -210,15 +211,12 @@ pub struct Temperature { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct TimeZone { - pub time_zone: String, + pub time_zone: Tz, } -impl TimeZone { - #[must_use] - pub fn best_guess() -> Self { - Self { - time_zone: best_guess_timezone(), - } +impl From for TimeZone { + fn from(tz: Tz) -> Self { + Self { time_zone: tz } } } diff --git a/crates/hue/src/legacy_api.rs b/crates/hue/src/legacy_api.rs index 4a892544..c66c5013 100644 --- a/crates/hue/src/legacy_api.rs +++ b/crates/hue/src/legacy_api.rs @@ -1,14 +1,15 @@ use std::{collections::HashMap, net::Ipv4Addr}; use chrono::{DateTime, Local, NaiveDateTime, Utc}; +use chrono_tz::Tz; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use uuid::Uuid; +use crate::api; use crate::api::{ColorGamut, DeviceProductData}; use crate::date_format; use crate::hs::RawHS; -use crate::{api, best_guess_timezone}; #[cfg(feature = "mac")] use crate::version::SwVersion; @@ -253,7 +254,7 @@ pub struct ApiConfig { pub ipaddress: Ipv4Addr, pub netmask: Ipv4Addr, pub gateway: Ipv4Addr, - pub timezone: String, + pub timezone: Tz, #[serde(with = "date_format::legacy_utc", rename = "UTC")] pub utc: DateTime, #[serde(with = "date_format::legacy_naive")] @@ -261,6 +262,12 @@ pub struct ApiConfig { pub whitelist: HashMap, } +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct ApiConfigUpdate { + #[serde(skip_serializing_if = "Option::is_none")] + pub timezone: Option, +} + #[derive(Debug, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum ApiEffect { @@ -796,7 +803,7 @@ impl Default for ApiConfig { ipaddress: Ipv4Addr::UNSPECIFIED, netmask: Ipv4Addr::UNSPECIFIED, gateway: Ipv4Addr::UNSPECIFIED, - timezone: best_guess_timezone(), + timezone: Tz::UTC, utc: Utc::now(), localtime: Local::now().naive_local(), whitelist: HashMap::new(), @@ -899,12 +906,8 @@ impl Capabilities { channels: 20, }, timezones: json!({ - "values": [ - "CET", - "UTC", - "GMT", - "Europe/Copenhagen", - ], + "values": + chrono_tz::TZ_VARIANTS.iter().collect::>(), }), } } diff --git a/crates/hue/src/lib.rs b/crates/hue/src/lib.rs index c089776f..ad15bf3a 100644 --- a/crates/hue/src/lib.rs +++ b/crates/hue/src/lib.rs @@ -34,11 +34,6 @@ pub const HUE_BRIDGE_V2_MODEL_ID: &str = "BSB002"; pub const HUE_BRIDGE_V2_DEFAULT_SWVERSION: u64 = 1_970_084_010; pub const HUE_BRIDGE_V2_DEFAULT_APIVERSION: &str = "1.70.0"; -#[must_use] -pub fn best_guess_timezone() -> String { - iana_time_zone::get_timezone().unwrap_or_else(|_| "none".to_string()) -} - #[cfg(feature = "mac")] #[must_use] pub fn bridge_id_raw(mac: MacAddress) -> [u8; 8] { @@ -144,13 +139,6 @@ mod tests { ); } - #[test] - fn best_guess_timezone() { - let res = crate::best_guess_timezone(); - assert!(!res.is_empty()); - assert_ne!(res, "none"); - } - #[test] fn bridge_id() { let mac = MacAddress::new([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); diff --git a/src/resource.rs b/src/resource.rs index 88d6a43b..601fbd0c 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use std::io::{Read, Write}; use std::sync::Arc; +use chrono_tz::Tz; use itertools::Itertools; use maplit::btreeset; use serde::Serialize; @@ -14,7 +15,7 @@ use bifrost_api::backend::BackendRequest; use hue::api::{ Bridge, BridgeHome, Device, DeviceArchetype, DeviceProductData, DimmingUpdate, Entertainment, EntertainmentConfiguration, GroupedLight, Light, Metadata, On, RType, Resource, ResourceLink, - ResourceRecord, Room, Stub, TimeZone, ZigbeeConnectivity, ZigbeeConnectivityStatus, + ResourceRecord, Room, Stub, ZigbeeConnectivity, ZigbeeConnectivityStatus, ZigbeeDeviceDiscovery, ZigbeeDeviceDiscoveryAction, ZigbeeDeviceDiscoveryStatus, Zone, }; use hue::error::{HueError, HueResult}; @@ -89,8 +90,8 @@ impl Resources { Ok(serde_yml::to_string(&self.state)?) } - pub fn init(&mut self, bridge_id: &str) -> ApiResult<()> { - self.add_bridge(bridge_id.to_owned()) + pub fn init(&mut self, bridge_id: &str, timezone: Tz) -> ApiResult<()> { + self.add_bridge(bridge_id.to_owned(), timezone) } pub fn aux_get(&self, link: &ResourceLink) -> ApiResult<&AuxData> { @@ -271,7 +272,7 @@ impl Resources { Ok(()) } - pub fn add_bridge(&mut self, bridge_id: String) -> ApiResult<()> { + pub fn add_bridge(&mut self, bridge_id: String, timezone: Tz) -> ApiResult<()> { let link_bridge = RType::Bridge.deterministic(&bridge_id); let link_bridge_home = RType::BridgeHome.deterministic(format!("{bridge_id}HOME")); let link_bridge_dev = RType::Device.deterministic(link_bridge.rid); @@ -292,7 +293,7 @@ impl Resources { let bridge = Bridge { bridge_id, owner: link_bridge_dev, - time_zone: TimeZone::best_guess(), + time_zone: timezone.into(), }; let bridge_home_dev = Device { diff --git a/src/routes/api.rs b/src/routes/api.rs index a8e81588..f255e753 100644 --- a/src/routes/api.rs +++ b/src/routes/api.rs @@ -21,10 +21,10 @@ use hue::api::{ }; use hue::error::{HueApiV1Error, HueError, HueResult}; use hue::legacy_api::{ - ApiGroup, ApiGroupAction, ApiGroupActionUpdate, ApiGroupClass, ApiGroupNew, ApiGroupState, - ApiGroupType, ApiGroupUpdate2, ApiLight, ApiLightStateUpdate, ApiResourceType, ApiScene, - ApiSceneAppData, ApiSceneType, ApiSceneVersion, ApiSensor, ApiUserConfig, Capabilities, - HueApiResult, NewUser, NewUserReply, + ApiConfigUpdate, ApiGroup, ApiGroupAction, ApiGroupActionUpdate, ApiGroupClass, ApiGroupNew, + ApiGroupState, ApiGroupType, ApiGroupUpdate2, ApiLight, ApiLightStateUpdate, ApiResourceType, + ApiScene, ApiSceneAppData, ApiSceneType, ApiSceneVersion, ApiSensor, ApiUserConfig, + Capabilities, HueApiResult, NewUser, NewUserReply, }; use crate::error::{ApiError, ApiResult}; @@ -331,12 +331,30 @@ async fn post_api_user_resource( } async fn put_api_user_resource( - Path((_username, _resource)): Path<(String, String)>, + State(state): State, + Path((_username, resource)): Path<(String, ApiResourceType)>, Json(req): Json, ) -> ApiV1Result> { - warn!("PUT v1 user resource {req:?}"); - //Json(format!("user {username} resource {resource}")) - Ok(Json(vec![HueApiResult::Success(req)])) + let result = match resource { + ApiResourceType::Config => { + let upd: ApiConfigUpdate = serde_json::from_value(req.clone())?; + let mut config = (*state.config()).clone(); + + if let Some(timezone) = upd.timezone { + config.bridge.timezone = timezone; + } + + // todo: persist config + state.replace_config(config); + + vec![HueApiResult::Success(req)] + } + resource => { + warn!("PUT v1 user resource {resource:?} {req:?}"); + vec![HueApiResult::Success(req)] + } + }; + Ok(Json(result)) } #[allow(clippy::significant_drop_tightening)] diff --git a/src/server/appstate.rs b/src/server/appstate.rs index db42b872..5621d588 100644 --- a/src/server/appstate.rs +++ b/src/server/appstate.rs @@ -4,7 +4,8 @@ use std::sync::Arc; use camino::Utf8Path; use chrono::Utc; -use tokio::sync::Mutex; +use tokio::sync::watch::Sender; +use tokio::sync::{Mutex, watch}; use hue::legacy_api::{ApiConfig, ApiShortConfig, Whitelist}; use svc::manager::SvmClient; @@ -18,7 +19,7 @@ use crate::server::updater::VersionUpdater; #[derive(Clone)] pub struct AppState { - conf: Arc, + conf: Sender, upd: Arc>, svm: SvmClient, pub res: Arc>, @@ -60,14 +61,15 @@ impl AppState { } else { log::debug!("No state file found, initializing.."); res = Resources::new(swversion, State::new()); - res.init(&hue::bridge_id(config.bridge.mac))?; + res.init(&hue::bridge_id(config.bridge.mac), config.bridge.timezone)?; } res.reset_all_streaming()?; - let conf = Arc::new(config); let res = Arc::new(Mutex::new(res)); + let conf = Sender::new(config); + Ok(Self { conf, upd, @@ -78,7 +80,17 @@ impl AppState { #[must_use] pub fn config(&self) -> Arc { - self.conf.clone() + Arc::new(self.conf.borrow().clone()) + } + + #[must_use] + pub fn config_subscribe(&self) -> watch::Receiver { + self.conf.subscribe() + } + + #[allow(clippy::must_use_candidate)] + pub fn replace_config(&self, config: AppConfig) -> AppConfig { + self.conf.send_replace(config) } #[must_use] @@ -93,20 +105,22 @@ impl AppState { #[must_use] pub async fn api_short_config(&self) -> ApiShortConfig { - let mac = self.conf.bridge.mac; + let mac = self.conf.borrow().bridge.mac; ApiShortConfig::from_mac_and_version(mac, self.upd.lock().await.get().await) } pub async fn api_config(&self, username: String) -> ApiResult { - let tz = tzfile::Tz::named(&self.conf.bridge.timezone)?; - let localtime = Utc::now().with_timezone(&&tz).naive_local(); + let conf = self.config(); + let localtime = Utc::now() + .with_timezone(&conf.bridge.timezone) + .naive_local(); let res = ApiConfig { short_config: self.api_short_config().await, - ipaddress: self.conf.bridge.ipaddress, - netmask: self.conf.bridge.netmask, - gateway: self.conf.bridge.gateway, - timezone: self.conf.bridge.timezone.clone(), + ipaddress: conf.bridge.ipaddress, + netmask: conf.bridge.netmask, + gateway: conf.bridge.gateway, + timezone: conf.bridge.timezone, whitelist: HashMap::from([( username, Whitelist {