Files
smart-home-in-rust-with-hom…/home-assistant/src/notify/service/mobile_app/mod.rs
2026-07-12 19:38:27 -04:00

262 lines
7.5 KiB
Rust

use std::str::FromStr;
use pyo3::{
types::{PyDict, PyDictMethods},
Bound, IntoPyObject, PyErr, Python,
};
use python_utils::{FromPyFromStr, IntoPyObjectViaDisplay, ToStrToPy};
use snafu::Snafu;
use strum::EnumString;
use url::Url;
pub mod command;
pub mod standard;
pub mod text_to_speech;
pub use command::*;
pub use standard::StandardNotification;
pub use text_to_speech::TextToSpeech;
#[derive(Debug, Clone, derive_more::Display)]
pub struct NonSpecialMessage(String);
#[derive(Debug, Clone, EnumString, strum::Display)]
#[strum(serialize_all = "snake_case")]
pub enum SpecialMessage {
ClearBadge,
ClearNotification,
CommandActivity,
CommandAppLock,
CommandAutoScreenBrightness,
CommandBluetooth,
CommandBleTransmitter,
CommandBeaconMonitor,
CommandBroadcastIntent,
CommandDnd,
CommandFlashlight,
CommandHighAccuracyMode,
CommandLaunchApp,
CommandMedia,
CommandRingerMode,
CommandScreenBrightnessLevel,
CommandScreenOffTimeout,
CommandScreenOn,
CommandStopTts,
CommandPersistentConnection,
CommandUpdateSensors,
CommandVolumeLevel,
CommandWebivew,
RemoveChannel,
RequestLocationUpdate,
// #[strum(serialize = "TTS")] TODO: WIP: TESTING
Tts,
UpdateComplications,
UpdateWidgets,
}
/// wasn't supposed to get a specially-behaving message here, but got {got}
#[derive(Debug, Clone, Snafu)]
pub struct WasSpecialMessage {
pub got: SpecialMessage,
}
impl FromStr for NonSpecialMessage {
type Err = WasSpecialMessage;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match SpecialMessage::from_str(s) {
Ok(special_message) => Err(WasSpecialMessage {
got: special_message,
}),
Err(_e) => Ok(NonSpecialMessage(s.into())),
}
}
}
impl TryFrom<String> for NonSpecialMessage {
type Error = WasSpecialMessage;
fn try_from(s: String) -> Result<Self, Self::Error> {
match SpecialMessage::from_str(&s) {
Ok(special_message) => Err(WasSpecialMessage {
got: special_message,
}),
Err(_e) => Ok(NonSpecialMessage(s)),
}
}
}
/// How much of a notification is visible on the lock screen
#[derive(Debug, Clone, Default, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
#[strum(serialize_all = "snake_case")]
pub enum Visibility {
/// always show all notification content
Public,
/// visibility depends on your setting in the system Settings app > Notifications;
/// if the option to show sensitive notifications when locked is enabled all notification content will be shown,
/// otherwise only basic information such as the icon and app name are visible
#[default]
Private,
/// always hide notification from lock screen
Secret,
}
#[derive(Debug, Clone, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
pub enum Behavior {
/// prompt for text to return with the event
#[strum(serialize = "textInput")]
TextInput,
}
#[derive(Debug, Clone, Default, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
#[strum(serialize_all = "camelCase")]
pub enum ActivationMode {
/// launch the app when tapped
Foreground,
/// just fires the event
#[default]
Background,
}
// TODO: better typed versions like `CallNumber` or `OpenWebpage` where Action: From<CallNumber> and Action: From<OpenWebPage>
#[derive(Debug, Clone, IntoPyObject, typed_builder::TypedBuilder)]
#[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))]
pub struct Action {
// TODO: proper type
// TODO: I wish I could call this identifier or something instead
#[builder(!default, setter(!strip_option))]
pub action: String,
#[builder(!default, setter(!strip_option))]
pub title: String,
pub uri: Option<IntoPyObjectViaDisplay<Url>>,
pub behavior: Option<Behavior>,
// TODO: make this written as activationMode
/// (iOS only) decide whether to open the app (to the foreground) when tapped
/// or merely fire an event (in the background)
pub activation_mode: Option<ActivationMode>,
// TODO: make this written as authenticationRequired
/// (iOS only) require entering a passcode to use the action
pub authentication_required: Option<bool>,
/// (iOS only) color the action's title red, indicating a destructive action
pub destructive: Option<bool>,
// TODO: make this written as textInputButtonTitle
/// (iOS only) Title to use for text input for actions that prompt
pub text_input_button_title: Option<String>,
// TODO: make this written as textInputPlaceholder
/// (iOS only) Placeholder to use for text input for actions that prompt
pub text_input_placeholder: Option<String>,
// TODO: proper type
/// (iOS only) icon to use for the notification
pub icon: Option<String>,
}
#[derive(Debug, Clone, Default, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
#[strum(serialize_all = "snake_case", suffix = "_stream")]
pub enum MediaStream {
Alarm,
Call,
Dtmf,
#[default]
Music,
Notification,
Ring,
System,
}
#[derive(Debug, Default, Clone, typed_builder::TypedBuilder)]
#[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))]
pub struct NotifyMobileAppServiceDataData {
actions: Option<Vec<Action>>,
command: Option<String>,
media_stream: Option<MediaStream>,
tts_text: Option<String>,
visibility: Option<Visibility>,
}
// TODO: derive macro that does this for me
impl<'py> IntoPyObject<'py> for NotifyMobileAppServiceDataData {
type Target = PyDict;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let NotifyMobileAppServiceDataData {
actions,
command,
media_stream,
tts_text,
visibility,
} = self;
let dict = PyDict::new(py);
if let Some(actions) = actions {
dict.set_item("actions", actions)?;
}
if let Some(command) = command {
dict.set_item("command", command)?;
}
if let Some(media_stream) = media_stream {
dict.set_item("media_stream", media_stream)?;
}
if let Some(tts_text) = tts_text {
dict.set_item("tts_text", tts_text)?;
}
if let Some(visibility) = visibility {
dict.set_item("visibility", visibility)?;
}
Ok(dict)
}
}
#[derive(Debug, Clone, typed_builder::TypedBuilder)]
#[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))]
pub struct NotifyMobileAppServiceData {
#[builder(!default, setter(!strip_option))]
message: String,
title: Option<String>,
#[builder(setter(!strip_option))]
data: NotifyMobileAppServiceDataData,
}
// TODO: derive macro that does this for me
impl<'py> IntoPyObject<'py> for NotifyMobileAppServiceData {
type Target = PyDict;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let NotifyMobileAppServiceData {
message,
title,
data,
} = self;
let dict = PyDict::new(py);
dict.set_item("message", message)?;
if let Some(title) = title {
dict.set_item("title", title)?;
}
dict.set_item("data", data)?;
Ok(dict)
}
}