Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ Status Messages:
pir status
- `/status/motion` Contains the motion detection alarm status. `on` for motion
and `off` for still, only published when `enable_moton` is true in the config
- `/status/doorbell` Contains the doorbell button status. `pressed` when doorbell
is pressed, `idle` otherwise. Only published when `enable_doorbell` is true
in the config (default: true)
- `/status/ptz/preset` Sent in reply to a `/query/ptz/preset` an XML encoded
version of the PTZ presets
- `/status/preview` a base64 encoded camera image updated every 2s. Not
Expand Down Expand Up @@ -325,6 +328,8 @@ enable_motion = false # motion detection
# (limited battery drain since it
# is a passive listening connection)
#
enable_doorbell = false # doorbell button events
#
enable_light = false # flood lights only available on some camera
# (limited battery drain since it
# is a passive listening connection)
Expand Down Expand Up @@ -368,6 +373,7 @@ Available features are:
- `ir`: This adds a selection switch to chage the IR light on/off/auto to home
assistant
- `motion`: This adds a motion detection binary sensor to home assistant
- `doorbell`: This adds a doorbell binary sensor to home assistant
- `reboot`: This adds a reboot button to home assistant
- `pt`: This adds a selection of buttons to control the pan and tilt of the
camera
Expand Down
40 changes: 26 additions & 14 deletions crates/core/src/bc_protocol/motion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub enum MotionStatus {
Start(Instant),
/// Sent when motion stops
Stop(Instant),
/// Sent when doorbell is pressed
Doorbell(Instant),
/// Sent when an Alarm about something other than motion was received
NoChange(Instant),
}
Expand All @@ -37,6 +39,7 @@ impl MotionData {
MotionStatus::Start(_) => Some(true),
MotionStatus::Stop(_) => Some(false),
MotionStatus::NoChange(_) => None,
MotionStatus::Doorbell(_) => None,
})
}

Expand All @@ -50,6 +53,7 @@ impl MotionData {
MotionStatus::Start(_) => Some(true),
MotionStatus::Stop(time) => Some((Instant::now() - *time) < duration),
MotionStatus::NoChange(_) => None,
MotionStatus::Doorbell(_) => None,
})
}

