Get many parts working

This commit is contained in:
Austen Adler 2024-04-21 13:16:26 -04:00
parent 5ff96febac
commit f65d45260e
8 changed files with 949 additions and 638 deletions

View File

@ -1,4 +1,7 @@
use std::fmt::Debug;
use crate::pattern::Pattern;
#[derive(Debug)]
pub enum Message {
ClearLights,
ChangePattern(Box<dyn Pattern + Send + Sync>),
@ -6,3 +9,15 @@ pub enum Message {
SetTickTime(u64),
Quit,
}
// impl Debug for Message {
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// match self {
// Message::ClearLights => write!(f, "Message::ClearLights"),
// Message::ChangePattern(_) => write!(f, "Message::ChangePattern(_)"),
// Message::SetNumLights(n) => write!(f, "Message::SetNumLights({n})"),
// Message::SetTickTime(n) => write!(f, "Message::SetTickTime({n})"),
// Message::Quit => write!(f, "Message::Quit"),
// }
// }
// }

View File

@ -5,6 +5,21 @@ use serde::{Deserialize, Serialize};
// TODO:
pub type Icon = ();
pub mod homeassistant {
pub const STATUS_ONLINE: &[u8] = b"online";
pub const STATUS_OFFLINE: &[u8] = b"offline";
// use super::*;
// #[derive(Debug, PartialEq, Clone)]
// #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
// #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
// pub enum Status {
// Offline,
// Online,
// }
}
pub mod alarm_control_panel {
use super::*;

View File

@ -11,3 +11,4 @@ serde_json = "1.0.115"
tracing = "0.1.37"
homeassistant-mqtt-discovery = { path = "../homeassistant-mqtt-discovery/" }
clap = { version = "4.5.4", features = ["derive"] }
anyhow = "1.0.82"

View File

@ -1,119 +1,362 @@
#![feature(let_chains)]
use common::MqttConfig;
use homeassistant_mqtt_discovery::integrations::light;
use rumqttc::{Client, Event, Incoming, MqttOptions, Publish, QoS};
use anyhow::{Context as _, Result};
use common::{
color::Rgb,
error::ProgramError,
pattern::{MovingRainbow, Solid, SolidParams},
MqttConfig,
};
use homeassistant_mqtt_discovery::{
integrations::{
homeassistant,
light::{self, JsonIncoming},
},
Common,
};
use rumqttc::{
Client, Connection, ConnectionError, Event, Incoming, MqttOptions, Packet, PingReq, Publish,
QoS,
};
use std::time::Duration;
use tracing::info;
use tracing::{error, info, warn};
use common::{error::ProgramResult, strip};
use std::sync::mpsc::Sender;
const MQTT_PREFIX: &str = "aw_lights";
const FRIENDLY_NAME: &str = "AW Lights Light";
const HOMEASSISTANT_TOPIC: &str = "homeassistant/status";
const LIGHT_ON: &str = "ON";
const LIGHT_OFF: &str = "OFF";
pub fn start(_strip_tx: Sender<strip::Message>, config: MqttConfig) -> ProgramResult<()> {
info!("Starting mqtt with config {config:?}");
x(&config);
Ok(())
pub struct MqttBuilder {
topics: Topics,
config: MqttConfig,
}
fn x(config: &MqttConfig) {
let mut mqttoptions = MqttOptions::new(&config.mqtt_id, &config.mqtt_broker, config.mqtt_port);
impl MqttBuilder {
pub fn new(config: MqttConfig) -> Self {
let topics = Topics::new(&config);
mqttoptions.set_keep_alive(Duration::from_secs(5));
if let Some(mqtt_username) = config.mqtt_username.as_ref()
&& let Some(mqtt_password) = config.mqtt_password.as_ref()
{
info!("Using authentication with mqtt");
mqttoptions.set_credentials(mqtt_username, mqtt_password);
Self { config, topics }
}
info!("Starting mqtt client");
let (client, mut connection) = Client::new(mqttoptions, 10);
info!("Subscribing to homeassistant");
pub fn start(self, strip_tx: Sender<strip::Message>) -> ProgramResult<()> {
let (client, connection) = self.create_conection();
let (autodiscovery_topic_name, discovery) = gen_discovery_message(&config);
let mut mqtt = Mqtt {
topics: self.topics,
config: self.config,
client,
};
info!(
"discovery_message: {:#?} (topic: {autodiscovery_topic_name:?})",
discovery
);
mqtt.start(connection, strip_tx)
}
// client.publish(, QoS::AtLeastOnce, false, payload)
fn create_conection(&self) -> (Client, Connection) {
info!("Creating mqtt client");
let mut mqttoptions = MqttOptions::new(
&self.config.mqtt_id,
&self.config.mqtt_broker,
self.config.mqtt_port,
);
client
.subscribe(HOMEASSISTANT_TOPIC, QoS::AtMostOnce)
.unwrap();
mqttoptions.set_keep_alive(Duration::from_secs(5));
client
.subscribe(&discovery.command_topic, QoS::AtMostOnce)
.unwrap();
if let Some(mqtt_username) = self.config.mqtt_username.as_ref()
&& let Some(mqtt_password) = self.config.mqtt_password.as_ref()
{
info!("Using authentication with mqtt");
mqttoptions.set_credentials(mqtt_username, mqtt_password);
}
Client::new(mqttoptions, 10)
}
}
client
.publish(
autodiscovery_topic_name,
QoS::AtLeastOnce,
false,
serde_json::to_vec(&discovery).unwrap(),
)
.unwrap();
pub struct Mqtt {
topics: Topics,
config: MqttConfig,
client: rumqttc::Client,
// connection: rumqttc::Connection,
}
// thread::spawn(move || {
// for i in 0..10 {
// client
// .publish("hello/rumqtt", QoS::AtLeastOnce, false, vec![i; i as usize])
// .unwrap();
// thread::sleep(Duration::from_millis(100));
impl Mqtt {
// pub fn new(config: MqttConfig) -> Self {
// let topics = Topics::new(&config);
// Self {
// config,
// topics,
// // client,
// // connection,
// }
// });
// }
// Iterate to poll the eventloop for connection progress
for (i, notification) in connection.iter().enumerate() {
info!("Notification #{i} = {:?}", notification);
pub fn start(
&mut self,
mut connection: Connection,
strip_tx: Sender<strip::Message>,
) -> ProgramResult<()> {
info!("Starting mqtt");
self.init().map_err(|e| ProgramError::Boxed(Box::new(e)))?;
if let Ok(Event::Incoming(Incoming::Publish(p))) = notification {
info!(
"Got publish notification. Trying to deserialize: {:#?}",
serde_json::from_slice::<light::JsonIncoming>(&p.payload)
// Iterate to poll the eventloop for connection progress
for (i, message) in connection.iter().enumerate() {
// info!("Notification #{i} = {:?}", message);
match message {
Ok(Event::Incoming(Incoming::Publish(p))) => {
if let Err(e) = self.handle_incoming_message(p) {
info!("Got error: {e:?}");
}
// info!(
// "Got publish notification. Trying to deserialize: {:#?}",
// serde_json::from_slice::<light::JsonIncoming>(&p.payload)
// )
}
Ok(Event::Outgoing(_))
| Ok(Event::Incoming(Packet::PingResp))
| Ok(Event::Incoming(Incoming::SubAck(_)))
| Ok(Event::Incoming(Incoming::ConnAck(_)))
| Ok(Event::Incoming(Incoming::PubAck(_)))
| Ok(Event::Incoming(Incoming::PubRec(_)))
| Ok(Event::Incoming(Packet::PingReq)) => {}
Ok(m) => info!("Got unhandled message: {m:?}"),
Err(e) => {
error!("Connection error to mqtt: {e:?}")
}
}
}
// let (client, mut connection) = self.create_conection();
// self.handle_loop(&mut connection, strip_tx)
// .map_err(|e| ProgramError::Boxed(Box::new(e)))?;
// Iterate to poll the eventloop for connection progress
// for (i, message) in connection.iter().enumerate() {
// info!("Notification #{i} = {:?}", message);
// match message {
// Ok(Event::Incoming(Incoming::Publish(p))) => {
// info!(
// "Got publish notification. Trying to deserialize: {:#?}",
// serde_json::from_slice::<light::JsonIncoming>(&p.payload)
// )
// }
// Ok(_) => todo!(),
// Err(e) => {
// error!("Connection error to mqtt: {e:?}")
// }
// }
// // if let Ok(Event::Incoming(Incoming::Publish(p))) = message {
// // info!(
// // "Got publish notification. Trying to deserialize: {:#?}",
// // serde_json::from_slice::<light::JsonIncoming>(&p.payload)
// // )
// // }
// }
info!("Done with mqtt");
Ok(())
}
fn handle_incoming_message(&self, publish: Publish) -> Result<()> {
info!("Got incoming message: {publish:?}");
if publish.topic == self.topics.command_topic {
info!("Got command topic");
let command = serde_json::from_slice::<light::JsonIncoming>(&publish.payload)
.context("Deserializing command message")?;
let translated_command = build_strip_tx_msg(&command);
info!("Setting light to state: {:?}", translated_command);
} else if publish.topic == HOMEASSISTANT_TOPIC {
if &publish.payload == homeassistant::STATUS_ONLINE {
info!("Homeassistant is online");
self.send_discovery()?;
} else if &publish.payload == homeassistant::STATUS_OFFLINE {
warn!("Homeassistant is offline");
} else {
anyhow::bail!("Homeassistant status topic {:?} unknown", &publish.payload);
}
} else {
anyhow::bail!("Incoming message has unknown topic: {:?}", publish.topic);
}
Ok(())
}
// fn handle_loop(
// &mut self,
// // client: &Client,
// connection: &mut Connection,
// strip_tx: Sender<strip::Message>,
// ) -> Result<()> {
// // Iterate to poll the eventloop for connection progress
// for (i, message) in connection.iter().enumerate() {
// info!("Notification #{i} = {:?}", message);
// match message {
// Ok(Event::Incoming(Incoming::Publish(p))) => {
// info!(
// "Got publish notification. Trying to deserialize: {:#?}",
// serde_json::from_slice::<light::JsonIncoming>(&p.payload)
// )
// }
// Ok(_) => todo!(),
// Err(e) => {
// error!("Connection error to mqtt: {e:?}")
// }
// }
// todo![]
// }
// Ok(())
// }
/// Called after the initial connection to mqtt
fn init(&mut self) -> Result<()> {
// let topics = Topics::new(self.config);
info!("Subscribing to homeassistant");
// Check if homeassistant is starting or not
self.client
.subscribe(HOMEASSISTANT_TOPIC, QoS::AtMostOnce)
.context("Subscribing to homeassistant status topic")?;
// Check for commands
self.client
.subscribe(&self.topics.command_topic, QoS::AtMostOnce)
.context("Subscribing to command topic")?;
self.send_discovery()?;
Ok(())
}
fn send_discovery(&self) -> Result<()> {
let discovery = self.gen_discovery_message();
info!(
"Sending discovery_message: {:?} (topic: {:?})",
discovery, self.topics.autodiscovery
);
// Send initial autodiscovery
self.client
.publish(
&self.topics.autodiscovery,
QoS::AtLeastOnce,
false,
serde_json::to_vec(&discovery).unwrap(),
)
.context("Sending initial autodiscovery")?;
Ok(())
}
// /// Called for each message
// fn handle_incoming_event(
// &self,
// idx: usize,
// message: Result<Event, ConnectionError>,
// ) -> Result<()> {
// }
/// Generates a discovery message
fn gen_discovery_message(&self) -> light::JsonDiscovery {
// "<discovery_prefix>/device_trigger/[<node_id>/]<object_id>/config",
// homeassistant/device_trigger/0x90fd9ffffedf1266/action_arrow_left_click/config
let discovery = light::JsonDiscovery {
common: Common {
unique_id: Some(self.config.mqtt_id.to_string()),
..Common::default()
},
name: Some(FRIENDLY_NAME.to_string()),
effect_list: Some(vec!["Rainbow".into()]),
brightness: Some(false),
effect: Some(true),
supported_color_modes: Some(vec![light::ColorMode::Rgb]),
state_topic: Some(self.topics.state_topic.clone()),
..light::JsonDiscovery::new(self.topics.command_topic.clone())
};
discovery
}
}
fn build_strip_tx_msg(command: &JsonIncoming) -> Option<strip::Message> {
use strip::Message;
if let Some(state) = &command.state
&& state == LIGHT_OFF
{
return Some(Message::ClearLights);
}
if let Some(effect) = &command.effect
&& effect == "Rainbow"
{
return Some(Message::ChangePattern(Box::new(MovingRainbow::default())));
}
if command.effect.is_none()
&& let Some(color) = &command.color
&& let Some(r) = color.r
&& let Some(g) = color.g
&& let Some(b) = color.b
{
return Some(Message::ChangePattern(Box::new(Solid::new(&SolidParams {
color: Rgb(r as u8, g as u8, b as u8),
}))));
}
error!("Not able to parse input as a command: {command:?}");
None
// ClearLights
// ChangePattern(Box<dyn Pattern + Send + Sync>)
// SetNumLights(u16)
// SetTickTime(u64)
// Quit
}
// enum Status {
// Alive,
// Dead,
// }
// impl ToString for Status {
// fn to_string(&self) -> String {
// match self {
// Self::Alive => String::from("alive"),
// Self::Dead => String::from("dead"),
// }
// }
// }
struct Topics {
autodiscovery: String,
state_topic: String,
command_topic: String,
status_topic: String,
}
impl Topics {
pub fn new(config: &MqttConfig) -> Self {
let mqtt_id = &config.mqtt_id;
Self {
autodiscovery: format!("{}/light/{mqtt_id}/config", config.mqtt_discovery_prefix),
state_topic: format!("{MQTT_PREFIX}/{mqtt_id}/state"),
command_topic: format!("{MQTT_PREFIX}/{mqtt_id}/set"),
status_topic: format!("{MQTT_PREFIX}/{mqtt_id}/status"),
}
}
info!("Done with mqtt");
}
fn gen_discovery_message(config: &MqttConfig) -> (String, light::JsonDiscovery) {
// "<discovery_prefix>/light/[<node_id>/]<object_id>/config",
// homeassistant/light/0x90fd9ffffedf1266/action_arrow_left_click/config
let autodiscovery_topic_name = format!(
"{}/light/{}/config",
config.mqtt_discovery_prefix, config.mqtt_id
);
let discovery = light::JsonDiscovery {
effect: Some(true),
effect_list: Some(vec!["rainbow".into()]),
supported_color_modes: Some(vec![light::ColorMode::Rgb]),
state_topic: Some(format!("aw_lights/{}", config.mqtt_id)),
..light::JsonDiscovery::new(format!("aw_lights/{}/set", config.mqtt_id))
};
// {
// common: Common {
// device: Some(Device {
// identifiers: Some(vec![format!("aw_lights_{}", config.mqtt_id)]),
// ..Device::default()
// }),
// ..Common::default()
// },
// topic: format!("aw_lights/{}/action", config.mqtt_id),
// r#type: "action".to_string(),
// subtype: format!("_click"),
// automation_type: "trigger".to_string(),
// payload: None,
// qos: None,
// value_template: None,
// };
(autodiscovery_topic_name, discovery)
}
// unique_id: bedroom_switch
@ -129,3 +372,42 @@ fn gen_discovery_message(config: &MqttConfig) -> (String, light::JsonDiscovery)
// optimistic: false
// qos: 0
// retain: true
// https://github.com/smrtnt/Open-Home-Automation/blob/master/ha_mqtt_rgbw_light_with_discovery/ha_mqtt_rgbw_light_with_discovery.ino
// On connect:
// JsonObject& root = staticJsonBuffer.createObject();
// root["name"] = FRIENDLY_NAME;
// root["platform"] = "mqtt_json";
// root["state_topic"] = MQTT_STATE_TOPIC;
// root["command_topic"] = MQTT_COMMAND_TOPIC;
// root["brightness"] = true;
// root["rgb"] = true;
// root["white_value"] = true;
// root["color_temp"] = true;
// root["effect"] = true;
// root["effect_list"] = EFFECT_LIST;
// On update
// cmd = CMD_NOT_DEFINED;
// DynamicJsonBuffer dynamicJsonBuffer;
// JsonObject& root = dynamicJsonBuffer.createObject();
// root["state"] = bulb.getState() ? MQTT_STATE_ON_PAYLOAD : MQTT_STATE_OFF_PAYLOAD;
// root["brightness"] = bulb.getBrightness();
// JsonObject& color = root.createNestedObject("color");
// color["r"] = bulb.getColor().red;
// color["g"] = bulb.getColor().green;
// color["b"] = bulb.getColor().blue;
// root["white_value"] = bulb.getColor().white;
// root["color_temp"] = bulb.getColorTemperature();
// Status topic (/status)
// "alive" or "dead"
//#define MQTT_STATE_TOPIC_TEMPLATE "%s/rgbw/state"
// #define MQTT_COMMAND_TOPIC_TEMPLATE "%s/rgbw/set"
// #define MQTT_STATUS_TOPIC_TEMPLATE "%s/rgbw/status" // MQTT connection: alive/dead
// #define MQTT_HOME_ASSISTANT_DISCOVERY_PREFIX "homeassistant"
// #define MQTT_STATE_ON_PAYLOAD "ON"
// #define MQTT_STATE_OFF_PAYLOAD "OFF"

View File

@ -77,7 +77,10 @@ fn main() -> ProgramResult<()> {
// Mqtt user-interface
make_child(
message_tx.clone(),
move |_message_tx| -> ProgramResult<()> { mqtt::start(mqtt_strip_tx, config.mqtt.clone()) },
move |_message_tx| -> ProgramResult<()> {
mqtt::MqttBuilder::new(config.mqtt.clone()).start(mqtt_strip_tx)
// mqtt::start(mqtt_strip_tx, config.mqtt.clone())
},
);
std::mem::drop(message_tx);

View File

@ -15,7 +15,7 @@ fn main() {
},
)
.spawn()
.unwrap()
.expect("Could not spawn npm")
.wait()
.unwrap()
.success();

View File

@ -23,7 +23,7 @@
"morphdom": "git+https://github.com/austenadler/morphdom#fix/input-value-type-change"
},
"devDependencies": {
"webpack-cli": "^4.6.0",
"webpack": "^5.28.0"
"webpack": "^5.28.0",
"webpack-cli": "^4.6.0"
}
}

File diff suppressed because it is too large Load Diff