diff options
| author | diogo464 <[email protected]> | 2025-12-05 12:17:01 +0000 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-12-05 12:17:01 +0000 |
| commit | 0c86da392af50c7588b087c3f72602e8368af65e (patch) | |
| tree | 894cd2f353298b83a56cde06eafd7b1e366aa6b3 /src/lib.rs | |
| parent | 1d2ee64d0ec917a2c2b66f8d58e1f37dd174a89d (diff) | |
reworked entity creation
Diffstat (limited to 'src/lib.rs')
| -rw-r--r-- | src/lib.rs | 344 |
1 files changed, 71 insertions, 273 deletions
| @@ -1,6 +1,6 @@ | |||
| 1 | #![no_std] | 1 | #![no_std] |
| 2 | 2 | ||
| 3 | use core::{cell::RefCell, str::FromStr, task::Waker}; | 3 | use core::{cell::RefCell, task::Waker}; |
| 4 | 4 | ||
| 5 | use defmt::Format; | 5 | use defmt::Format; |
| 6 | use embassy_sync::waitqueue::AtomicWaker; | 6 | use embassy_sync::waitqueue::AtomicWaker; |
| @@ -12,10 +12,35 @@ use heapless::{ | |||
| 12 | use serde::Serialize; | 12 | use serde::Serialize; |
| 13 | 13 | ||
| 14 | pub mod constants; | 14 | pub mod constants; |
| 15 | mod transport; | ||
| 16 | mod unit; | ||
| 17 | 15 | ||
| 16 | mod binary_state; | ||
| 17 | pub use binary_state::*; | ||
| 18 | |||
| 19 | mod entity; | ||
| 20 | pub use entity::*; | ||
| 21 | |||
| 22 | mod entity_binary_sensor; | ||
| 23 | pub use entity_binary_sensor::*; | ||
| 24 | |||
| 25 | mod entity_button; | ||
| 26 | pub use entity_button::*; | ||
| 27 | |||
| 28 | mod entity_category; | ||
| 29 | pub use entity_category::*; | ||
| 30 | |||
| 31 | mod entity_number; | ||
| 32 | pub use entity_number::*; | ||
| 33 | |||
| 34 | mod entity_sensor; | ||
| 35 | pub use entity_sensor::*; | ||
| 36 | |||
| 37 | mod entity_switch; | ||
| 38 | pub use entity_switch::*; | ||
| 39 | |||
| 40 | mod transport; | ||
| 18 | pub use transport::Transport; | 41 | pub use transport::Transport; |
| 42 | |||
| 43 | mod unit; | ||
| 19 | pub use unit::*; | 44 | pub use unit::*; |
| 20 | 45 | ||
| 21 | #[derive(Debug, Format, Clone, Copy, Serialize)] | 46 | #[derive(Debug, Format, Clone, Copy, Serialize)] |
| @@ -31,7 +56,8 @@ struct EntityDiscovery<'a> { | |||
| 31 | #[serde(rename = "unique_id")] | 56 | #[serde(rename = "unique_id")] |
| 32 | id: &'a str, | 57 | id: &'a str, |
| 33 | 58 | ||
| 34 | name: &'a str, | 59 | #[serde(skip_serializing_if = "Option::is_none")] |
| 60 | name: Option<&'a str>, | ||
| 35 | 61 | ||
| 36 | #[serde(skip_serializing_if = "Option::is_none")] | 62 | #[serde(skip_serializing_if = "Option::is_none")] |
| 37 | device_class: Option<&'a str>, | 63 | device_class: Option<&'a str>, |
| @@ -55,6 +81,9 @@ struct EntityDiscovery<'a> { | |||
| 55 | icon: Option<&'a str>, | 81 | icon: Option<&'a str>, |
| 56 | 82 | ||
| 57 | #[serde(skip_serializing_if = "Option::is_none")] | 83 | #[serde(skip_serializing_if = "Option::is_none")] |
| 84 | entity_picture: Option<&'a str>, | ||
| 85 | |||
| 86 | #[serde(skip_serializing_if = "Option::is_none")] | ||
| 58 | min: Option<f32>, | 87 | min: Option<f32>, |
| 59 | 88 | ||
| 60 | #[serde(skip_serializing_if = "Option::is_none")] | 89 | #[serde(skip_serializing_if = "Option::is_none")] |
| @@ -152,223 +181,6 @@ impl Default for DeviceResources { | |||
| 152 | } | 181 | } |
| 153 | } | 182 | } |
| 154 | 183 | ||
| 155 | pub struct TemperatureSensor<'a>(Entity<'a>); | ||
| 156 | |||
| 157 | impl<'a> TemperatureSensor<'a> { | ||
| 158 | pub fn publish(&mut self, temperature: f32) { | ||
| 159 | use core::fmt::Write; | ||
| 160 | self.0 | ||
| 161 | .publish_with(|view| write!(view, "{}", temperature).unwrap()); | ||
| 162 | } | ||
| 163 | } | ||
| 164 | |||
| 165 | pub struct Button<'a>(Entity<'a>); | ||
| 166 | |||
| 167 | impl<'a> Button<'a> { | ||
| 168 | pub async fn pressed(&mut self) { | ||
| 169 | self.0.wait_command().await; | ||
| 170 | } | ||
| 171 | } | ||
| 172 | |||
| 173 | pub struct Number<'a>(Entity<'a>); | ||
| 174 | |||
| 175 | impl<'a> Number<'a> { | ||
| 176 | pub fn value(&mut self) -> Option<f32> { | ||
| 177 | self.0.with_data(|data| { | ||
| 178 | str::from_utf8(&data.command_value) | ||
| 179 | .ok() | ||
| 180 | .and_then(|v| v.parse::<f32>().ok()) | ||
| 181 | }) | ||
| 182 | } | ||
| 183 | |||
| 184 | pub async fn value_wait(&mut self) -> f32 { | ||
| 185 | loop { | ||
| 186 | self.0.wait_command().await; | ||
| 187 | match self.value() { | ||
| 188 | Some(value) => return value, | ||
| 189 | None => continue, | ||
| 190 | } | ||
| 191 | } | ||
| 192 | } | ||
| 193 | |||
| 194 | pub fn value_set(&mut self, value: f32) { | ||
| 195 | use core::fmt::Write; | ||
| 196 | self.0 | ||
| 197 | .publish_with(|view| write!(view, "{}", value).unwrap()); | ||
| 198 | } | ||
| 199 | } | ||
| 200 | |||
| 201 | pub struct InvalidSwitchState; | ||
| 202 | |||
| 203 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| 204 | pub enum SwitchState { | ||
| 205 | On, | ||
| 206 | Off, | ||
| 207 | } | ||
| 208 | |||
| 209 | impl SwitchState { | ||
| 210 | pub fn as_str(&self) -> &'static str { | ||
| 211 | match self { | ||
| 212 | Self::On => constants::HA_SWITCH_STATE_ON, | ||
| 213 | Self::Off => constants::HA_SWITCH_STATE_OFF, | ||
| 214 | } | ||
| 215 | } | ||
| 216 | |||
| 217 | pub fn flip(self) -> Self { | ||
| 218 | match self { | ||
| 219 | Self::On => Self::Off, | ||
| 220 | Self::Off => Self::On, | ||
| 221 | } | ||
| 222 | } | ||
| 223 | } | ||
| 224 | |||
| 225 | impl core::fmt::Display for SwitchState { | ||
| 226 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { | ||
| 227 | f.write_str(self.as_str()) | ||
| 228 | } | ||
| 229 | } | ||
| 230 | |||
| 231 | impl FromStr for SwitchState { | ||
| 232 | type Err = InvalidSwitchState; | ||
| 233 | |||
| 234 | fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
| 235 | if s.eq_ignore_ascii_case(constants::HA_SWITCH_STATE_ON) { | ||
| 236 | return Ok(Self::On); | ||
| 237 | } | ||
| 238 | if s.eq_ignore_ascii_case(constants::HA_SWITCH_STATE_OFF) { | ||
| 239 | return Ok(Self::Off); | ||
| 240 | } | ||
| 241 | Err(InvalidSwitchState) | ||
| 242 | } | ||
| 243 | } | ||
| 244 | |||
| 245 | impl TryFrom<&[u8]> for SwitchState { | ||
| 246 | type Error = InvalidSwitchState; | ||
| 247 | |||
| 248 | fn try_from(value: &[u8]) -> Result<Self, Self::Error> { | ||
| 249 | let string = str::from_utf8(value).map_err(|_| InvalidSwitchState)?; | ||
| 250 | string.parse() | ||
| 251 | } | ||
| 252 | } | ||
| 253 | |||
| 254 | pub struct Switch<'a>(Entity<'a>); | ||
| 255 | |||
| 256 | impl<'a> Switch<'a> { | ||
| 257 | pub fn state(&self) -> Option<SwitchState> { | ||
| 258 | self.0 | ||
| 259 | .with_data(|data| SwitchState::try_from(data.command_value.as_slice()).ok()) | ||
| 260 | } | ||
| 261 | |||
| 262 | pub fn toggle(&mut self) -> SwitchState { | ||
| 263 | let new_state = self.state().unwrap_or(SwitchState::Off).flip(); | ||
| 264 | self.set(new_state); | ||
| 265 | new_state | ||
| 266 | } | ||
| 267 | |||
| 268 | pub fn set(&mut self, state: SwitchState) { | ||
| 269 | self.0.publish(state.as_str().as_bytes()); | ||
| 270 | } | ||
| 271 | |||
| 272 | pub async fn wait(&mut self) -> SwitchState { | ||
| 273 | loop { | ||
| 274 | self.0.wait_command().await; | ||
| 275 | if let Some(state) = self.state() { | ||
| 276 | return state; | ||
| 277 | } | ||
| 278 | } | ||
| 279 | } | ||
| 280 | } | ||
| 281 | |||
| 282 | pub struct InvalidBinarySensorState; | ||
| 283 | |||
| 284 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| 285 | pub enum BinarySensorState { | ||
| 286 | On, | ||
| 287 | Off, | ||
| 288 | } | ||
| 289 | |||
| 290 | impl BinarySensorState { | ||
| 291 | pub fn as_str(&self) -> &'static str { | ||
| 292 | match self { | ||
| 293 | Self::On => constants::HA_BINARY_SENSOR_STATE_ON, | ||
| 294 | Self::Off => constants::HA_BINARY_SENSOR_STATE_OFF, | ||
| 295 | } | ||
| 296 | } | ||
| 297 | |||
| 298 | pub fn flip(self) -> Self { | ||
| 299 | match self { | ||
| 300 | Self::On => Self::Off, | ||
| 301 | Self::Off => Self::On, | ||
| 302 | } | ||
| 303 | } | ||
| 304 | } | ||
| 305 | |||
| 306 | impl core::fmt::Display for BinarySensorState { | ||
| 307 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { | ||
| 308 | f.write_str(self.as_str()) | ||
| 309 | } | ||
| 310 | } | ||
| 311 | |||
| 312 | impl FromStr for BinarySensorState { | ||
| 313 | type Err = InvalidBinarySensorState; | ||
| 314 | |||
| 315 | fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
| 316 | if s.eq_ignore_ascii_case(constants::HA_BINARY_SENSOR_STATE_ON) { | ||
| 317 | return Ok(Self::On); | ||
| 318 | } | ||
| 319 | if s.eq_ignore_ascii_case(constants::HA_BINARY_SENSOR_STATE_OFF) { | ||
| 320 | return Ok(Self::Off); | ||
| 321 | } | ||
| 322 | Err(InvalidBinarySensorState) | ||
| 323 | } | ||
| 324 | } | ||
| 325 | |||
| 326 | impl TryFrom<&[u8]> for BinarySensorState { | ||
| 327 | type Error = InvalidBinarySensorState; | ||
| 328 | |||
| 329 | fn try_from(value: &[u8]) -> Result<Self, Self::Error> { | ||
| 330 | let string = str::from_utf8(value).map_err(|_| InvalidBinarySensorState)?; | ||
| 331 | string.parse() | ||
| 332 | } | ||
| 333 | } | ||
| 334 | |||
| 335 | pub struct BinarySensor<'a>(Entity<'a>); | ||
| 336 | |||
| 337 | impl<'a> BinarySensor<'a> { | ||
| 338 | pub fn set(&mut self, state: BinarySensorState) { | ||
| 339 | self.0.publish(state.as_str().as_bytes()); | ||
| 340 | } | ||
| 341 | |||
| 342 | pub fn value(&self) -> Option<BinarySensorState> { | ||
| 343 | self.0 | ||
| 344 | .with_data(|data| BinarySensorState::try_from(data.publish_value.as_slice())) | ||
| 345 | .ok() | ||
| 346 | } | ||
| 347 | |||
| 348 | pub fn toggle(&mut self) -> BinarySensorState { | ||
| 349 | let new_state = self.value().unwrap_or(BinarySensorState::Off).flip(); | ||
| 350 | self.set(new_state); | ||
| 351 | new_state | ||
| 352 | } | ||
| 353 | } | ||
| 354 | |||
| 355 | #[derive(Default)] | ||
| 356 | pub struct EntityConfig { | ||
| 357 | pub id: &'static str, | ||
| 358 | pub name: &'static str, | ||
| 359 | pub domain: &'static str, | ||
| 360 | pub device_class: Option<&'static str>, | ||
| 361 | pub measurement_unit: Option<&'static str>, | ||
| 362 | pub icon: Option<&'static str>, | ||
| 363 | pub category: Option<&'static str>, | ||
| 364 | pub state_class: Option<&'static str>, | ||
| 365 | pub schema: Option<&'static str>, | ||
| 366 | pub min: Option<f32>, | ||
| 367 | pub max: Option<f32>, | ||
| 368 | pub step: Option<f32>, | ||
| 369 | pub mode: Option<&'static str>, | ||
| 370 | } | ||
| 371 | |||
| 372 | struct EntityData { | 184 | struct EntityData { |
| 373 | config: EntityConfig, | 185 | config: EntityConfig, |
| 374 | publish_dirty: bool, | 186 | publish_dirty: bool, |
| @@ -380,8 +192,8 @@ struct EntityData { | |||
| 380 | } | 192 | } |
| 381 | 193 | ||
| 382 | pub struct Entity<'a> { | 194 | pub struct Entity<'a> { |
| 383 | data: &'a RefCell<Option<EntityData>>, | 195 | pub(crate) data: &'a RefCell<Option<EntityData>>, |
| 384 | waker: &'a AtomicWaker, | 196 | pub(crate) waker: &'a AtomicWaker, |
| 385 | } | 197 | } |
| 386 | 198 | ||
| 387 | impl<'a> Entity<'a> { | 199 | impl<'a> Entity<'a> { |
| @@ -521,69 +333,54 @@ impl<'a> Device<'a> { | |||
| 521 | pub fn create_temperature_sensor( | 333 | pub fn create_temperature_sensor( |
| 522 | &self, | 334 | &self, |
| 523 | id: &'static str, | 335 | id: &'static str, |
| 524 | name: &'static str, | 336 | config: TemperatureSensorConfig, |
| 525 | unit: TemperatureUnit, | ||
| 526 | ) -> TemperatureSensor<'a> { | 337 | ) -> TemperatureSensor<'a> { |
| 527 | let entity = self.create_entity(EntityConfig { | 338 | let mut entity_config = EntityConfig::default(); |
| 528 | id, | 339 | entity_config.id = id; |
| 529 | name, | 340 | config.populate(&mut entity_config); |
| 530 | domain: constants::HA_DOMAIN_SENSOR, | 341 | |
| 531 | device_class: Some(constants::HA_DEVICE_CLASS_SENSOR_TEMPERATURE), | 342 | let entity = self.create_entity(entity_config); |
| 532 | measurement_unit: Some(unit.as_str()), | 343 | TemperatureSensor::new(entity) |
| 533 | ..Default::default() | ||
| 534 | }); | ||
| 535 | TemperatureSensor(entity) | ||
| 536 | } | 344 | } |
| 537 | 345 | ||
| 538 | pub fn create_button(&self, id: &'static str, name: &'static str) -> Button<'a> { | 346 | pub fn create_button(&self, id: &'static str, config: ButtonConfig) -> Button<'a> { |
| 539 | let entity = self.create_entity(EntityConfig { | 347 | let mut entity_config = EntityConfig::default(); |
| 540 | id, | 348 | entity_config.id = id; |
| 541 | name, | 349 | config.populate(&mut entity_config); |
| 542 | domain: constants::HA_DOMAIN_BUTTON, | 350 | |
| 543 | ..Default::default() | 351 | let entity = self.create_entity(entity_config); |
| 544 | }); | 352 | Button::new(entity) |
| 545 | Button(entity) | ||
| 546 | } | 353 | } |
| 547 | 354 | ||
| 548 | pub fn create_number(&self, id: &'static str, name: &'static str) -> Number<'a> { | 355 | pub fn create_number(&self, id: &'static str, config: NumberConfig) -> Number<'a> { |
| 549 | let entity = self.create_entity(EntityConfig { | 356 | let mut entity_config = EntityConfig::default(); |
| 550 | id, | 357 | entity_config.id = id; |
| 551 | name, | 358 | config.populate(&mut entity_config); |
| 552 | domain: constants::HA_DOMAIN_NUMBER, | 359 | |
| 553 | measurement_unit: Some("s"), | 360 | let entity = self.create_entity(entity_config); |
| 554 | min: Some(0.0), | 361 | Number::new(entity) |
| 555 | max: Some(200.0), | ||
| 556 | step: Some(2.0), | ||
| 557 | mode: Some(constants::HA_NUMBER_MODE_AUTO), | ||
| 558 | ..Default::default() | ||
| 559 | }); | ||
| 560 | Number(entity) | ||
| 561 | } | 362 | } |
| 562 | 363 | ||
| 563 | pub fn create_switch(&self, id: &'static str, name: &'static str) -> Switch<'a> { | 364 | pub fn create_switch(&self, id: &'static str, config: SwitchConfig) -> Switch<'a> { |
| 564 | let entity = self.create_entity(EntityConfig { | 365 | let mut entity_config = EntityConfig::default(); |
| 565 | id, | 366 | entity_config.id = id; |
| 566 | name, | 367 | config.populate(&mut entity_config); |
| 567 | domain: constants::HA_DOMAIN_SWITCH, | 368 | |
| 568 | ..Default::default() | 369 | let entity = self.create_entity(entity_config); |
| 569 | }); | 370 | Switch::new(entity) |
| 570 | Switch(entity) | ||
| 571 | } | 371 | } |
| 572 | 372 | ||
| 573 | pub fn create_binary_sensor( | 373 | pub fn create_binary_sensor( |
| 574 | &self, | 374 | &self, |
| 575 | id: &'static str, | 375 | id: &'static str, |
| 576 | name: &'static str, | 376 | config: BinarySensorConfig, |
| 577 | class: &'static str, | ||
| 578 | ) -> BinarySensor<'a> { | 377 | ) -> BinarySensor<'a> { |
| 579 | let entity = self.create_entity(EntityConfig { | 378 | let mut entity_config = EntityConfig::default(); |
| 580 | id, | 379 | entity_config.id = id; |
| 581 | name, | 380 | config.populate(&mut entity_config); |
| 582 | domain: constants::HA_DOMAIN_BINARY_SENSOR, | 381 | |
| 583 | device_class: Some(class), | 382 | let entity = self.create_entity(entity_config); |
| 584 | ..Default::default() | 383 | BinarySensor::new(entity) |
| 585 | }); | ||
| 586 | BinarySensor(entity) | ||
| 587 | } | 384 | } |
| 588 | 385 | ||
| 589 | pub async fn run<T: Transport>(&mut self, transport: &mut T) -> ! { | 386 | pub async fn run<T: Transport>(&mut self, transport: &mut T) -> ! { |
| @@ -667,6 +464,7 @@ impl<'a> Device<'a> { | |||
| 667 | schema: entity_config.schema, | 464 | schema: entity_config.schema, |
| 668 | state_class: entity_config.state_class, | 465 | state_class: entity_config.state_class, |
| 669 | icon: entity_config.icon, | 466 | icon: entity_config.icon, |
| 467 | entity_picture: entity_config.picture, | ||
| 670 | min: entity_config.min, | 468 | min: entity_config.min, |
| 671 | max: entity_config.max, | 469 | max: entity_config.max, |
| 672 | step: entity_config.step, | 470 | step: entity_config.step, |
