use serde::Deserialize;
use crate::config::{CameraConfig, Config, MqttServerConfig};
#[derive(Debug, Clone, Default)]
pub struct MqttServiceFlags {
pub host: Option<String>,
pub port: Option<u16>,
pub username: Option<String>,
pub password: Option<String>,
pub ssl: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HassioOptions {
#[serde(default = "default_topic_prefix")]
pub topic_prefix: String,
#[serde(default = "default_log_level")]
pub log_level: String,
#[serde(default)]
pub cameras: Vec<HassioCamera>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HassioCamera {
pub name: String,
#[serde(default)]
pub address: Option<String>,
#[serde(default)]
pub uid: Option<String>,
#[serde(default = "default_username")]
pub username: String,
pub password: String,
#[serde(default = "default_idle_disconnect")]
pub idle_disconnect: bool,
}
fn default_username() -> String {
"admin".into()
}
fn default_idle_disconnect() -> bool {
true
}
fn default_topic_prefix() -> String {
"bairelay".into()
}
fn default_log_level() -> String {
"info".into()
}
pub fn build_base_config(opts: &HassioOptions, mqtt: &MqttServiceFlags) -> Config {
let mut cfg = Config::default();
if let Some(host) = &mqtt.host {
let credentials = match (&mqtt.username, &mqtt.password) {
(Some(u), Some(p)) => Some((u.clone(), p.clone())),
_ => None,
};
cfg.mqtt = Some(MqttServerConfig {
broker_addr: host.clone(),
port: mqtt.port.unwrap_or(1883),
credentials,
ca: None,
client_auth: None,
topic_prefix: opts.topic_prefix.clone(),
discovery: Some(crate::config::MqttDiscoveryConfig {
topic: "homeassistant".into(),
features: bairelay_mqtt::discovery::Feature::ALL
.iter()
.copied()
.collect(),
}),
});
}
cfg.cameras = opts
.cameras
.iter()
.map(|c| CameraConfig {
name: c.name.clone(),
address: c.address.clone().filter(|s| !s.is_empty()),
uid: c.uid.clone().filter(|s| !s.is_empty()),
username: c.username.clone(),
password: Some(c.password.clone()),
idle_disconnect: c.idle_disconnect,
..CameraConfig::default()
})
.collect();
cfg
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_minimal_options_json() {
let json = r#"{
"topic_prefix": "bairelay",
"log_level": "info",
"cameras": [
{"name": "Hallway", "address": "192.168.1.50", "password": "secret"}
]
}"#;
let opts: HassioOptions = serde_json::from_str(json).unwrap();
assert_eq!(opts.topic_prefix, "bairelay");
assert_eq!(opts.log_level, "info");
assert_eq!(opts.cameras.len(), 1);
assert_eq!(opts.cameras[0].name, "Hallway");
assert_eq!(opts.cameras[0].address.as_deref(), Some("192.168.1.50"));
assert!(opts.cameras[0].uid.is_none());
assert_eq!(opts.cameras[0].username, "admin");
assert_eq!(opts.cameras[0].password, "secret");
assert!(opts.cameras[0].idle_disconnect, "default true");
}
#[test]
fn parses_options_json_with_uid_and_custom_username() {
let json = r#"{
"topic_prefix": "bairelay",
"log_level": "info",
"cameras": [
{"name": "Garage", "uid": "9527000ABCDEF123", "username": "operator", "password": "pw"}
]
}"#;
let opts: HassioOptions = serde_json::from_str(json).unwrap();
assert!(opts.cameras[0].address.is_none());
assert_eq!(opts.cameras[0].uid.as_deref(), Some("9527000ABCDEF123"));
assert_eq!(opts.cameras[0].username, "operator");
}
#[test]
fn mqtt_service_flags_defaults_to_unset() {
let flags = MqttServiceFlags::default();
assert!(flags.host.is_none());
assert!(flags.port.is_none());
assert!(flags.username.is_none());
assert!(flags.password.is_none());
assert!(!flags.ssl);
}
#[test]
fn builds_base_config_with_cameras_and_mqtt() {
let opts = HassioOptions {
topic_prefix: "bairelay".into(),
log_level: "info".into(),
cameras: vec![HassioCamera {
name: "Hallway".into(),
address: Some("192.168.1.50".into()),
uid: None,
username: "admin".into(),
password: "secret".into(),
idle_disconnect: true,
}],
};
let mqtt = MqttServiceFlags {
host: Some("core-mosquitto".into()),
port: Some(1883),
username: Some("addons".into()),
password: Some("pw".into()),
ssl: false,
};
let cfg = build_base_config(&opts, &mqtt);
assert_eq!(cfg.cameras.len(), 1);
assert_eq!(cfg.cameras[0].name, "Hallway");
let m = cfg.mqtt.as_ref().expect("mqtt set");
assert_eq!(m.broker_addr, "core-mosquitto");
assert_eq!(m.port, 1883);
assert_eq!(m.topic_prefix, "bairelay");
assert_eq!(cfg.cameras[0].address.as_deref(), Some("192.168.1.50"));
assert!(cfg.cameras[0].uid.is_none());
assert_eq!(cfg.cameras[0].username, "admin");
assert_eq!(cfg.cameras[0].password.as_deref(), Some("secret"));
let creds = m.credentials.as_ref().expect("creds set");
assert_eq!(creds.0, "addons");
assert_eq!(creds.1, "pw");
crate::config::validate_config(&cfg).expect("base config validates");
}
#[test]
fn uid_camera_routes_to_uid_field_not_address() {
let opts = HassioOptions {
topic_prefix: "bairelay".into(),
log_level: "info".into(),
cameras: vec![HassioCamera {
name: "Garage".into(),
address: None,
uid: Some("9527000ABCDEF123".into()),
username: "admin".into(),
password: "p".into(),
idle_disconnect: true,
}],
};
let cfg = build_base_config(&opts, &MqttServiceFlags::default());
assert!(cfg.cameras[0].address.is_none());
assert_eq!(cfg.cameras[0].uid.as_deref(), Some("9527000ABCDEF123"));
crate::config::validate_config(&cfg).expect("uid-only camera validates");
}
#[test]
fn empty_address_and_uid_collapse_to_none() {
let opts = HassioOptions {
topic_prefix: "bairelay".into(),
log_level: "info".into(),
cameras: vec![HassioCamera {
name: "Cam".into(),
address: Some("".into()),
uid: Some("ABC".into()),
username: "admin".into(),
password: "p".into(),
idle_disconnect: true,
}],
};
let cfg = build_base_config(&opts, &MqttServiceFlags::default());
assert!(cfg.cameras[0].address.is_none());
assert_eq!(cfg.cameras[0].uid.as_deref(), Some("ABC"));
}
#[test]
fn no_cameras_yields_empty_camera_list() {
let opts = HassioOptions {
topic_prefix: "bairelay".into(),
log_level: "info".into(),
cameras: vec![],
};
let cfg = build_base_config(&opts, &MqttServiceFlags::default());
assert!(cfg.cameras.is_empty());
}
#[test]
fn no_mqtt_injection_leaves_mqtt_unset() {
let opts = HassioOptions {
topic_prefix: "bairelay".into(),
log_level: "info".into(),
cameras: vec![],
};
let cfg = build_base_config(&opts, &MqttServiceFlags::default());
assert!(
cfg.mqtt.is_none(),
"mqtt must stay None for overlay to fill in"
);
}
#[test]
fn ssl_flag_does_not_break_mqtt_propagation() {
let opts = HassioOptions {
topic_prefix: "bairelay".into(),
log_level: "info".into(),
cameras: vec![],
};
let mqtt = MqttServiceFlags {
host: Some("broker.example".into()),
port: Some(8883),
username: None,
password: None,
ssl: true,
};
let cfg = build_base_config(&opts, &mqtt);
let m = cfg.mqtt.as_ref().expect("mqtt set");
assert_eq!(m.broker_addr, "broker.example");
assert_eq!(m.port, 8883);
assert!(m.ca.is_none(), "TLS deferred to overlay");
}
#[test]
fn half_set_mqtt_credentials_yields_none() {
let opts = HassioOptions {
topic_prefix: "bairelay".into(),
log_level: "info".into(),
cameras: vec![],
};
let user_only = MqttServiceFlags {
host: Some("b.example".into()),
port: Some(1883),
username: Some("u".into()),
password: None,
ssl: false,
};
let cfg = build_base_config(&opts, &user_only);
assert!(
cfg.mqtt.as_ref().unwrap().credentials.is_none(),
"user-only injection collapses to None"
);
let pass_only = MqttServiceFlags {
host: Some("b.example".into()),
port: Some(1883),
username: None,
password: Some("p".into()),
ssl: false,
};
let cfg = build_base_config(&opts, &pass_only);
assert!(
cfg.mqtt.as_ref().unwrap().credentials.is_none(),
"password-only injection collapses to None"
);
}
#[test]
fn mqtt_port_zero_is_sentinel_for_unset() {
let opts = HassioOptions {
topic_prefix: "bairelay".into(),
log_level: "info".into(),
cameras: vec![],
};
let mqtt = MqttServiceFlags {
host: None,
port: Some(0),
username: None,
password: None,
ssl: false,
};
let cfg = build_base_config(&opts, &mqtt);
assert!(cfg.mqtt.is_none(), "host=None overrides any port value");
}
#[test]
fn mqtt_port_unset_defaults_to_1883() {
let opts = HassioOptions {
topic_prefix: "bairelay".into(),
log_level: "info".into(),
cameras: vec![],
};
let mqtt = MqttServiceFlags {
host: Some("b.example".into()),
port: None,
username: None,
password: None,
ssl: false,
};
let cfg = build_base_config(&opts, &mqtt);
assert_eq!(cfg.mqtt.as_ref().unwrap().port, 1883);
}
}