Skip to content

Commit

Permalink
Retain holding and param registers (#154)
Browse files Browse the repository at this point in the history
  • Loading branch information
lupine authored Mar 28, 2023
1 parent a62a89e commit dd9118c
Show file tree
Hide file tree
Showing 9 changed files with 49 additions and 6 deletions.
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

0 comments on commit dd9118c

Please sign in to comment.