From 402626e95de08ca96c0ad02d11a42433b6e6fa47 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Tue, 28 Feb 2023 21:07:00 +0000 Subject: [PATCH] Retain holding and param registers --- CHANGELOG.md | 1 + src/coordinator/commands/time_register_ops.rs | 2 ++ src/coordinator/mod.rs | 1 + src/home_assistant.rs | 4 ++++ src/mqtt.rs | 16 ++++++++++++++-- tests/test_config.rs | 3 +++ tests/test_coordinator.rs | 5 +++++ tests/test_home_assistant.rs | 7 +++++++ tests/test_mqtt_message.rs | 16 ++++++++++++---- 9 files changed, 49 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8d2826..79a61af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/coordinator/commands/time_register_ops.rs b/src/coordinator/commands/time_register_ops.rs index c8b9268..d123afa 100644 --- a/src/coordinator/commands/time_register_ops.rs +++ b/src/coordinator/commands/time_register_ops.rs @@ -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); @@ -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); diff --git a/src/coordinator/mod.rs b/src/coordinator/mod.rs index a0d4bcd..83fc8d0 100644 --- a/src/coordinator/mod.rs +++ b/src/coordinator/mod.rs @@ -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() { diff --git a/src/home_assistant.rs b/src/home_assistant.rs index ab50db6..23e353a 100644 --- a/src/home_assistant.rs +++ b/src/home_assistant.rs @@ -227,6 +227,7 @@ impl Config { Ok(mqtt::Message { topic: self.ha_discovery_topic("sensor", name), + retain: true, payload: serde_json::to_string(&config)?, }) } @@ -253,6 +254,7 @@ impl Config { Ok(mqtt::Message { topic: self.ha_discovery_topic("switch", name), + retain: true, payload: serde_json::to_string(&config)?, }) } @@ -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)?, }) } @@ -314,6 +317,7 @@ impl Config { Ok(mqtt::Message { topic: self.ha_discovery_topic("text", name), + retain: true, payload: serde_json::to_string(&config)?, }) } diff --git a/src/mqtt.rs b/src/mqtt.rs index fe62f34..85b6bce 100644 --- a/src/mqtt.rs +++ b/src/mqtt.rs @@ -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, } @@ -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)?, }); } @@ -34,6 +36,7 @@ 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)?, }); @@ -41,6 +44,7 @@ impl Message { 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)?, }); } @@ -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)?, }); } @@ -63,6 +68,7 @@ impl Message { ) -> Result { Ok(mqtt::Message { topic: format!("{}/inputs/all", datalog), + retain: false, payload: serde_json::to_string(&inputs)?, }) } @@ -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)?, }); } @@ -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), @@ -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; } } @@ -368,6 +379,7 @@ impl Mqtt { let message = Message { topic, + retain: publish.retain, payload: String::from_utf8(publish.payload.to_vec())?, }; debug!("RX: {:?}", message); @@ -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)); } diff --git a/tests/test_config.rs b/tests/test_config.rs index 2be1b0c..d57775b 100644 --- a/tests/test_config.rs +++ b/tests/test_config.rs @@ -128,6 +128,7 @@ fn inverters_for_message() { let message = mqtt::Message { topic: "cmd/all/foo".to_string(), + retain: false, payload: "foo".to_string(), }; @@ -136,6 +137,7 @@ fn inverters_for_message() { let message = mqtt::Message { topic: "cmd/MISMATCHED/foo".to_string(), + retain: false, payload: "foo".to_string(), }; @@ -144,6 +146,7 @@ fn inverters_for_message() { let message = mqtt::Message { topic: "cmd/TESTSERIAL/foo".to_string(), + retain: false, payload: "foo".to_string(), }; diff --git a/tests/test_coordinator.rs b/tests/test_coordinator.rs index 2f6fada..1735971 100644 --- a/tests/test_coordinator.rs +++ b/tests/test_coordinator.rs @@ -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() }) ); @@ -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() }) ); @@ -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 @@ -167,6 +170,7 @@ 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() }) ); @@ -174,6 +178,7 @@ async fn complete_path_read_hold_command() { to_mqtt.recv().await?, mqtt::ChannelData::Message(mqtt::Message { topic: "result/2222222222/read/hold/12".to_owned(), + retain: false, payload: "OK".to_owned() }) ); diff --git a/tests/test_home_assistant.rs b/tests/test_home_assistant.rs index 1da4262..e999baf 100644 --- a/tests/test_home_assistant.rs +++ b/tests/test_home_assistant.rs @@ -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() })); } @@ -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() })); } @@ -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() })); } @@ -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() })); } @@ -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() })); } @@ -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() })); } @@ -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() })); } diff --git a/tests/test_mqtt_message.rs b/tests/test_mqtt_message.rs index ac51834..5317f89 100644 --- a/tests/test_mqtt_message.rs +++ b/tests/test_mqtt_message.rs @@ -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() }] ); @@ -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() }] ); @@ -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() } ] ); } @@ -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() } ] ); } @@ -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() }, ] @@ -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() }] ); @@ -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() } ]