Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ serde_json = "~1"
serde_yaml = "~0.9"
tokio = { version = "~1", features = ["net", "macros", "signal"] }
tokio-util = { version = "~0.7", features = ["codec"] }
chrono = "~0.4"
chrono = { version = "~0.4", features = ["serde"] }
cron-parser = "~0.7"
enum_dispatch = "~0.3"
async-trait = "~0.1"
reqwest = "~0.11"
rinfluxdb = { version = "~0.1", git = "https://gitlab.com/celsworth/rinfluxdb.git", rev = "f3f5b23e" }
sqlx = { version = "~0.6", features = ["runtime-tokio-native-tls", "any", "postgres", "mysql", "sqlite", "chrono"] }
url = "~2.4"
2 changes: 1 addition & 1 deletion src/command.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::prelude::*;

#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum Command {
ReadInputs(config::Inverter, u16),
ReadInput(config::Inverter, u16, u16),
Expand Down
68 changes: 64 additions & 4 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
use crate::prelude::*;

use serde::Deserialize;
use serde_with::serde_as; //, OneOrMany;
use serde_with::serde_as;
use serde_yaml;

#[serde_as]
#[derive(Clone, Debug, Deserialize)]
pub struct Config {
pub inverters: Vec<Inverter>,
//#[serde_as(deserialize_as = "OneOrMany<_>")]
pub mqtt: Mqtt,
//#[serde_as(deserialize_as = "OneOrMany<_>")]
pub influx: Influx,
#[serde(default = "Vec::new")]
pub databases: Vec<Database>,
Expand Down Expand Up @@ -336,7 +335,68 @@ impl Config {
let content = std::fs::read_to_string(&file)
.map_err(|err| anyhow!("error reading {}: {}", file, err))?;

Ok(serde_yaml::from_str(&content)?)
let config: Self = serde_yaml::from_str(&content)?;
config.validate()?;
Ok(config)
}

fn validate(&self) -> Result<()> {
// Validate MQTT configuration
if self.mqtt.enabled {
if self.mqtt.port == 0 {
bail!("mqtt.port must be between 1 and 65535");
}
if self.mqtt.host.is_empty() {
return Err(anyhow!("MQTT host cannot be empty"));
}
}

// Validate InfluxDB configuration
if self.influx.enabled {
if let Err(e) = url::Url::parse(&self.influx.url) {
return Err(anyhow!("Invalid InfluxDB URL: {}", e));
}
if self.influx.database.is_empty() {
return Err(anyhow!("InfluxDB database name cannot be empty"));
}
}

// Validate database URLs
for db in &self.databases {
if db.enabled {
if let Err(e) = url::Url::parse(db.url()) {
return Err(anyhow!("Invalid database URL: {}", e));
}
}
}

// Validate inverter configurations
for (i, inv) in self.inverters.iter().enumerate() {
if inv.enabled {
if inv.port == 0 {
bail!("inverter[{}].port must be between 1 and 65535", i);
}
if inv.host.is_empty() {
return Err(anyhow!("Inverter host cannot be empty"));
}
if inv.read_timeout.unwrap_or(900) == 0 {
return Err(anyhow!("Invalid read timeout: 0"));
}
}
}

// Validate scheduler configuration
if let Some(scheduler) = &self.scheduler {
if scheduler.enabled {
if let Some(cron) = &scheduler.timesync_cron {
if cron.is_empty() {
return Err(anyhow!("Scheduler cron expression cannot be empty"));
}
}
}
}

Ok(())
}

fn default_mqtt_port() -> u16 {
Expand Down
67 changes: 38 additions & 29 deletions src/coordinator/commands/time_register_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use serde::Serialize;
pub struct ReadTimeRegister {
channels: Channels,
inverter: config::Inverter,
config: ConfigWrapper,
action: Action,
}

Expand Down Expand Up @@ -59,10 +60,11 @@ impl Action {
}

impl ReadTimeRegister {
pub fn new(channels: Channels, inverter: config::Inverter, action: Action) -> Self {
pub fn new(channels: Channels, inverter: config::Inverter, config: ConfigWrapper, action: Action) -> Self {
Self {
channels,
inverter,
config,
action,
}
}
Expand Down Expand Up @@ -90,19 +92,22 @@ impl ReadTimeRegister {
let reply = receiver.wait_for_reply(&packet).await?;

if let Packet::TranslatedData(td) = reply {
let payload = MqttReplyPayload {
start: format!("{:02}:{:02}", td.values[0], td.values[1]),
end: format!("{:02}:{:02}", td.values[2], td.values[3]),
};
let message = mqtt::Message {
topic: self.action.mqtt_reply_topic(td.datalog),
retain: true,
payload: serde_json::to_string(&payload)?,
};
let channel_data = mqtt::ChannelData::Message(message);

if self.channels.to_mqtt.send(channel_data).is_err() {
bail!("send(to_mqtt) failed - channel closed?");
// Only send MQTT message if MQTT is enabled
if self.config.mqtt().enabled() {
let payload = MqttReplyPayload {
start: format!("{:02}:{:02}", td.values[0], td.values[1]),
end: format!("{:02}:{:02}", td.values[2], td.values[3]),
};
let message = mqtt::Message {
topic: self.action.mqtt_reply_topic(td.datalog),
retain: true,
payload: serde_json::to_string(&payload)?,
};
let channel_data = mqtt::ChannelData::Message(message);

if self.channels.to_mqtt.send(channel_data).is_err() {
bail!("send(to_mqtt) failed - channel closed?");
}
}

Ok(())
Expand All @@ -115,6 +120,7 @@ impl ReadTimeRegister {
pub struct SetTimeRegister {
channels: Channels,
inverter: config::Inverter,
config: ConfigWrapper,
action: Action,
values: [u8; 4],
}
Expand All @@ -123,12 +129,14 @@ impl SetTimeRegister {
pub fn new(
channels: Channels,
inverter: config::Inverter,
config: ConfigWrapper,
action: Action,
values: [u8; 4],
) -> Self {
Self {
channels,
inverter,
config,
action,
values,
}
Expand All @@ -140,21 +148,22 @@ impl SetTimeRegister {
self.set_register(self.action.register()? + 1, &self.values[2..4])
.await?;

// FIXME: If we only update one of the two registers, we should probably
// still output the change we did manage to make here.
let payload = MqttReplyPayload {
start: format!("{:02}:{:02}", self.values[0], self.values[1]),
end: format!("{:02}:{:02}", self.values[2], self.values[3]),
};
let message = mqtt::Message {
topic: self.action.mqtt_reply_topic(self.inverter.datalog),
retain: true,
payload: serde_json::to_string(&payload)?,
};
let channel_data = mqtt::ChannelData::Message(message);

if self.channels.to_mqtt.send(channel_data).is_err() {
bail!("send(to_mqtt) failed - channel closed?");
// Only send MQTT message if MQTT is enabled
if self.config.mqtt().enabled() {
let payload = MqttReplyPayload {
start: format!("{:02}:{:02}", self.values[0], self.values[1]),
end: format!("{:02}:{:02}", self.values[2], self.values[3]),
};
let message = mqtt::Message {
topic: self.action.mqtt_reply_topic(self.inverter.datalog),
retain: true,
payload: serde_json::to_string(&payload)?,
};
let channel_data = mqtt::ChannelData::Message(message);

if self.channels.to_mqtt.send(channel_data).is_err() {
bail!("send(to_mqtt) failed - channel closed?");
}
}

Ok(())
Expand Down
Loading