Expand Down Expand Up @@ -236,25 +240,33 @@ impl BcCamera {
..
}) = motion_msg.body
{
let mut result = MotionStatus::NoChange(Instant::now());
let mut events = Vec::new();
for alarm_event in &alarm_event_list.alarm_events {
if alarm_event.channel_id == channel_id {
if alarm_event.status != "none"
|| alarm_event
.ai_type
.as_ref()
.map(|ai_type| ai_type != "none")
.unwrap_or(false)
{
result = MotionStatus::Start(Instant::now());
break;
} else {
result = MotionStatus::Stop(Instant::now());
break;
let statuses: Vec<&str> = alarm_event.status.split(',').collect();
for status in statuses {
let event = match status {
"visitor" => MotionStatus::Doorbell(Instant::now()),
"MD" | "PIR" => MotionStatus::Start(Instant::now()),
"none" => MotionStatus::Stop(Instant::now()),
_ => MotionStatus::Start(Instant::now()),
};
events.push(event);
}
if alarm_event.ai_type.as_ref().map(|t| t != "none").unwrap_or(false) {
if !events.iter().any(|e| matches!(e, MotionStatus::Start(_))) {
events.push(MotionStatus::Start(Instant::now()));
}
}
break;
}
}
for event in events {
if tx.send(Ok(event)).await.is_err() {
break;
}
}
Ok(result)
Ok(MotionStatus::NoChange(Instant::now()))
} else {
Ok(MotionStatus::NoChange(Instant::now()))
}
Expand Down
7 changes: 7 additions & 0 deletions sample_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ address = "192.168.1.187:9000"
# mqtt.discovery.topic = "homeassistant" # Uncomment to enable
# If using discovery, _ characters are replaced with spaces in the name and title case is applied
# mqtt.discovery.features = ["floodlight"] # Uncomment if this camera has a spotlight/floodlight
# MQTT specific camera settings
# mqtt.enable_motion = true
# mqtt.enable_doorbell = true
# mqtt.enable_light = true
# mqtt.enable_battery = true
# mqtt.enable_preview = true
# mqtt.enable_floodlight = true

# If you use a battery camera: **Instead** of an `address` supply the uid
# as follows
Expand Down
8 changes: 8 additions & 0 deletions src/common/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,14 @@ impl NeoInstance {
Ok(instance_rx.await?)
}

pub(crate) async fn doorbell(&self) -> Result<WatchReceiver<MdState>> {
let (instance_tx, instance_rx) = oneshot();
self.camera_control
.send(NeoCamCommand::Doorbell(instance_tx))
.await?;
Ok(instance_rx.await?)
}

pub(crate) async fn config(&self) -> Result<WatchReceiver<CameraConfig>> {
let (instance_tx, instance_rx) = oneshot();
self.camera_control
Expand Down
4 changes: 2 additions & 2 deletions src/common/instance/gst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ impl NeoInstance {
log::info!("{name}::{stream:?}: Starting with Motion");
counter.create_activated().await?
}
MdState::Stop(_) | MdState::Unknown => {
MdState::Stop(_) | MdState::Doorbell(_) | MdState::Unknown => {
log::info!("{name}::{stream:?}: Waiting with Motion");
counter.create_deactivated().await?
}
Expand All @@ -64,7 +64,7 @@ impl NeoInstance {
log::info!("{thread_name}::{stream:?}: Motion Started");
md_permit.activate().await?;
}
MdState::Stop(_) => {
MdState::Stop(_) | MdState::Doorbell(_) => {
log::info!("{thread_name}::{stream:?}: Motion Stopped");
md_permit.deactivate().await?;
}
Expand Down
6 changes: 6 additions & 0 deletions src/common/mdthread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use neolink_core::bc_protocol::MotionStatus;
pub(crate) enum MdState {
Start(Instant),
Stop(Instant),
Doorbell(Instant),
Unknown,
}

Expand Down Expand Up @@ -87,6 +88,11 @@ impl NeoCamMdThread {
MdState::Stop(at.into())
);
}
MotionStatus::Doorbell(at) => {
watcher.send_replace(
MdState::Doorbell(at.into())
);
}
MotionStatus::NoChange(_) => {},
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/common/neocam.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub(crate) enum NeoCamCommand {
HangUp,
Instance(OneshotSender<Result<NeoInstance>>),
Motion(OneshotSender<WatchReceiver<MdState>>),
Doorbell(OneshotSender<WatchReceiver<MdState>>),
Config(OneshotSender<WatchReceiver<CameraConfig>>),
Disconnect(OneshotSender<()>),
Connect(OneshotSender<()>),
Expand Down Expand Up @@ -114,6 +115,13 @@ impl NeoCam {
}
).await?;
},
NeoCamCommand::Doorbell(sender) => {
md_request_tx.send(
MdRequest::Get {
sender,
}
).await?;
},
NeoCamCommand::Config(sender) => {
let _ = sender.send(thread_watch_config_rx.clone());
},
Expand Down
5 changes: 4 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ pub(crate) struct CameraConfig {
pub(crate) idle_disconnect: bool,
}

#[derive(Debug, Deserialize, Serialize, Validate, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Deserialize, Serialize, Clone, Validate, PartialEq, Eq, Hash)]
pub(crate) struct UserConfig {
#[validate(custom(function = "validate_username"))]
#[serde(alias = "username")]
Expand All @@ -235,6 +235,8 @@ pub(crate) struct MqttConfig {
#[serde(default = "default_true")]
pub(crate) enable_motion: bool,
#[serde(default = "default_true")]
pub(crate) enable_doorbell: bool,
#[serde(default = "default_true")]
pub(crate) enable_light: bool,
#[serde(default = "default_true")]
pub(crate) enable_battery: bool,
Expand Down Expand Up @@ -303,6 +305,7 @@ const fn default_false() -> bool {
fn default_mqtt() -> MqttConfig {
MqttConfig {
enable_motion: true,
enable_doorbell: true,
enable_light: true,
enable_battery: true,
battery_update: 2000,
Expand Down
32 changes: 32 additions & 0 deletions src/mqtt/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ pub(crate) enum Discoveries {
Battery,
#[serde(alias = "siren", alias = "alarm")]
Siren,
#[serde(alias = "doorbell", alias = "db", alias = "visitor")]
Doorbell,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -624,6 +626,36 @@ pub(crate) async fn enable_discovery(
)
})?;
}
Discoveries::Doorbell => {
let config_data = DiscoveryBinarySensor {
device: device.clone(),
availability: availability.clone(),
name: format!("{} Doorbell", friendly_name.as_str()),
unique_id: format!("neolink_{}_doorbell", cam_config.name),
icon: Some("mdi:doorbell".to_string()),
state_topic: format!("neolink/{}/status/doorbell", cam_config.name),
payload_off: "idle".to_string(),
payload_on: "pressed".to_string(),
};

mqtt.send_message_with_root_topic(
&format!(
"{}/binary_sensor/{}",
discovery_config.topic, &config_data.unique_id
),
"config",
&serde_json::to_string(&config_data)
.with_context(|| "Could not serialise discovery doorbell config into json")?,
true,
)
.await
.with_context(|| {
format!(
"Failed to publish doorbell auto-discover data over MQTT for {}",
cam_config.name
)
})?;
}
}
}

Expand Down
30 changes: 30 additions & 0 deletions src/mqtt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,13 @@ async fn listen_on_camera(camera: NeoInstance, mqtt_instance: MqttInstance) -> R
.with_context(|| format!("Failed to publish push notification unknown for {}", camera_name))?;
let _drop_message2 = mqtt_instance.last_will("status/motion", "unknown").await?;

// Publish initial doorbell state
mqtt_instance
.send_message("status/doorbell", "unknown", true)
.await
.with_context(|| format!("Failed to publish doorbell unknown for {}", camera_name))?;
let _drop_message3 = mqtt_instance.last_will("status/doorbell", "unknown").await?;

if let Some(discovery_config) = config.discovery.as_ref() {
enable_discovery(discovery_config, &mqtt_instance, &camera).await?;
}
Expand All @@ -331,6 +338,9 @@ async fn listen_on_camera(camera: NeoInstance, mqtt_instance: MqttInstance) -> R
let camera_motion = camera.clone();
let mqtt_motion = mqtt_instance.resubscribe().await?;

let camera_doorbell = camera.clone();
let mqtt_doorbell = mqtt_instance.resubscribe().await?;

#[cfg(feature = "pushnoti")]
let camera_pn = camera.clone();
#[cfg(feature = "pushnoti")]
Expand Down Expand Up @@ -476,6 +486,26 @@ async fn listen_on_camera(camera: NeoInstance, mqtt_instance: MqttInstance) -> R
}?;
}
}, if config.enable_motion => v,
// Handle the doorbell messages
v = async {
let mut db = camera_doorbell.doorbell().await?;
loop {
db.wait_for(|state| matches!(state, MdState::Doorbell(_))).await.with_context(|| {
format!("{}: Doorbell Watch Dropped", camera_name)
})?;
mqtt_doorbell.send_message("status/doorbell", "pressed", true).await.with_context(|| {
format!("{}: Failed to publish doorbell pressed", camera_name)
})?;
// Wait for non-doorbell state to reset
db.wait_for(|state| !matches!(state, MdState::Doorbell(_))).await.with_context(|| {
format!("{}: Doorbell Reset Watch Dropped", camera_name)
})?;
mqtt_doorbell.send_message("status/doorbell", "idle", true).await.with_context(|| {
format!("{}: Failed to publish doorbell idle", camera_name)
})?;
}
AnyResult::Ok(())
}, if config.enable_doorbell => v,
// Handle the SNAP (image preview)
v = async {
let mut wait = IntervalStream::new({
Expand Down