aboutsummaryrefslogtreecommitdiff
path: root/src/lib.rs
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-12-12 12:05:58 +0000
committerdiogo464 <[email protected]>2025-12-12 12:05:58 +0000
commite041614b0607a0950be1446f595d85c93b501418 (patch)
tree54a69e34ad53bf7c8a844dba0f6585d52f31d2f4 /src/lib.rs
parentfeb5ae59654bb11365b939ee871adb9760229355 (diff)
added device tracker entity
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs128
1 files changed, 115 insertions, 13 deletions
diff --git a/src/lib.rs b/src/lib.rs
index 234d0e9..f28d2bd 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -52,6 +52,9 @@ pub use entity_button::*;
52mod entity_category; 52mod entity_category;
53pub use entity_category::*; 53pub use entity_category::*;
54 54
55mod entity_device_tracker;
56pub use entity_device_tracker::*;
57
55mod entity_number; 58mod entity_number;
56pub use entity_number::*; 59pub use entity_number::*;
57 60
@@ -115,12 +118,18 @@ struct EntityDiscovery<'a> {
115 command_topic: Option<&'a str>, 118 command_topic: Option<&'a str>,
116 119
117 #[serde(skip_serializing_if = "Option::is_none")] 120 #[serde(skip_serializing_if = "Option::is_none")]
121 json_attributes_topic: Option<&'a str>,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
118 unit_of_measurement: Option<&'a str>, 124 unit_of_measurement: Option<&'a str>,
119 125
120 #[serde(skip_serializing_if = "Option::is_none")] 126 #[serde(skip_serializing_if = "Option::is_none")]
121 schema: Option<&'a str>, 127 schema: Option<&'a str>,
122 128
123 #[serde(skip_serializing_if = "Option::is_none")] 129 #[serde(skip_serializing_if = "Option::is_none")]
130 platform: Option<&'a str>,
131
132 #[serde(skip_serializing_if = "Option::is_none")]
124 state_class: Option<&'a str>, 133 state_class: Option<&'a str>,
125 134
126 #[serde(skip_serializing_if = "Option::is_none")] 135 #[serde(skip_serializing_if = "Option::is_none")]
@@ -198,6 +207,21 @@ impl<'a> core::fmt::Display for CommandTopicDisplay<'a> {
198 } 207 }
199} 208}
200 209
210struct AttributesTopicDisplay<'a> {
211 device_id: &'a str,
212 entity_id: &'a str,
213}
214
215impl<'a> core::fmt::Display for AttributesTopicDisplay<'a> {
216 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
217 write!(
218 f,
219 "embassy-ha/{}/{}/attributes",
220 self.device_id, self.entity_id
221 )
222 }
223}
224
201struct DeviceAvailabilityTopic<'a> { 225struct DeviceAvailabilityTopic<'a> {
202 device_id: &'a str, 226 device_id: &'a str,
203} 227}
@@ -227,6 +251,7 @@ pub struct DeviceResources {
227 discovery_topic_buffer: String<128>, 251 discovery_topic_buffer: String<128>,
228 state_topic_buffer: String<128>, 252 state_topic_buffer: String<128>,
229 command_topic_buffer: String<128>, 253 command_topic_buffer: String<128>,
254 attributes_topic_buffer: String<128>,
230} 255}
231 256
232impl DeviceResources { 257impl DeviceResources {
@@ -247,6 +272,7 @@ impl Default for DeviceResources {
247 discovery_topic_buffer: Default::default(), 272 discovery_topic_buffer: Default::default(),
248 state_topic_buffer: Default::default(), 273 state_topic_buffer: Default::default(),
249 command_topic_buffer: Default::default(), 274 command_topic_buffer: Default::default(),
275 attributes_topic_buffer: Default::default(),
250 } 276 }
251 } 277 }
252} 278}
@@ -323,6 +349,19 @@ pub(crate) struct NumberStorage {
323 pub publish_on_command: bool, 349 pub publish_on_command: bool,
324} 350}
325 351
352#[derive(Debug, Serialize)]
353pub(crate) struct DeviceTrackerState {
354 pub latitude: f32,
355 pub longitude: f32,
356 #[serde(skip_serializing_if = "Option::is_none")]
357 pub gps_accuracy: Option<f32>,
358}
359
360#[derive(Debug, Default)]
361pub(crate) struct DeviceTrackerStorage {
362 pub state: Option<DeviceTrackerState>,
363}
364
326#[derive(Debug)] 365#[derive(Debug)]
327pub(crate) enum EntityStorage { 366pub(crate) enum EntityStorage {
328 Button(ButtonStorage), 367 Button(ButtonStorage),
@@ -330,6 +369,7 @@ pub(crate) enum EntityStorage {
330 BinarySensor(BinarySensorStorage), 369 BinarySensor(BinarySensorStorage),
331 NumericSensor(NumericSensorStorage), 370 NumericSensor(NumericSensorStorage),
332 Number(NumberStorage), 371 Number(NumberStorage),
372 DeviceTracker(DeviceTrackerStorage),
333} 373}
334 374
335impl EntityStorage { 375impl EntityStorage {
@@ -367,6 +407,13 @@ impl EntityStorage {
367 _ => panic!("expected storage type to be number"), 407 _ => panic!("expected storage type to be number"),
368 } 408 }
369 } 409 }
410
411 pub fn as_device_tracker_mut(&mut self) -> &mut DeviceTrackerStorage {
412 match self {
413 EntityStorage::DeviceTracker(storage) => storage,
414 _ => panic!("expected storage type to be device tracker"),
415 }
416 }
370} 417}
371 418
372struct EntityData { 419struct EntityData {
@@ -440,6 +487,7 @@ pub struct Device<'a> {
440 discovery_topic_buffer: &'a mut StringView, 487 discovery_topic_buffer: &'a mut StringView,
441 state_topic_buffer: &'a mut StringView, 488 state_topic_buffer: &'a mut StringView,
442 command_topic_buffer: &'a mut StringView, 489 command_topic_buffer: &'a mut StringView,
490 attributes_topic_buffer: &'a mut StringView,
443} 491}
444 492
445pub fn new<'a>(resources: &'a mut DeviceResources, config: DeviceConfig) -> Device<'a> { 493pub fn new<'a>(resources: &'a mut DeviceResources, config: DeviceConfig) -> Device<'a> {
@@ -456,6 +504,7 @@ pub fn new<'a>(resources: &'a mut DeviceResources, config: DeviceConfig) -> Devi
456 discovery_topic_buffer: &mut resources.discovery_topic_buffer, 504 discovery_topic_buffer: &mut resources.discovery_topic_buffer,
457 state_topic_buffer: &mut resources.state_topic_buffer, 505 state_topic_buffer: &mut resources.state_topic_buffer,
458 command_topic_buffer: &mut resources.command_topic_buffer, 506 command_topic_buffer: &mut resources.command_topic_buffer,
507 attributes_topic_buffer: &mut resources.attributes_topic_buffer,
459 } 508 }
460} 509}
461 510
@@ -589,6 +638,25 @@ pub fn create_binary_sensor<'a>(
589 BinarySensor::new(entity) 638 BinarySensor::new(entity)
590} 639}
591 640
641pub fn create_device_tracker<'a>(
642 device: &Device<'a>,
643 id: &'static str,
644 config: DeviceTrackerConfig,
645) -> DeviceTracker<'a> {
646 let mut entity_config = EntityConfig {
647 id,
648 ..Default::default()
649 };
650 config.populate(&mut entity_config);
651
652 let entity = create_entity(
653 device,
654 entity_config,
655 EntityStorage::DeviceTracker(Default::default()),
656 );
657 DeviceTracker::new(entity)
658}
659
592pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Result<(), Error> { 660pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Result<(), Error> {
593 use core::fmt::Write; 661 use core::fmt::Write;
594 662
@@ -647,6 +715,7 @@ pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Re
647 device.discovery_topic_buffer.clear(); 715 device.discovery_topic_buffer.clear();
648 device.state_topic_buffer.clear(); 716 device.state_topic_buffer.clear();
649 device.command_topic_buffer.clear(); 717 device.command_topic_buffer.clear();
718 device.attributes_topic_buffer.clear();
650 719
651 // borrow the entity and fill out the buffers to be sent 720 // borrow the entity and fill out the buffers to be sent
652 // this should be done inside a block so that we do not hold the RefMut across an 721 // this should be done inside a block so that we do not hold the RefMut across an
@@ -672,6 +741,10 @@ pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Re
672 device_id: device.config.device_id, 741 device_id: device.config.device_id,
673 entity_id: entity_config.id, 742 entity_id: entity_config.id,
674 }; 743 };
744 let attributes_topic_display = AttributesTopicDisplay {
745 device_id: device.config.device_id,
746 entity_id: entity_config.id,
747 };
675 748
676 write!(device.discovery_topic_buffer, "{discovery_topic_display}") 749 write!(device.discovery_topic_buffer, "{discovery_topic_display}")
677 .expect("discovery topic buffer too small"); 750 .expect("discovery topic buffer too small");
@@ -679,6 +752,8 @@ pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Re
679 .expect("state topic buffer too small"); 752 .expect("state topic buffer too small");
680 write!(device.command_topic_buffer, "{command_topic_display}") 753 write!(device.command_topic_buffer, "{command_topic_display}")
681 .expect("command topic buffer too small"); 754 .expect("command topic buffer too small");
755 write!(device.attributes_topic_buffer, "{attributes_topic_display}")
756 .expect("attributes topic buffer too small");
682 757
683 let discovery = EntityDiscovery { 758 let discovery = EntityDiscovery {
684 id: entity_config.id, 759 id: entity_config.id,
@@ -686,8 +761,10 @@ pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Re
686 device_class: entity_config.device_class, 761 device_class: entity_config.device_class,
687 state_topic: Some(device.state_topic_buffer.as_str()), 762 state_topic: Some(device.state_topic_buffer.as_str()),
688 command_topic: Some(device.command_topic_buffer.as_str()), 763 command_topic: Some(device.command_topic_buffer.as_str()),
764 json_attributes_topic: Some(device.attributes_topic_buffer.as_str()),
689 unit_of_measurement: entity_config.measurement_unit, 765 unit_of_measurement: entity_config.measurement_unit,
690 schema: entity_config.schema, 766 schema: entity_config.schema,
767 platform: entity_config.platform,
691 state_class: entity_config.state_class, 768 state_class: entity_config.state_class,
692 icon: entity_config.icon, 769 icon: entity_config.icon,
693 entity_picture: entity_config.picture, 770 entity_picture: entity_config.picture,
@@ -790,7 +867,7 @@ pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Re
790 use core::fmt::Write; 867 use core::fmt::Write;
791 868
792 for entity in device.entities { 869 for entity in device.entities {
793 { 870 let publish_topic = {
794 let mut entity = entity.borrow_mut(); 871 let mut entity = entity.borrow_mut();
795 let entity = match entity.as_mut() { 872 let entity = match entity.as_mut() {
796 Some(entity) => entity, 873 Some(entity) => entity,
@@ -804,6 +881,7 @@ pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Re
804 entity.publish = false; 881 entity.publish = false;
805 device.publish_buffer.clear(); 882 device.publish_buffer.clear();
806 883
884 let mut publish_to_attributes = false;
807 match &entity.storage { 885 match &entity.storage {
808 EntityStorage::Switch(SwitchStorage { 886 EntityStorage::Switch(SwitchStorage {
809 state: Some(SwitchState { value, .. }), 887 state: Some(SwitchState { value, .. }),
@@ -828,6 +906,19 @@ pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Re
828 .. 906 ..
829 }) => write!(device.publish_buffer, "{}", value) 907 }) => write!(device.publish_buffer, "{}", value)
830 .expect("publish buffer too small for number state payload"), 908 .expect("publish buffer too small for number state payload"),
909 EntityStorage::DeviceTracker(DeviceTrackerStorage {
910 state: Some(tracker_state),
911 }) => {
912 publish_to_attributes = true;
913 device
914 .publish_buffer
915 .resize(device.publish_buffer.capacity(), 0)
916 .expect("resize to capacity should never fail");
917 let n =
918 serde_json_core::to_slice(&tracker_state, &mut device.publish_buffer)
919 .expect("publish buffer too small for tracker state payload");
920 device.publish_buffer.truncate(n);
921 }
831 _ => { 922 _ => {
832 crate::log::warn!( 923 crate::log::warn!(
833 "entity '{}' requested state publish but its storage does not support it", 924 "entity '{}' requested state publish but its storage does not support it",
@@ -837,19 +928,30 @@ pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Re
837 } 928 }
838 } 929 }
839 930
840 let state_topic_display = StateTopicDisplay { 931 if publish_to_attributes {
841 device_id: device.config.device_id, 932 let attributes_topic_display = AttributesTopicDisplay {
842 entity_id: entity.config.id, 933 device_id: device.config.device_id,
843 }; 934 entity_id: entity.config.id,
844 device.state_topic_buffer.clear(); 935 };
845 write!(device.state_topic_buffer, "{state_topic_display}") 936 device.attributes_topic_buffer.clear();
846 .expect("state topic buffer too small"); 937 write!(device.attributes_topic_buffer, "{attributes_topic_display}")
847 } 938 .expect("attributes topic buffer too small");
939 device.attributes_topic_buffer.as_str()
940 } else {
941 let state_topic_display = StateTopicDisplay {
942 device_id: device.config.device_id,
943 entity_id: entity.config.id,
944 };
945 device.state_topic_buffer.clear();
946 write!(device.state_topic_buffer, "{state_topic_display}")
947 .expect("state topic buffer too small");
948 device.state_topic_buffer.as_str()
949 }
950 };
848 951
849 let state_topic = device.state_topic_buffer.as_str();
850 match embassy_time::with_timeout( 952 match embassy_time::with_timeout(
851 MQTT_TIMEOUT, 953 MQTT_TIMEOUT,
852 client.publish(state_topic, device.publish_buffer), 954 client.publish(publish_topic, device.publish_buffer),
853 ) 955 )
854 .await 956 .await
855 { 957 {
@@ -857,13 +959,13 @@ pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Re
857 Ok(Err(err)) => { 959 Ok(Err(err)) => {
858 crate::log::error!( 960 crate::log::error!(
859 "mqtt state publish on topic '{}' failed with: {:?}", 961 "mqtt state publish on topic '{}' failed with: {:?}",
860 state_topic, 962 publish_topic,
861 crate::log::Debug2Format(&err) 963 crate::log::Debug2Format(&err)
862 ); 964 );
863 return Err(Error::new("mqtt publish failed")); 965 return Err(Error::new("mqtt publish failed"));
864 } 966 }
865 Err(_) => { 967 Err(_) => {
866 crate::log::error!("mqtt state publish on topic '{}' timed out", state_topic); 968 crate::log::error!("mqtt state publish on topic '{}' timed out", publish_topic);
867 return Err(Error::new("mqtt publish timed out")); 969 return Err(Error::new("mqtt publish timed out"));
868 } 970 }
869 } 971 }