Skip to content
This repository has been archived by the owner on Jun 6, 2024. It is now read-only.

Retain holding and parameter registers #154

Merged
merged 1 commit into from
Mar 28, 2023
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* Fix crash in timesync during DST transition times (#153)
* Add option to send holding registers on startup (#147, @lupine)
* Add HomeAssistant time control discovery messages (#143, @lupine)
* Retain holding and parameter register messages (#154, @lupine)


# 0.9.0 - 2nd November 2022
Expand Down
2 changes: 2 additions & 0 deletions src/coordinator/commands/time_register_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ impl ReadTimeRegister {
};
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);
Expand Down Expand Up @@ -142,6 +143,7 @@ impl SetTimeRegister {
};
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);
Expand Down
1 change: 1 addition & 0 deletions src/coordinator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ impl Coordinator {

let reply = mqtt::ChannelData::Message(mqtt::Message {
topic: topic_reply,
retain: false,
payload: if result.is_ok() { "OK" } else { "FAIL" }.to_string(),
});
if self.channels.to_mqtt.send(reply).is_err() {
Expand Down
4 changes: 4 additions & 0 deletions src/home_assistant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ impl Config {

Ok(mqtt::Message {
topic: self.ha_discovery_topic("sensor", name),
retain: true,
payload: serde_json::to_string(&config)?,
})
}
Expand All @@ -253,6 +254,7 @@ impl Config {

Ok(mqtt::Message {
topic: self.ha_discovery_topic("switch", name),
retain: true,
payload: serde_json::to_string(&config)?,
})
}
Expand Down Expand Up @@ -284,6 +286,7 @@ impl Config {

Ok(mqtt::Message {
topic: self.ha_discovery_topic("number", &format!("{:?}", register)),
retain: true,
payload: serde_json::to_string(&config)?,
})
}
Expand Down Expand Up @@ -314,6 +317,7 @@ impl Config {

Ok(mqtt::Message {
topic: self.ha_discovery_topic("text", name),
retain: true,
payload: serde_json::to_string(&config)?,
})
}
Expand Down
16 changes: 14 additions & 2 deletions src/mqtt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use rumqttc::{AsyncClient, Event, EventLoop, Incoming, LastWill, MqttOptions, Pu
#[derive(Eq, PartialEq, Debug, Clone)]
pub struct Message {
pub topic: String,
pub retain: bool,
pub payload: String,
}

Expand All @@ -21,6 +22,7 @@ impl Message {
for (register, value) in rp.pairs() {
r.push(mqtt::Message {
topic: format!("{}/param/{}", rp.datalog, register),
retain: true,
payload: serde_json::to_string(&value)?,
});
}
Expand All @@ -34,13 +36,15 @@ impl Message {
for (register, value) in td.pairs() {
r.push(mqtt::Message {
topic: format!("{}/hold/{}", td.datalog, register),
retain: true,
payload: serde_json::to_string(&value)?,
});

if register == 21 {
let bits = lxp::packet::Register21Bits::new(value);
r.push(mqtt::Message {
topic: format!("{}/hold/{}/bits", td.datalog, register),
retain: true,
payload: serde_json::to_string(&bits)?,
});
}
Expand All @@ -49,6 +53,7 @@ impl Message {
let bits = lxp::packet::Register110Bits::new(value);
r.push(mqtt::Message {
topic: format!("{}/hold/{}/bits", td.datalog, register),
retain: true,
payload: serde_json::to_string(&bits)?,
});
}
Expand All @@ -63,6 +68,7 @@ impl Message {
) -> Result<Message> {
Ok(mqtt::Message {
topic: format!("{}/inputs/all", datalog),
retain: false,
payload: serde_json::to_string(&inputs)?,
})
}
Expand All @@ -79,6 +85,7 @@ impl Message {
for (register, value) in td.pairs() {
r.push(mqtt::Message {
topic: format!("{}/input/{}", td.datalog, register),
retain: false,
payload: serde_json::to_string(&value)?,
});
}
Expand All @@ -87,18 +94,22 @@ impl Message {
match td.read_input() {
Ok(ReadInput::ReadInputAll(r_all)) => r.push(mqtt::Message {
topic: format!("{}/inputs/all", td.datalog),
retain: false,
payload: serde_json::to_string(&r_all)?,
}),
Ok(ReadInput::ReadInput1(r1)) => r.push(mqtt::Message {
topic: format!("{}/inputs/1", td.datalog),
retain: false,
payload: serde_json::to_string(&r1)?,
}),
Ok(ReadInput::ReadInput2(r2)) => r.push(mqtt::Message {
topic: format!("{}/inputs/2", td.datalog),
retain: false,
payload: serde_json::to_string(&r2)?,
}),
Ok(ReadInput::ReadInput3(r3)) => r.push(mqtt::Message {
topic: format!("{}/inputs/3", td.datalog),
retain: false,
payload: serde_json::to_string(&r3)?,
}),
Err(x) => warn!("ignoring {:?}", x),
Expand Down Expand Up @@ -322,7 +333,7 @@ impl Mqtt {
let ha = home_assistant::Config::new(&inverter, &self.config.mqtt());
for msg in ha.all()?.into_iter() {
let _ = client
.publish(&msg.topic, QoS::AtLeastOnce, true, msg.payload)
.publish(&msg.topic, QoS::AtLeastOnce, msg.retain, msg.payload)
.await;
}
}
Expand Down Expand Up @@ -368,6 +379,7 @@ impl Mqtt {

let message = Message {
topic,
retain: publish.retain,
payload: String::from_utf8(publish.payload.to_vec())?,
};
debug!("RX: {:?}", message);
Expand Down Expand Up @@ -396,7 +408,7 @@ impl Mqtt {
let topic = format!("{}/{}", self.config.mqtt().namespace(), message.topic);
info!("publishing: {} = {}", topic, message.payload);
let _ = client
.publish(&topic, QoS::AtLeastOnce, false, message.payload)
.publish(&topic, QoS::AtLeastOnce, message.retain, message.payload)
.await
.map_err(|err| error!("publish {} failed: {:?} .. skipping", topic, err));
}
Expand Down
3 changes: 3 additions & 0 deletions tests/test_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ fn inverters_for_message() {

let message = mqtt::Message {
topic: "cmd/all/foo".to_string(),
retain: false,
payload: "foo".to_string(),
};

Expand All @@ -136,6 +137,7 @@ fn inverters_for_message() {

let message = mqtt::Message {
topic: "cmd/MISMATCHED/foo".to_string(),
retain: false,
payload: "foo".to_string(),
};

Expand All @@ -144,6 +146,7 @@ fn inverters_for_message() {

let message = mqtt::Message {
topic: "cmd/TESTSERIAL/foo".to_string(),
retain: false,
payload: "foo".to_string(),
};

Expand Down
5 changes: 5 additions & 0 deletions tests/test_coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ async fn publishes_read_hold_mqtt() {
to_mqtt.recv().await?,
mqtt::ChannelData::Message(mqtt::Message {
topic: format!("{}/hold/12", inverter.datalog()),
retain: true,
payload: "1558".to_owned()
})
);
Expand Down Expand Up @@ -89,6 +90,7 @@ async fn handles_read_input_all() {
to_mqtt.recv().await?,
mqtt::ChannelData::Message(mqtt::Message {
topic: format!("{}/inputs/all", inverter.datalog()),
retain: false,
payload: "{\"status\":257,\"v_pv_1\":25.7,\"v_pv_2\":25.7,\"v_pv_3\":25.7,\"v_bat\":25.7,\"soc\":1,\"soh\":1,\"p_pv\":771,\"p_pv_1\":257,\"p_pv_2\":257,\"p_pv_3\":257,\"p_charge\":257,\"p_discharge\":257,\"v_ac_r\":25.7,\"v_ac_s\":25.7,\"v_ac_t\":25.7,\"f_ac\":2.57,\"p_inv\":257,\"p_rec\":257,\"pf\":0.257,\"v_eps_r\":25.7,\"v_eps_s\":25.7,\"v_eps_t\":25.7,\"f_eps\":2.57,\"p_eps\":257,\"s_eps\":257,\"p_to_grid\":257,\"p_to_user\":257,\"e_pv_day\":77.1,\"e_pv_day_1\":25.7,\"e_pv_day_2\":25.7,\"e_pv_day_3\":25.7,\"e_inv_day\":25.7,\"e_rec_day\":25.7,\"e_chg_day\":25.7,\"e_dischg_day\":25.7,\"e_eps_day\":25.7,\"e_to_grid_day\":25.7,\"e_to_user_day\":25.7,\"v_bus_1\":25.7,\"v_bus_2\":25.7,\"e_pv_all\":5052902.699999999,\"e_pv_all_1\":1684300.9,\"e_pv_all_2\":1684300.9,\"e_pv_all_3\":1684300.9,\"e_inv_all\":1684300.9,\"e_rec_all\":1684300.9,\"e_chg_all\":1684300.9,\"e_dischg_all\":1684300.9,\"e_eps_all\":1684300.9,\"e_to_grid_all\":1684300.9,\"e_to_user_all\":1684300.9,\"t_inner\":257,\"t_rad_1\":257,\"t_rad_2\":257,\"t_bat\":257,\"runtime\":16843009,\"max_chg_curr\":2.57,\"max_dischg_curr\":2.57,\"charge_volt_ref\":25.7,\"dischg_cut_volt\":25.7,\"bat_status_0\":257,\"bat_status_1\":257,\"bat_status_2\":257,\"bat_status_3\":257,\"bat_status_4\":257,\"bat_status_5\":257,\"bat_status_6\":257,\"bat_status_7\":257,\"bat_status_8\":257,\"bat_status_9\":257,\"bat_status_inv\":257,\"bat_count\":257,\"bat_capacity\":257,\"bat_current\":2.57,\"bms_event_1\":257,\"bms_event_2\":257,\"max_cell_voltage\":2.57,\"min_cell_voltage\":2.57,\"max_cell_temp\":2.57,\"min_cell_temp\":2.57,\"bms_fw_update_state\":257,\"cycle_count\":257,\"vbat_inv\":25.7,\"time\":1646370367,\"datalog\":\"2222222222\"}".to_owned()
})
);
Expand Down Expand Up @@ -129,6 +131,7 @@ async fn complete_path_read_hold_command() {
// mqtt incoming "read this hold" command
let message = mqtt::Message {
topic: "cmd/all/read/hold/12".to_owned(),
retain: false,
payload: "".to_owned(),
};
channels
Expand Down Expand Up @@ -167,13 +170,15 @@ async fn complete_path_read_hold_command() {
to_mqtt.recv().await?,
mqtt::ChannelData::Message(mqtt::Message {
topic: "2222222222/hold/12".to_owned(),
retain: true,
payload: "1558".to_owned()
})
);
assert_eq!(
to_mqtt.recv().await?,
mqtt::ChannelData::Message(mqtt::Message {
topic: "result/2222222222/read/hold/12".to_owned(),
retain: false,
payload: "OK".to_owned()
})
);
Expand Down
7 changes: 7 additions & 0 deletions tests/test_home_assistant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ async fn all_has_soc() {
assert!(r.is_ok());
assert!(r.unwrap().contains(&mqtt::Message {
topic: "homeassistant/sensor/lxp_2222222222/soc/config".to_string(),
retain: true,
payload: "{\"device_class\":\"battery\",\"name\":\"Battery Percentage\",\"state_topic\":\"lxp/2222222222/inputs/all\",\"state_class\":\"measurement\",\"value_template\":\"{{ value_json.soc }}\",\"unit_of_measurement\":\"%\",\"unique_id\":\"lxp_2222222222_soc\",\"device\":{\"manufacturer\":\"LuxPower\",\"name\":\"lxp_2222222222\",\"identifiers\":[\"lxp_2222222222\"]},\"availability\":{\"topic\":\"lxp/LWT\"}}".to_string()
}));
}
Expand All @@ -25,6 +26,7 @@ async fn all_has_v_pv_1() {
assert!(r.is_ok());
assert!(r.unwrap().contains(&mqtt::Message {
topic: "homeassistant/sensor/lxp_2222222222/v_pv_1/config".to_string(),
retain: true,
payload: "{\"device_class\":\"voltage\",\"name\":\"Voltage (PV String 1)\",\"state_topic\":\"lxp/2222222222/inputs/all\",\"state_class\":\"measurement\",\"value_template\":\"{{ value_json.v_pv_1 }}\",\"unit_of_measurement\":\"V\",\"unique_id\":\"lxp_2222222222_v_pv_1\",\"device\":{\"manufacturer\":\"LuxPower\",\"name\":\"lxp_2222222222\",\"identifiers\":[\"lxp_2222222222\"]},\"availability\":{\"topic\":\"lxp/LWT\"}}".to_string()
}));
}
Expand All @@ -39,6 +41,7 @@ async fn all_has_p_pv() {
assert!(r.is_ok());
assert!(r.unwrap().contains(&mqtt::Message {
topic: "homeassistant/sensor/lxp_2222222222/p_pv/config".to_string(),
retain: true,
payload: "{\"device_class\":\"power\",\"name\":\"Power (PV Array)\",\"state_topic\":\"lxp/2222222222/inputs/all\",\"state_class\":\"measurement\",\"value_template\":\"{{ value_json.p_pv }}\",\"unit_of_measurement\":\"W\",\"unique_id\":\"lxp_2222222222_p_pv\",\"device\":{\"manufacturer\":\"LuxPower\",\"name\":\"lxp_2222222222\",\"identifiers\":[\"lxp_2222222222\"]},\"availability\":{\"topic\":\"lxp/LWT\"}}".to_string()
}));
}
Expand All @@ -53,6 +56,7 @@ async fn all_has_e_pv_all() {
assert!(r.is_ok());
assert!(r.unwrap().contains(&mqtt::Message {
topic: "homeassistant/sensor/lxp_2222222222/e_pv_all/config".to_string(),
retain: true,
payload: "{\"device_class\":\"energy\",\"name\":\"PV Generation (All time)\",\"state_topic\":\"lxp/2222222222/inputs/all\",\"state_class\":\"total_increasing\",\"value_template\":\"{{ value_json.e_pv_all }}\",\"unit_of_measurement\":\"kWh\",\"unique_id\":\"lxp_2222222222_e_pv_all\",\"device\":{\"manufacturer\":\"LuxPower\",\"name\":\"lxp_2222222222\",\"identifiers\":[\"lxp_2222222222\"]},\"availability\":{\"topic\":\"lxp/LWT\"}}".to_string()
}));
}
Expand All @@ -67,6 +71,7 @@ async fn all_has_switch_ac_charge() {
assert!(r.is_ok());
assert!(r.unwrap().contains(&mqtt::Message {
topic: "homeassistant/switch/lxp_2222222222/ac_charge/config".to_string(),
retain: true,
payload: "{\"name\":\"AC Charge\",\"state_topic\":\"lxp/2222222222/hold/21/bits\",\"command_topic\":\"lxp/cmd/2222222222/set/ac_charge\",\"value_template\":\"{{ value_json.ac_charge_en }}\",\"unique_id\":\"lxp_2222222222_ac_charge\",\"device\":{\"manufacturer\":\"LuxPower\",\"name\":\"lxp_2222222222\",\"identifiers\":[\"lxp_2222222222\"]},\"availability\":{\"topic\":\"lxp/LWT\"}}".to_string()
}));
}
Expand All @@ -81,6 +86,7 @@ async fn all_has_number_ac_charge_soc_limit_pct() {
assert!(r.is_ok());
assert!(r.unwrap().contains(&mqtt::Message {
topic: "homeassistant/number/lxp_2222222222/AcChargeSocLimit/config".to_string(),
retain: true,
payload: "{\"name\":\"AC Charge Limit %\",\"state_topic\":\"lxp/2222222222/hold/67\",\"command_topic\":\"lxp/cmd/2222222222/set/hold/67\",\"value_template\":\"{{ float(value) }}\",\"unique_id\":\"lxp_2222222222_number_AcChargeSocLimit\",\"device\":{\"manufacturer\":\"LuxPower\",\"name\":\"lxp_2222222222\",\"identifiers\":[\"lxp_2222222222\"]},\"availability\":{\"topic\":\"lxp/LWT\"},\"min\":0.0,\"max\":100.0,\"step\":1.0,\"unit_of_measurement\":\"%\"}".to_string()
}));
}
Expand All @@ -95,6 +101,7 @@ async fn all_has_time_range_ac_charge_1() {
assert!(r.is_ok());
assert!(r.unwrap().contains(&mqtt::Message {
topic: "homeassistant/text/lxp_2222222222/ac_charge_1/config".to_string(),
retain: true,
payload: r#"{"name":"AC Charge Timeslot 1","state_topic":"lxp/2222222222/ac_charge/1","command_topic":"lxp/cmd/2222222222/set/ac_charge/1","command_template":"{% set parts = value.split(\"-\") %}{\"start\":\"{{ parts[0] }}\", \"end\":\"{{ parts[1] }}\"}","value_template":"{{ value_json[\"start\"] }}-{{ value_json[\"end\"] }}","unique_id":"lxp_2222222222_text_ac_charge/1","device":{"manufacturer":"LuxPower","name":"lxp_2222222222","identifiers":["lxp_2222222222"]},"availability":{"topic":"lxp/LWT"},"pattern":"([01]?[0-9]|2[0-3]):[0-5][0-9]-([01]?[0-9]|2[0-3]):[0-5][0-9]"}"#.to_string()
}));
}
16 changes: 12 additions & 4 deletions tests/test_mqtt_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ async fn for_param() {
mqtt::Message::for_param(packet).unwrap(),
vec![mqtt::Message {
topic: "2222222222/param/0".to_owned(),
retain: true,
payload: "1".to_owned()
}]
);
Expand All @@ -40,6 +41,7 @@ async fn for_hold_single() {
mqtt::Message::for_hold(packet).unwrap(),
vec![mqtt::Message {
topic: "2222222222/hold/0".to_owned(),
retain: true,
payload: "1".to_owned()
}]
);
Expand All @@ -61,8 +63,8 @@ async fn for_hold_21() {

assert_eq!(
mqtt::Message::for_hold(packet).unwrap(),
vec![mqtt::Message { topic: "2222222222/hold/21".to_owned(), payload: "8716".to_owned() },
mqtt::Message { topic: "2222222222/hold/21/bits".to_owned(), payload: "{\"eps_en\":\"OFF\",\"ovf_load_derate_en\":\"OFF\",\"drms_en\":\"ON\",\"lvrt_en\":\"ON\",\"anti_island_en\":\"OFF\",\"neutral_detect_en\":\"OFF\",\"grid_on_power_ss_en\":\"OFF\",\"ac_charge_en\":\"OFF\",\"sw_seamless_en\":\"OFF\",\"set_to_standby\":\"ON\",\"forced_discharge_en\":\"OFF\",\"charge_priority_en\":\"OFF\",\"iso_en\":\"OFF\",\"gfci_en\":\"ON\",\"dci_en\":\"OFF\",\"feed_in_grid_en\":\"OFF\"}".to_owned() }
vec![mqtt::Message { topic: "2222222222/hold/21".to_owned(), retain: true, payload: "8716".to_owned() },
mqtt::Message { topic: "2222222222/hold/21/bits".to_owned(), retain: true, payload: "{\"eps_en\":\"OFF\",\"ovf_load_derate_en\":\"OFF\",\"drms_en\":\"ON\",\"lvrt_en\":\"ON\",\"anti_island_en\":\"OFF\",\"neutral_detect_en\":\"OFF\",\"grid_on_power_ss_en\":\"OFF\",\"ac_charge_en\":\"OFF\",\"sw_seamless_en\":\"OFF\",\"set_to_standby\":\"ON\",\"forced_discharge_en\":\"OFF\",\"charge_priority_en\":\"OFF\",\"iso_en\":\"OFF\",\"gfci_en\":\"ON\",\"dci_en\":\"OFF\",\"feed_in_grid_en\":\"OFF\"}".to_owned() }
]
);
}
Expand All @@ -83,8 +85,8 @@ async fn for_hold_110() {

assert_eq!(
mqtt::Message::for_hold(packet).unwrap(),
vec![mqtt::Message { topic: "2222222222/hold/110".to_owned(), payload: "1033".to_owned() },
mqtt::Message { topic: "2222222222/hold/110/bits".to_owned(), payload: "{\"ub_pv_grid_off_en\":\"ON\",\"ub_run_without_grid\":\"OFF\",\"ub_micro_grid_en\":\"OFF\"}".to_owned() }
vec![mqtt::Message { topic: "2222222222/hold/110".to_owned(), retain: true, payload: "1033".to_owned() },
mqtt::Message { topic: "2222222222/hold/110/bits".to_owned(), retain: true, payload: "{\"ub_pv_grid_off_en\":\"ON\",\"ub_run_without_grid\":\"OFF\",\"ub_micro_grid_en\":\"OFF\"}".to_owned() }
]
);
}
Expand All @@ -108,14 +110,17 @@ async fn for_hold_multi() {
vec![
mqtt::Message {
topic: "2222222222/hold/12".to_owned(),
retain: true,
payload: "1558".to_owned()
},
mqtt::Message {
topic: "2222222222/hold/13".to_owned(),
retain: true,
payload: "2055".to_owned()
},
mqtt::Message {
topic: "2222222222/hold/14".to_owned(),
retain: true,
payload: "9".to_owned()
},
]
Expand All @@ -141,6 +146,7 @@ async fn for_input() {
mqtt::Message::for_input(packet, false).unwrap(),
vec![mqtt::Message {
topic: "2222222222/inputs/1".to_owned(),
retain: false,
payload: "{\"status\":0,\"v_pv_1\":0.0,\"v_pv_2\":0.0,\"v_pv_3\":0.0,\"v_bat\":0.0,\"soc\":0,\"soh\":0,\"p_pv\":0,\"p_pv_1\":0,\"p_pv_2\":0,\"p_pv_3\":0,\"p_charge\":0,\"p_discharge\":0,\"v_ac_r\":0.0,\"v_ac_s\":0.0,\"v_ac_t\":0.0,\"f_ac\":0.0,\"p_inv\":0,\"p_rec\":0,\"pf\":0.0,\"v_eps_r\":0.0,\"v_eps_s\":0.0,\"v_eps_t\":0.0,\"f_eps\":0.0,\"p_eps\":0,\"s_eps\":0,\"p_to_grid\":0,\"p_to_user\":0,\"e_pv_day\":0.0,\"e_pv_day_1\":0.0,\"e_pv_day_2\":0.0,\"e_pv_day_3\":0.0,\"e_inv_day\":0.0,\"e_rec_day\":0.0,\"e_chg_day\":0.0,\"e_dischg_day\":0.0,\"e_eps_day\":0.0,\"e_to_grid_day\":0.0,\"e_to_user_day\":0.0,\"v_bus_1\":0.0,\"v_bus_2\":0.0,\"time\":1646370367,\"datalog\":\"2222222222\"}".to_owned()
}]
);
Expand All @@ -158,10 +164,12 @@ async fn for_input() {
vec![
mqtt::Message {
topic: "2222222222/input/0".to_owned(),
retain: false,
payload: "0".to_owned()
},
mqtt::Message {
topic: "2222222222/input/1".to_owned(),
retain: false,
payload: "0".to_owned()
}
]
Expand Down