From 383c4825fdf612b009463563a1e67d428388a7c2 Mon Sep 17 00:00:00 2001 From: medusalix Date: Thu, 25 Sep 2025 23:54:43 +0200 Subject: [PATCH 1/2] Add `command_topic` and make `state_topic` optional --- src/homeassistant/light.rs | 6 +----- src/homeassistant/mod.rs | 17 ++++++++++++++--- src/homeassistant/ser.rs | 21 +++++++++++++++++++-- src/lib.rs | 2 ++ 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/homeassistant/light.rs b/src/homeassistant/light.rs index 47cd4f1..1b85d11 100644 --- a/src/homeassistant/light.rs +++ b/src/homeassistant/light.rs @@ -324,9 +324,6 @@ impl Serialize for LightState<'_> { /// A light entity pub struct Light<'a, const C: usize, const E: usize> { - /// A command topic that Home Assistant can use to control the light. - /// It will be sent a [`LightState`] payload. - pub command_topic: Option>, /// The color modes supported by the light. pub supported_color_modes: [SupportedColorMode; C], /// Any effects that can be used. @@ -338,7 +335,7 @@ impl Serialize for Light<'_, C, E> { where S: Serializer, { - let mut len = 3; + let mut len = 2; if C > 0 { len += 1; @@ -350,7 +347,6 @@ impl Serialize for Light<'_, C, E> { let mut serializer = serializer.serialize_struct("Light", len)?; - serializer.serialize_field("cmd_t", &self.command_topic)?; serializer.serialize_field("schema", "json")?; if C > 0 { diff --git a/src/homeassistant/mod.rs b/src/homeassistant/mod.rs index 4c00a88..523fcdb 100644 --- a/src/homeassistant/mod.rs +++ b/src/homeassistant/mod.rs @@ -20,7 +20,8 @@ //! unique_id: Some("motion"), //! name: "Motion", //! availability: AvailabilityTopics::All([DEVICE_AVAILABILITY_TOPIC]), -//! state_topic: MOTION_STATE_TOPIC, +//! state_topic: Some(MOTION_STATE_TOPIC), +//! command_topic: None, //! component: BinarySensor { //! device_class: Some(BinarySensorClass::Motion), //! }, @@ -206,7 +207,9 @@ pub struct Entity<'a, const A: usize, C: Component> { /// determine this entity's availability. pub availability: AvailabilityTopics<'a, A>, /// The state topic that this entity's state is published to. - pub state_topic: Topic<&'a str>, + pub state_topic: Option>, + /// The command topic that this entity receives commands from. + pub command_topic: Option>, /// The specific entity. pub component: C, } @@ -233,8 +236,16 @@ impl Entity<'_, A, C> { } /// Publishes this entity's state to the broker. + /// + /// # Errors + /// + /// - [`Error::Invalid`] if the entity doesn't have a state topic. pub async fn publish_state(&self, state: C::State) -> Result<(), Error> { - self.component.publish_state(&self.state_topic, state).await + if let Some(topic) = self.state_topic { + self.component.publish_state(&topic, state).await + } else { + Err(Error::Invalid) + } } } diff --git a/src/homeassistant/ser.rs b/src/homeassistant/ser.rs index a25d883..80e99d9 100644 --- a/src/homeassistant/ser.rs +++ b/src/homeassistant/ser.rs @@ -88,7 +88,13 @@ impl Serializer for DiscoverySerial name: &'static str, mut len: usize, ) -> Result { - len += 6; + len += 5; + if self.discovery.state_topic.is_some() { + len += 1; + } + if self.discovery.command_topic.is_some() { + len += 1; + } if self.discovery.unique_id.is_some() { len += 1; } @@ -104,7 +110,18 @@ impl Serializer for DiscoverySerial serializer.serialize_field("obj_id", self.discovery.object_id)?; serializer.serialize_field("name", self.discovery.name)?; - serializer.serialize_field("stat_t", &self.discovery.state_topic)?; + + if let Some(t) = self.discovery.state_topic { + serializer.serialize_field("stat_t", &t)?; + } else { + serializer.skip_field("stat_t")?; + } + + if let Some(t) = self.discovery.command_topic { + serializer.serialize_field("cmd_t", &t)?; + } else { + serializer.skip_field("cmd_t")?; + } match &self.discovery.availability { AvailabilityTopics::None => { diff --git a/src/lib.rs b/src/lib.rs index 706a20a..d8bcd96 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,6 +68,8 @@ pub enum Error { TooLarge, /// A packet or payload could not be decoded or encoded. PacketError, + /// An invalid or unsupported operation was attempted. + Invalid, } #[allow(clippy::large_enum_variant)] From 08420f2f0942e8886ad43c3da124b248637a4a09 Mon Sep 17 00:00:00 2001 From: medusalix Date: Thu, 25 Sep 2025 23:54:56 +0200 Subject: [PATCH 2/2] Add `Button` component --- src/homeassistant/button.rs | 40 +++++++++++++++++++++++++++++++++++++ src/homeassistant/mod.rs | 1 + 2 files changed, 41 insertions(+) create mode 100644 src/homeassistant/button.rs diff --git a/src/homeassistant/button.rs b/src/homeassistant/button.rs new file mode 100644 index 0000000..b19a66f --- /dev/null +++ b/src/homeassistant/button.rs @@ -0,0 +1,40 @@ +//! Tools for publishing a [Home Assistant button](https://www.home-assistant.io/integrations/button.mqtt/). +use core::ops::Deref; + +use serde::Serialize; + +use crate::{homeassistant::Component, Error, Topic}; + +/// The type of button. +#[derive(Serialize)] +#[serde(rename_all = "snake_case")] +#[allow(missing_docs)] +pub enum ButtonClass { + Identify, + Restart, + Update, +} + +/// A button that can be pressed. +#[derive(Serialize)] +pub struct Button { + /// The type of button. + pub device_class: Option, +} + +impl Component for Button { + type State = (); + + fn platform() -> &'static str { + "button" + } + + async fn publish_state>( + &self, + _topic: &Topic, + _state: Self::State, + ) -> Result<(), Error> { + // Buttons don't have a state + Err(Error::Invalid) + } +} diff --git a/src/homeassistant/mod.rs b/src/homeassistant/mod.rs index 523fcdb..8d98205 100644 --- a/src/homeassistant/mod.rs +++ b/src/homeassistant/mod.rs @@ -50,6 +50,7 @@ use crate::{ }; pub mod binary_sensor; +pub mod button; pub mod light; pub mod sensor; mod ser;