aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-12-04 11:51:07 +0000
committerdiogo464 <[email protected]>2025-12-04 11:51:07 +0000
commit3289c2d9f6257f68cbdd37b78e5e79a41e9e33a1 (patch)
treeb6316df529e56a65e914e982e3c2981605e7a43c /src
init
Diffstat (limited to 'src')
-rw-r--r--src/constants.rs97
-rw-r--r--src/lib.rs967
-rw-r--r--src/transport.rs3
-rw-r--r--src/unit.rs45
4 files changed, 1112 insertions, 0 deletions
diff --git a/src/constants.rs b/src/constants.rs
new file mode 100644
index 0000000..a67b6fe
--- /dev/null
+++ b/src/constants.rs
@@ -0,0 +1,97 @@
1#![allow(unused)]
2
3pub const HA_DOMAIN_SENSOR: &str = "sensor";
4pub const HA_DOMAIN_BINARY_SENSOR: &str = "binary_sensor";
5pub const HA_DOMAIN_SWITCH: &str = "switch";
6pub const HA_DOMAIN_LIGHT: &str = "light";
7pub const HA_DOMAIN_BUTTON: &str = "button";
8pub const HA_DOMAIN_SELECT: &str = "select";
9
10pub const HA_DEVICE_CLASS_SENSOR_APPARENT_POWER: &str = "apparent_power";
11pub const HA_DEVICE_CLASS_SENSOR_AQI: &str = "aqi";
12pub const HA_DEVICE_CLASS_SENSOR_ATMOSPHERIC_PRESSURE: &str = "atmospheric_pressure";
13pub const HA_DEVICE_CLASS_SENSOR_BATTERY: &str = "battery";
14pub const HA_DEVICE_CLASS_SENSOR_CARBON_DIOXIDE: &str = "carbon_dioxide";
15pub const HA_DEVICE_CLASS_SENSOR_CARBON_MONOXIDE: &str = "carbon_monoxide";
16pub const HA_DEVICE_CLASS_SENSOR_CURRENT: &str = "current";
17pub const HA_DEVICE_CLASS_SENSOR_DATA_RATE: &str = "data_rate";
18pub const HA_DEVICE_CLASS_SENSOR_DATA_SIZE: &str = "data_size";
19pub const HA_DEVICE_CLASS_SENSOR_DATE: &str = "date";
20pub const HA_DEVICE_CLASS_SENSOR_DISTANCE: &str = "distance";
21pub const HA_DEVICE_CLASS_SENSOR_DURATION: &str = "duration";
22pub const HA_DEVICE_CLASS_SENSOR_ENERGY: &str = "energy";
23pub const HA_DEVICE_CLASS_SENSOR_ENERGY_STORAGE: &str = "energy_storage";
24pub const HA_DEVICE_CLASS_SENSOR_ENUM: &str = "enum";
25pub const HA_DEVICE_CLASS_SENSOR_FREQUENCY: &str = "frequency";
26pub const HA_DEVICE_CLASS_SENSOR_GAS: &str = "gas";
27pub const HA_DEVICE_CLASS_SENSOR_HUMIDITY: &str = "humidity";
28pub const HA_DEVICE_CLASS_SENSOR_ILLUMINANCE: &str = "illuminance";
29pub const HA_DEVICE_CLASS_SENSOR_IRRADIANCE: &str = "irradiance";
30pub const HA_DEVICE_CLASS_SENSOR_MOISTURE: &str = "moisture";
31pub const HA_DEVICE_CLASS_SENSOR_MONETARY: &str = "monetary";
32pub const HA_DEVICE_CLASS_SENSOR_NITROGEN_DIOXIDE: &str = "nitrogen_dioxide";
33pub const HA_DEVICE_CLASS_SENSOR_NITROGEN_MONOXIDE: &str = "nitrogen_monoxide";
34pub const HA_DEVICE_CLASS_SENSOR_NITROUS_OXIDE: &str = "nitrous_oxide";
35pub const HA_DEVICE_CLASS_SENSOR_OZONE: &str = "ozone";
36pub const HA_DEVICE_CLASS_SENSOR_PH: &str = "ph";
37pub const HA_DEVICE_CLASS_SENSOR_PM1: &str = "pm1";
38pub const HA_DEVICE_CLASS_SENSOR_PM25: &str = "pm25";
39pub const HA_DEVICE_CLASS_SENSOR_PM10: &str = "pm10";
40pub const HA_DEVICE_CLASS_SENSOR_POWER_FACTOR: &str = "power_factor";
41pub const HA_DEVICE_CLASS_SENSOR_POWER: &str = "power";
42pub const HA_DEVICE_CLASS_SENSOR_PRECIPITATION: &str = "precipitation";
43pub const HA_DEVICE_CLASS_SENSOR_PRECIPITATION_INTENSITY: &str = "precipitation_intensity";
44pub const HA_DEVICE_CLASS_SENSOR_PRESSURE: &str = "pressure";
45pub const HA_DEVICE_CLASS_SENSOR_REACTIVE_POWER: &str = "reactive_power";
46pub const HA_DEVICE_CLASS_SENSOR_SIGNAL_STRENGTH: &str = "signal_strength";
47pub const HA_DEVICE_CLASS_SENSOR_SOUND_PRESSURE: &str = "sound_pressure";
48pub const HA_DEVICE_CLASS_SENSOR_SPEED: &str = "speed";
49pub const HA_DEVICE_CLASS_SENSOR_SULPHUR_DIOXIDE: &str = "sulphur_dioxide";
50pub const HA_DEVICE_CLASS_SENSOR_TEMPERATURE: &str = "temperature";
51pub const HA_DEVICE_CLASS_SENSOR_TIMESTAMP: &str = "timestamp";
52pub const HA_DEVICE_CLASS_SENSOR_VOLATILE_ORGANIC_COMPOUNDS: &str = "volatile_organic_compounds";
53pub const HA_DEVICE_CLASS_SENSOR_VOLATILE_ORGANIC_COMPOUNDS_PARTS: &str =
54 "volatile_organic_compounds_parts";
55pub const HA_DEVICE_CLASS_SENSOR_VOLTAGE: &str = "voltage";
56pub const HA_DEVICE_CLASS_SENSOR_VOLUME: &str = "volume";
57pub const HA_DEVICE_CLASS_SENSOR_VOLUME_FLOW_RATE: &str = "volume_flow_rate";
58pub const HA_DEVICE_CLASS_SENSOR_VOLUME_STORAGE: &str = "volume_storage";
59pub const HA_DEVICE_CLASS_SENSOR_WATER: &str = "water";
60pub const HA_DEVICE_CLASS_SENSOR_WEIGHT: &str = "weight";
61pub const HA_DEVICE_CLASS_SENSOR_WIND_SPEED: &str = "wind_speed";
62
63pub const HA_DEVICE_CLASS_BINARY_SENSOR_BATTERY: &str = "battery";
64pub const HA_DEVICE_CLASS_BINARY_SENSOR_BATTERY_CHARGING: &str = "battery_charging";
65pub const HA_DEVICE_CLASS_BINARY_SENSOR_CARBON_MONOXIDE: &str = "carbon_monoxide";
66pub const HA_DEVICE_CLASS_BINARY_SENSOR_COLD: &str = "cold";
67pub const HA_DEVICE_CLASS_BINARY_SENSOR_CONNECTIVITY: &str = "connectivity";
68pub const HA_DEVICE_CLASS_BINARY_SENSOR_DOOR: &str = "door";
69pub const HA_DEVICE_CLASS_BINARY_SENSOR_GARAGE_DOOR: &str = "garage_door";
70pub const HA_DEVICE_CLASS_BINARY_SENSOR_GAS: &str = "gas";
71pub const HA_DEVICE_CLASS_BINARY_SENSOR_HEAT: &str = "heat";
72pub const HA_DEVICE_CLASS_BINARY_SENSOR_LIGHT: &str = "light";
73pub const HA_DEVICE_CLASS_BINARY_SENSOR_LOCK: &str = "lock";
74pub const HA_DEVICE_CLASS_BINARY_SENSOR_MOISTURE: &str = "moisture";
75pub const HA_DEVICE_CLASS_BINARY_SENSOR_MOTION: &str = "motion";
76pub const HA_DEVICE_CLASS_BINARY_SENSOR_MOVING: &str = "moving";
77pub const HA_DEVICE_CLASS_BINARY_SENSOR_OCCUPANCY: &str = "occupancy";
78pub const HA_DEVICE_CLASS_BINARY_SENSOR_OPENING: &str = "opening";
79pub const HA_DEVICE_CLASS_BINARY_SENSOR_PLUG: &str = "plug";
80pub const HA_DEVICE_CLASS_BINARY_SENSOR_POWER: &str = "power";
81pub const HA_DEVICE_CLASS_BINARY_SENSOR_PRESENCE: &str = "presence";
82pub const HA_DEVICE_CLASS_BINARY_SENSOR_PROBLEM: &str = "problem";
83pub const HA_DEVICE_CLASS_BINARY_SENSOR_RUNNING: &str = "running";
84pub const HA_DEVICE_CLASS_BINARY_SENSOR_SAFETY: &str = "safety";
85pub const HA_DEVICE_CLASS_BINARY_SENSOR_SMOKE: &str = "smoke";
86pub const HA_DEVICE_CLASS_BINARY_SENSOR_SOUND: &str = "sound";
87pub const HA_DEVICE_CLASS_BINARY_SENSOR_TAMPER: &str = "tamper";
88pub const HA_DEVICE_CLASS_BINARY_SENSOR_UPDATE: &str = "update";
89pub const HA_DEVICE_CLASS_BINARY_SENSOR_VIBRATION: &str = "vibration";
90pub const HA_DEVICE_CLASS_BINARY_SENSOR_WINDOW: &str = "window";
91
92pub const HA_DEVICE_CLASS_BUTTON_IDENTIFY: &str = "identify";
93pub const HA_DEVICE_CLASS_BUTTON_RESTART: &str = "restart";
94pub const HA_DEVICE_CLASS_BUTTON_UPDATE: &str = "update";
95
96pub const HA_DEVICE_CLASS_SWITCH_OUTLET: &str = "outlet";
97pub const HA_DEVICE_CLASS_SWITCH_SWITCH: &str = "switch";
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..5ecd5ea
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,967 @@
1#![no_std]
2
3use core::{
4 cell::RefCell,
5 net::SocketAddrV4,
6 sync::atomic::{AtomicBool, AtomicU32},
7 task::Waker,
8};
9
10use defmt::Format;
11use embassy_net::tcp::TcpSocket;
12use embassy_sync::waitqueue::AtomicWaker;
13use embassy_time::Timer;
14use heapless::{
15 Vec, VecView,
16 string::{String, StringView},
17};
18use serde::Serialize;
19
20mod constants;
21mod transport;
22mod unit;
23
24pub use constants::*;
25pub use transport::Transport;
26pub use unit::*;
27
28enum Unit {
29 Temperature(TemperatureUnit),
30}
31
32impl Unit {
33 fn as_str(&self) -> &'static str {
34 match self {
35 Unit::Temperature(unit) => unit.as_str(),
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum ComponentType {
42 Sensor,
43 BinarySensor,
44}
45
46impl core::fmt::Display for ComponentType {
47 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
48 f.write_str(self.as_str())
49 }
50}
51
52impl ComponentType {
53 fn as_str(&self) -> &'static str {
54 match self {
55 ComponentType::Sensor => "sensor",
56 ComponentType::BinarySensor => "binary_sensor",
57 }
58 }
59}
60
61// TODO: see what classes need this and defaults
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum StateClass {
64 Measurement,
65 Total,
66 TotalIncreasing,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum DeviceClass {
71 Temperature {
72 unit: TemperatureUnit,
73 },
74 Humidity {
75 unit: HumidityUnit,
76 },
77
78 // binary sensors
79 Door,
80 Window,
81 Motion,
82 Occupancy,
83 Opening,
84 Plug,
85 Presence,
86 Problem,
87 Safety,
88 Smoke,
89 Sound,
90 Vibration,
91
92 Battery {
93 unit: BatteryUnit,
94 },
95 Illuminance {
96 unit: LightUnit,
97 },
98 Pressure {
99 unit: PressureUnit,
100 },
101 Generic {
102 device_class: Option<&'static str>,
103 unit: Option<&'static str>,
104 },
105 Energy {
106 unit: EnergyUnit,
107 },
108}
109
110impl DeviceClass {
111 fn tag(&self) -> &'static str {
112 match self {
113 DeviceClass::Temperature { .. } => "temperature",
114 DeviceClass::Humidity { .. } => "humidity",
115 _ => todo!(),
116 }
117 }
118
119 fn unit_of_measurement(&self) -> Option<Unit> {
120 // TODO: fix
121 Some(Unit::Temperature(TemperatureUnit::Celcius))
122 }
123
124 fn component_type(&self) -> ComponentType {
125 match self {
126 DeviceClass::Temperature { .. } => ComponentType::Sensor,
127 DeviceClass::Humidity { .. } => ComponentType::Sensor,
128 DeviceClass::Door => ComponentType::BinarySensor,
129 DeviceClass::Window => ComponentType::BinarySensor,
130 _ => todo!(),
131 }
132 }
133}
134
135pub trait Entity {
136 // TODO: possibly collapse all these functions into a single one that returns a struct
137 fn id(&self) -> &'static str;
138 fn name(&self) -> &'static str;
139 fn device_class(&self) -> DeviceClass;
140 fn register_waker(&self, waker: &Waker);
141 fn value(&self) -> Option<StateValue>;
142}
143
144// TODO: figure out proper atomic orderings
145
146struct StateContainer {
147 dirty: AtomicBool,
148 waker: AtomicWaker,
149 value: StateContainerValue,
150}
151
152impl StateContainer {
153 const fn new(value: StateContainerValue) -> Self {
154 Self {
155 dirty: AtomicBool::new(false),
156 waker: AtomicWaker::new(),
157 value,
158 }
159 }
160
161 pub const fn new_u32() -> Self {
162 Self::new(StateContainerValue::U32(AtomicU32::new(0)))
163 }
164
165 pub const fn new_f32() -> Self {
166 Self::new(StateContainerValue::F32(AtomicU32::new(0)))
167 }
168}
169
170enum StateContainerValue {
171 U32(AtomicU32),
172 F32(AtomicU32),
173}
174
175pub enum StateValue {
176 U32(u32),
177 F32(f32),
178}
179
180#[derive(Debug, Format, Clone, Copy, Serialize)]
181struct DeviceDiscovery<'a> {
182 identifiers: &'a [&'a str],
183 name: &'a str,
184 manufacturer: &'a str,
185 model: &'a str,
186}
187
188pub enum SensorKind {
189 Generic,
190 Temperature { unit: TemperatureUnit },
191 Humidity { unit: HumidityUnit },
192 // TODO: complete
193}
194
195impl SensorKind {
196 fn as_str(&self) -> &'static str {
197 match self {
198 SensorKind::Generic => "sensor",
199 SensorKind::Temperature { .. } => "temperature",
200 SensorKind::Humidity { .. } => "humidity",
201 }
202 }
203}
204
205impl core::fmt::Display for SensorKind {
206 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
207 f.write_str(self.as_str())
208 }
209}
210
211enum BinarySensorKind {
212 Generic,
213 Motion,
214 Door,
215 Window,
216 Occupancy,
217 // TODO: complete
218}
219
220impl BinarySensorKind {
221 fn as_str(&self) -> &'static str {
222 match self {
223 BinarySensorKind::Generic => "binary_sensor",
224 BinarySensorKind::Motion => "motion",
225 BinarySensorKind::Door => "door",
226 BinarySensorKind::Window => "window",
227 BinarySensorKind::Occupancy => "occupancy",
228 }
229 }
230}
231
232impl core::fmt::Display for BinarySensorKind {
233 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
234 f.write_str(self.as_str())
235 }
236}
237
238enum SwitchKind {
239 Generic,
240 Outlet,
241 Switch,
242}
243
244impl SwitchKind {
245 fn as_str(&self) -> &'static str {
246 match self {
247 SwitchKind::Generic => "switch",
248 SwitchKind::Outlet => "outlet",
249 SwitchKind::Switch => "switch",
250 }
251 }
252}
253
254impl core::fmt::Display for SwitchKind {
255 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
256 f.write_str(self.as_str())
257 }
258}
259
260enum ButtonKind {
261 Generic,
262 Identify,
263 Restart,
264 Update,
265}
266
267impl ButtonKind {
268 fn as_str(&self) -> &'static str {
269 match self {
270 ButtonKind::Generic => "button",
271 ButtonKind::Identify => "identify",
272 ButtonKind::Restart => "restart",
273 ButtonKind::Update => "update",
274 }
275 }
276}
277
278impl core::fmt::Display for ButtonKind {
279 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
280 f.write_str(self.as_str())
281 }
282}
283
284enum NumberKind {
285 Generic,
286 // TODO: alot of different ones
287 // https://www.home-assistant.io/integrations/number
288}
289
290impl NumberKind {
291 fn as_str(&self) -> &'static str {
292 match self {
293 NumberKind::Generic => "number",
294 }
295 }
296}
297
298impl core::fmt::Display for NumberKind {
299 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
300 f.write_str(self.as_str())
301 }
302}
303
304// this is called the component type in the ha api
305pub enum EntityDomain {
306 Sensor(SensorKind),
307 BinarySensor(BinarySensorKind),
308 Switch(SwitchKind),
309 Light,
310 Button(ButtonKind),
311 Select,
312}
313
314impl core::fmt::Display for EntityDomain {
315 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
316 f.write_str(self.as_str())
317 }
318}
319
320impl EntityDomain {
321 fn as_str(&self) -> &'static str {
322 match self {
323 EntityDomain::Sensor(_) => "sensor",
324 EntityDomain::BinarySensor(_) => "binary_sensor",
325 EntityDomain::Switch(_) => "switch",
326 EntityDomain::Light => "light",
327 EntityDomain::Button(_) => "button",
328 EntityDomain::Select => "select",
329 }
330 }
331}
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
334enum EntityCategory {
335 Config,
336 Diagnostic,
337}
338
339#[derive(Debug, Format, Serialize)]
340struct EntityDiscovery<'a> {
341 #[serde(rename = "unique_id")]
342 id: &'a str,
343
344 name: &'a str,
345
346 #[serde(skip_serializing_if = "Option::is_none")]
347 device_class: Option<&'a str>,
348
349 #[serde(skip_serializing_if = "Option::is_none")]
350 state_topic: Option<&'a str>,
351
352 #[serde(skip_serializing_if = "Option::is_none")]
353 command_topic: Option<&'a str>,
354
355 #[serde(skip_serializing_if = "Option::is_none")]
356 unit_of_measurement: Option<&'a str>,
357
358 #[serde(skip_serializing_if = "Option::is_none")]
359 schema: Option<&'a str>,
360
361 #[serde(skip_serializing_if = "Option::is_none")]
362 state_class: Option<&'a str>,
363
364 #[serde(skip_serializing_if = "Option::is_none")]
365 icon: Option<&'a str>,
366
367 device: &'a DeviceDiscovery<'a>,
368}
369
370struct DiscoveryTopicDisplay<'a> {
371 domain: &'a str,
372 device_id: &'a str,
373 entity_id: &'a str,
374}
375
376impl<'a> core::fmt::Display for DiscoveryTopicDisplay<'a> {
377 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
378 write!(
379 f,
380 "homeassistant/{}/{}_{}/config",
381 self.domain, self.device_id, self.entity_id
382 )
383 }
384}
385
386struct StateTopicDisplay<'a> {
387 device_id: &'a str,
388 entity_id: &'a str,
389}
390
391impl<'a> core::fmt::Display for StateTopicDisplay<'a> {
392 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
393 write!(f, "embassy-ha/{}/{}/state", self.device_id, self.entity_id)
394 }
395}
396
397struct CommandTopicDisplay<'a> {
398 device_id: &'a str,
399 entity_id: &'a str,
400}
401
402impl<'a> core::fmt::Display for CommandTopicDisplay<'a> {
403 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
404 write!(
405 f,
406 "embassy-ha/{}/{}/command",
407 self.device_id, self.entity_id
408 )
409 }
410}
411
412pub struct DeviceConfig {
413 pub device_id: &'static str,
414 pub device_name: &'static str,
415 pub manufacturer: &'static str,
416 pub model: &'static str,
417}
418
419pub struct DeviceResources {
420 waker: AtomicWaker,
421 entities: [RefCell<Option<EntityData>>; Self::ENTITY_LIMIT],
422
423 mqtt_resources: embedded_mqtt::ClientResources,
424 publish_buffer: Vec<u8, 2048>,
425 subscribe_buffer: Vec<u8, 128>,
426 discovery_buffer: Vec<u8, 2048>,
427 discovery_topic_buffer: String<128>,
428 state_topic_buffer: String<128>,
429 command_topic_buffer: String<128>,
430}
431
432impl DeviceResources {
433 const RX_BUFFER_LEN: usize = 2048;
434 const TX_BUFFER_LEN: usize = 2048;
435 const ENTITY_LIMIT: usize = 16;
436}
437
438impl Default for DeviceResources {
439 fn default() -> Self {
440 Self {
441 waker: AtomicWaker::new(),
442 entities: [const { RefCell::new(None) }; Self::ENTITY_LIMIT],
443
444 mqtt_resources: Default::default(),
445 publish_buffer: Default::default(),
446 subscribe_buffer: Default::default(),
447 discovery_buffer: Default::default(),
448 discovery_topic_buffer: Default::default(),
449 state_topic_buffer: Default::default(),
450 command_topic_buffer: Default::default(),
451 }
452 }
453}
454
455pub struct TemperatureSensor<'a>(Entity2<'a>);
456
457impl<'a> TemperatureSensor<'a> {
458 pub fn publish(&mut self, temperature: f32) {
459 use core::fmt::Write;
460 self.0
461 .publish_with(|view| write!(view, "{}", temperature).unwrap());
462 }
463}
464
465pub struct Button<'a>(Entity2<'a>);
466
467impl<'a> Button<'a> {
468 pub async fn pressed(&mut self) {
469 self.0.wait_command().await;
470 }
471}
472
473pub struct EntityConfig {
474 pub id: &'static str,
475 pub name: &'static str,
476 pub domain: &'static str,
477 pub device_class: Option<&'static str>,
478 pub measurement_unit: Option<&'static str>,
479 pub icon: Option<&'static str>,
480 pub category: Option<&'static str>,
481 pub state_class: Option<&'static str>,
482 pub schema: Option<&'static str>,
483}
484
485struct EntityData {
486 config: EntityConfig,
487 publish_dirty: bool,
488 publish_value: heapless::Vec<u8, 64>,
489 command_dirty: bool,
490 command_value: heapless::Vec<u8, 64>,
491 command_wait_waker: Option<Waker>,
492}
493
494pub struct Entity2<'a> {
495 data: &'a RefCell<Option<EntityData>>,
496 waker: &'a AtomicWaker,
497}
498
499impl<'a> Entity2<'a> {
500 pub fn publish(&mut self, payload: &[u8]) {
501 self.publish_with(|view| view.extend_from_slice(payload).unwrap());
502 }
503
504 pub fn publish_with<F>(&mut self, f: F)
505 where
506 F: FnOnce(&mut VecView<u8>),
507 {
508 self.with_data(move |data| {
509 data.publish_value.clear();
510 f(data.publish_value.as_mut_view());
511 data.publish_dirty = true;
512 });
513 self.waker.wake();
514 }
515
516 pub async fn wait_command(&mut self) {
517 struct Fut<'a, 'b>(&'a mut Entity2<'b>);
518
519 impl<'a, 'b> core::future::Future for Fut<'a, 'b> {
520 type Output = ();
521
522 fn poll(
523 mut self: core::pin::Pin<&mut Self>,
524 cx: &mut core::task::Context<'_>,
525 ) -> core::task::Poll<Self::Output> {
526 let this = &mut self.as_mut().0;
527 this.with_data(|data| {
528 let dirty = data.command_dirty;
529 if dirty {
530 data.command_dirty = false;
531 data.command_wait_waker = None;
532 core::task::Poll::Ready(())
533 } else {
534 // TODO: avoid clone if waker would wake
535 data.command_wait_waker = Some(cx.waker().clone());
536 core::task::Poll::Pending
537 }
538 })
539 }
540 }
541
542 Fut(self).await
543 }
544
545 pub fn with_command<F, R>(&mut self, f: F) -> R
546 where
547 F: FnOnce(&[u8]) -> R,
548 {
549 self.with_data(|data| f(data.command_value.as_slice()))
550 }
551
552 fn with_data<F, R>(&self, f: F) -> R
553 where
554 F: FnOnce(&mut EntityData) -> R,
555 {
556 f(self.data.borrow_mut().as_mut().unwrap())
557 }
558}
559
560pub struct Device<'a> {
561 config: DeviceConfig,
562
563 // resources
564 waker: &'a AtomicWaker,
565 entities: &'a [RefCell<Option<EntityData>>],
566
567 mqtt_resources: &'a mut embedded_mqtt::ClientResources,
568 publish_buffer: &'a mut VecView<u8>,
569 subscribe_buffer: &'a mut VecView<u8>,
570 discovery_buffer: &'a mut VecView<u8>,
571 discovery_topic_buffer: &'a mut StringView,
572 state_topic_buffer: &'a mut StringView,
573 command_topic_buffer: &'a mut StringView,
574}
575
576impl<'a> Device<'a> {
577 pub fn new(resources: &'a mut DeviceResources, config: DeviceConfig) -> Self {
578 Self {
579 config,
580 waker: &resources.waker,
581 entities: &resources.entities,
582
583 mqtt_resources: &mut resources.mqtt_resources,
584 publish_buffer: &mut resources.publish_buffer,
585 subscribe_buffer: &mut resources.subscribe_buffer,
586 discovery_buffer: &mut resources.discovery_buffer,
587 discovery_topic_buffer: &mut resources.discovery_topic_buffer,
588 state_topic_buffer: &mut resources.state_topic_buffer,
589 command_topic_buffer: &mut resources.command_topic_buffer,
590 }
591 }
592
593 pub fn create_entity(&self, config: EntityConfig) -> Entity2<'a> {
594 let index = 'outer: {
595 for idx in 0..self.entities.len() {
596 if self.entities[idx].borrow().is_none() {
597 break 'outer idx;
598 }
599 }
600 panic!("device entity limit reached");
601 };
602
603 let data = EntityData {
604 config,
605 publish_dirty: false,
606 publish_value: Default::default(),
607 command_dirty: false,
608 command_value: Default::default(),
609 command_wait_waker: None,
610 };
611 self.entities[index].replace(Some(data));
612
613 Entity2 {
614 data: &self.entities[index],
615 waker: self.waker,
616 }
617 }
618
619 pub fn create_temperature_sensor(
620 &self,
621 id: &'static str,
622 name: &'static str,
623 unit: TemperatureUnit,
624 ) -> TemperatureSensor<'a> {
625 let entity = self.create_entity(EntityConfig {
626 id,
627 name,
628 domain: HA_DOMAIN_SENSOR,
629 device_class: Some(HA_DEVICE_CLASS_SENSOR_TEMPERATURE),
630 measurement_unit: Some(unit.as_str()),
631 icon: None,
632 category: None,
633 state_class: None,
634 schema: None,
635 });
636 TemperatureSensor(entity)
637 }
638
639 pub fn create_button(&self, id: &'static str, name: &'static str) -> Button<'a> {
640 let entity = self.create_entity(EntityConfig {
641 id,
642 name,
643 domain: HA_DOMAIN_BUTTON,
644 device_class: None,
645 measurement_unit: None,
646 icon: None,
647 category: None,
648 state_class: None,
649 schema: None,
650 });
651 Button(entity)
652 }
653
654 pub async fn run<T: Transport>(&mut self, transport: &mut T) -> ! {
655 loop {
656 self.run_iteration(&mut *transport).await;
657 Timer::after_millis(5000).await;
658 }
659 }
660
661 async fn run_iteration<T: Transport>(&mut self, transport: T) {
662 let mut client = embedded_mqtt::Client::new(self.mqtt_resources, transport);
663 client.connect("embassy-ha-client-id").await.unwrap();
664
665 defmt::info!("sending discover messages");
666 let device_discovery = DeviceDiscovery {
667 identifiers: &[self.config.device_id],
668 name: self.config.device_name,
669 manufacturer: self.config.manufacturer,
670 model: self.config.model,
671 };
672
673 for entity in self.entities {
674 use core::fmt::Write;
675
676 self.publish_buffer.clear();
677 self.subscribe_buffer.clear();
678 self.discovery_buffer.clear();
679 self.discovery_topic_buffer.clear();
680 self.state_topic_buffer.clear();
681 self.command_topic_buffer.clear();
682
683 // borrow the entity and fill out the buffers to be sent
684 // this should be done inside a block so that we do not hold the RefMut across an
685 // await
686 {
687 let mut entity = entity.borrow_mut();
688 let entity = match entity.as_mut() {
689 Some(entity) => entity,
690 None => break,
691 };
692 let entity_config = &entity.config;
693
694 write!(
695 self.discovery_topic_buffer,
696 "{}",
697 DiscoveryTopicDisplay {
698 domain: entity_config.domain,
699 device_id: self.config.device_id,
700 entity_id: entity_config.id,
701 }
702 )
703 .unwrap();
704
705 write!(
706 self.state_topic_buffer,
707 "{}",
708 StateTopicDisplay {
709 device_id: self.config.device_id,
710 entity_id: entity_config.id
711 }
712 )
713 .unwrap();
714
715 write!(
716 self.command_topic_buffer,
717 "{}",
718 CommandTopicDisplay {
719 device_id: self.config.device_id,
720 entity_id: entity_config.id
721 }
722 )
723 .unwrap();
724
725 let discovery = EntityDiscovery {
726 id: entity_config.id,
727 name: entity_config.name,
728 device_class: entity_config.device_class,
729 state_topic: Some(self.state_topic_buffer.as_str()),
730 command_topic: Some(self.command_topic_buffer.as_str()),
731 unit_of_measurement: entity_config.measurement_unit,
732 schema: entity_config.schema,
733 state_class: entity_config.state_class,
734 icon: entity_config.icon,
735 device: &device_discovery,
736 };
737 defmt::info!("discovery: {}", discovery);
738
739 self.discovery_buffer
740 .resize(self.discovery_buffer.capacity(), 0)
741 .unwrap();
742 let n = serde_json_core::to_slice(&discovery, &mut self.discovery_buffer).unwrap();
743 self.discovery_buffer.truncate(n);
744 }
745
746 defmt::info!(
747 "sending discovery to {}",
748 self.discovery_topic_buffer.as_str()
749 );
750 client
751 .publish(&self.discovery_topic_buffer, &self.discovery_buffer)
752 .await
753 .unwrap();
754 client.subscribe(&self.command_topic_buffer).await.unwrap();
755 }
756
757 loop {
758 use core::fmt::Write;
759
760 for entity in self.entities {
761 {
762 let mut entity = entity.borrow_mut();
763 let entity = match entity.as_mut() {
764 Some(entity) => entity,
765 None => break,
766 };
767
768 if !entity.publish_dirty {
769 continue;
770 }
771
772 entity.publish_dirty = false;
773
774 self.state_topic_buffer.clear();
775 write!(
776 self.state_topic_buffer,
777 "{}",
778 StateTopicDisplay {
779 device_id: self.config.device_id,
780 entity_id: entity.config.id
781 }
782 )
783 .unwrap();
784
785 self.publish_buffer.clear();
786 self.publish_buffer
787 .extend_from_slice(entity.publish_value.as_slice())
788 .unwrap();
789 }
790
791 client
792 .publish(&self.state_topic_buffer, self.publish_buffer)
793 .await
794 .unwrap();
795 }
796
797 let receive = client.receive();
798 let waker = wait_on_atomic_waker(self.waker);
799 match embassy_futures::select::select(receive, waker).await {
800 embassy_futures::select::Either::First(packet) => {
801 let packet = packet.unwrap();
802 let mut read_buffer = [0u8; 128];
803 if let embedded_mqtt::Packet::Publish(publish) = packet {
804 if publish.data_len > 128 {
805 defmt::warn!("mqtt publish payload too large, ignoring message");
806 } else {
807 let b = &mut read_buffer[..publish.data_len];
808 client.receive_data(b).await.unwrap();
809 defmt::info!("receive value {}", str::from_utf8(b).unwrap());
810 for entity in self.entities {
811 let mut entity = entity.borrow_mut();
812 if let Some(entity) = entity.as_mut() {
813 entity.command_dirty = true;
814 entity.command_value.clear();
815 entity.command_value.extend_from_slice(b"ON").unwrap();
816 if let Some(ref waker) = entity.command_wait_waker {
817 waker.wake_by_ref();
818 }
819 }
820 }
821 }
822 }
823 }
824 embassy_futures::select::Either::Second(_) => {}
825 }
826 }
827 }
828}
829
830async fn wait_on_atomic_waker(waker: &AtomicWaker) {
831 struct F<'a>(&'a AtomicWaker, bool);
832 impl<'a> core::future::Future for F<'a> {
833 type Output = ();
834
835 fn poll(
836 self: core::pin::Pin<&mut Self>,
837 cx: &mut core::task::Context<'_>,
838 ) -> core::task::Poll<Self::Output> {
839 if !self.1 {
840 self.0.register(cx.waker());
841 self.get_mut().1 = true;
842 core::task::Poll::Pending
843 } else {
844 core::task::Poll::Ready(())
845 }
846 }
847 }
848 F(waker, false).await
849}
850
851/*
852 Step-by-Step Process
853
854 1. What are you measuring/controlling?
855
856 Start with the physical thing:
857 - "I want to measure temperature"
858 - "I want to detect if a door is open"
859 - "I want to control a relay"
860 - "I want a button to restart the device"
861
862 2. Pick the component type based on behavior
863
864 Ask yourself:
865 - Is it read-only or controllable?
866 - Does it have numeric values or on/off states?
867
868 Decision tree:
869 Read-only measurement?
870 ├─ Numeric value (23.5, 65%, etc.)
871 │ └─ Component: sensor
872 └─ On/off state (open/closed, detected/not detected)
873 └─ Component: binary_sensor
874
875 Controllable?
876 ├─ On/off control
877 │ └─ Component: switch (or light for LEDs)
878 ├─ Adjustable number
879 │ └─ Component: number
880 ├─ Select from options
881 │ └─ Component: select
882 └─ Trigger action (no state)
883 └─ Component: button
884
885 3. Pick the device_class (if applicable)
886
887 Now look at the component type you chose:
888
889 For sensor - What kind of measurement?
890 - Temperature → device_class: "temperature"
891 - Humidity → device_class: "humidity"
892 - Pressure → device_class: "pressure"
893 - Custom metric → device_class: None
894
895 For binary_sensor - What kind of detection?
896 - Door → device_class: "door"
897 - Motion → device_class: "motion"
898 - Window → device_class: "window"
899 - Generic → device_class: None
900
901 For button - No device_class needed!
902
903 4. Pick units (if applicable)
904
905 Based on your device_class:
906 - Temperature → "°C" or "°F"
907 - Humidity → "%"
908 - Pressure → "hPa"
909
910 Examples
911
912 Example 1: DHT22 Temperature Reading
913
914 1. What? → Measure temperature
915 2. Component? → sensor (numeric, read-only)
916 3. Device class? → "temperature"
917 4. Unit? → "°C"
918
919 Result:
920 - Discovery: homeassistant/sensor/pico2w_temp/config
921 - JSON: device_class: "temperature", unit_of_measurement: "°C"
922
923 Example 2: Reed Switch on Door
924
925 1. What? → Detect door open/closed
926 2. Component? → binary_sensor (on/off state, read-only)
927 3. Device class? → "door"
928 4. Unit? → N/A
929
930 Result:
931 - Discovery: homeassistant/binary_sensor/pico2w_door/config
932 - JSON: device_class: "door"
933
934 Example 3: Relay Control
935
936 1. What? → Control a relay
937 2. Component? → switch (on/off, controllable)
938 3. Device class? → None (switches typically don't have device_class)
939 4. Unit? → N/A
940
941 Result:
942 - Discovery: homeassistant/switch/pico2w_relay/config
943 - JSON: No device_class needed
944
945 Example 4: Restart Button
946
947 1. What? → Trigger device restart
948 2. Component? → button (action trigger, no state)
949 3. Device class? → None (buttons don't have device_class)
950 4. Unit? → N/A
951
952 Result:
953 - Discovery: homeassistant/button/pico2w_restart/config
954 - JSON: No device_class, no state_topic
955
956 TL;DR Workflow
957
958 Physical thing
959
960 Component type (behavior: read-only numeric? binary? controllable?)
961
962 Device class (what specific type?)
963
964 Units (if numeric)
965
966 Does this mental model make sense now?
967*/
diff --git a/src/transport.rs b/src/transport.rs
new file mode 100644
index 0000000..5214b37
--- /dev/null
+++ b/src/transport.rs
@@ -0,0 +1,3 @@
1pub trait Transport: embedded_io_async::Read + embedded_io_async::Write {}
2
3impl<T> Transport for T where T: embedded_io_async::Read + embedded_io_async::Write {}
diff --git a/src/unit.rs b/src/unit.rs
new file mode 100644
index 0000000..4f3ca19
--- /dev/null
+++ b/src/unit.rs
@@ -0,0 +1,45 @@
1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2pub enum TemperatureUnit {
3 Celcius,
4 Kelvin,
5 Fahrenheit,
6 Other(&'static str),
7}
8
9impl TemperatureUnit {
10 pub fn as_str(&self) -> &'static str {
11 // TODO: improve
12 match self {
13 TemperatureUnit::Celcius => "C",
14 TemperatureUnit::Kelvin => "k",
15 TemperatureUnit::Fahrenheit => "F",
16 TemperatureUnit::Other(other) => other,
17 }
18 }
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum HumidityUnit {
23 Percentage,
24 Other(&'static str),
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum BatteryUnit {
29 Percentage,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum LightUnit {
34 Lux,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum PressureUnit {
39 HectoPascal,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum EnergyUnit {
44 KiloWattHour,
45}