diff --git a/README.md b/README.md index 3090f0f..d8beabf 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ The following devices have been tested and found to work with this library: Model Code | Device Name | Manufacturer | Type -----------|-------------|--------------|----- 0x649B | RM4 Pro | Broadlink | `Remote` +0x4E2A | [Ande Jupiter+](https://www.myande.pl/service/seria-jupiter-plus/) | ANG Klimatyzacja Sp. z o.o. | `Hvac` ## Setup @@ -90,6 +91,50 @@ remote_device.send_code(&code) .expect("Could not send code!"); ``` +## HVAC + +Starting from version *0.4.0* of this library the HVAC/Air Conditioners support was added. +Supported devices are broadlink device type `0x4E2A`. + +Although it was tested on one specific unit, it is very likely that it should work with more similar devices. +Those air conditioners are usually controlled with +[AC Freedom](https://play.google.com/store/apps/details?id=com.broadlink.acfreedom) application. + +If you have such device connected to the _AC Freedom_ application, then it is surely in a "locked" state +(cannot be controlled using this library). + +You can control it either from _AC Freedom_ or this library (not both). If you decide to use *rbroadlink*, then you +need to delete the device from _AC Freedom_ cloud, then reset the WiFi dongle and re-configure WiFi parameters again. + +You can also head to this post for details: +https://github.com/liaan/broadlink_ac_mqtt/issues/76#issuecomment-884763601 + +Probably configuring the WiFi parameters using this library/rbroadlink-cli should also work (refer to the _Setup_ section above). + +### A sample snippet for setting target temperature setpoint: +```rust +use rbroadlink::Device; + +// Assuming that you have a valid device in `device`... +let hvac_device = match device { + Device::Hvac { hvac } => hvac, + _ => return Err("Not a HVAC device!"), +}; + +// First obtain current state/parameters of the device: +let mut state = hvac_device.get_state().expect("Cannot obtain current state"); +println!("Current state: {:?}", state); + +// Print current temperature and try to set a new setpoint (degree Celsius) +println!("Target temp: {:.1}", state.get_target_temp()); +if let Err(e) = state.set_target_temp(22.0) { + println!("Error setting temperature: {}", e); +} + +// Request to set a new state (with new temperature) +hvac_device.set_state(&mut state); +``` + ## Examples There are a few examples of this library present in the `examples` folder. Refer to diff --git a/examples/README.md b/examples/README.md index 3af89f8..d925508 100644 --- a/examples/README.md +++ b/examples/README.md @@ -98,3 +98,28 @@ mqtt://1.2.3.4:1883 is shown below: ```sh cargo run --example mqtt-broadlink --features mqtt-broadlink -- -c 10.8.0.1 mqtt://1.2.3.4:1883 ``` + +## HVAC client + +This library includes an example HVAC/Air Conditioner client to show how to control supported devices. + +The source can be found [here](hvac-cli.rs) and its usage is shown below: + +```sh +USAGE: + hvac-cli MODE + +MODE: + info show air conditioner state + on power ON air conditioner + off power OFF air conditioner + toggle toggle power state +``` + +Note: by default this client is autodiscovering all devices and it is trying to issue the command on all discovered devices. + +An example of using this client to obtain information of the current state is show below: + +```sh +cargo run --example hvac-cli -- info +``` diff --git a/examples/hvac-cli.rs b/examples/hvac-cli.rs new file mode 100644 index 0000000..0dbb84c --- /dev/null +++ b/examples/hvac-cli.rs @@ -0,0 +1,73 @@ +use rbroadlink::{traits::DeviceTrait, Device}; +use std::env; + +#[derive(PartialEq)] +enum RunMode { + Help, + Info, + Toggle, + TurnOn, + TurnOff, +} + +fn main() { + let argument = env::args().nth(1); + let run_mode = if let Some(arg) = argument { + match &arg[..] { + "info" => RunMode::Info, + "toggle" => RunMode::Toggle, + "on" => RunMode::TurnOn, + "off" => RunMode::TurnOff, + _ => RunMode::Help, + } + } else { + RunMode::Help + }; + + if run_mode == RunMode::Help { + println! {"No arguments given, possible choices:\n"}; + println! {"info show air conditioner state"}; + println! {"on power ON air conditioner"}; + println! {"off power OFF air conditioner"}; + println! {"toggle toggle power state"}; + return; + }; + + println!(">>> autodiscovering broadlink devices..."); + let discovered = Device::list(None).expect("Could not enumerate devices!"); + for device in discovered { + println!(">>> device authentication ..."); + let addr = device.get_info().address; + println!(">>> device at {} => {}", addr, device); + + let hvac = match device { + Device::Hvac { hvac } => hvac, + _ => { + return; + } + }; + if run_mode == RunMode::Info { + println!(">>> get_info"); + let ac_info = hvac.get_info().unwrap(); + println!("Current power state: {}", ac_info.power); + println!("Ambient temperature: {:.1}", ac_info.get_ambient_temp()); + } else { + println!(">>> get_state"); + let mut state = hvac.get_state().unwrap(); + println!("Current state: {:?}", state); + + // Setting desired mode according to command line argument + if run_mode == RunMode::Toggle { + state.power = !state.power; + } else if run_mode == RunMode::TurnOn { + state.power = true; + } else if run_mode == RunMode::TurnOff { + state.power = false; + } + + println!(">>> set_state"); + let response = hvac.set_state(&mut state).unwrap(); + println!(">>> device response {:02x?}", response); + } + } +} diff --git a/src/device.rs b/src/device.rs index ec6e7e5..a12b92b 100644 --- a/src/device.rs +++ b/src/device.rs @@ -224,7 +224,12 @@ fn create_device_from_packet( _ if HVAC_CODES.contains_key(&response.model_code) => Device::Hvac { hvac: HvacDevice::new(name, addr_ip, response), }, - _ => return Err(format!("Unknown device: {}", response.model_code)), + _ => { + return Err(format!( + "Unknown device: {} ({:#06X})", + response.model_code, response.model_code + )) + } }; // Get the auth key for this device diff --git a/src/hvac.rs b/src/hvac.rs index b96a78a..394fac9 100644 --- a/src/hvac.rs +++ b/src/hvac.rs @@ -55,9 +55,9 @@ impl HvacDevice { pub fn get_info(&self) -> Result { let data = self .send_command(&[], HvacDataCommand::GetAcInfo) - .expect("Could not obtain AC info from device!"); - let info = - AirCondInfo::unpack_from_slice(&data).expect("Could not unpack command from bytes!"); + .map_err(|e| format!("Could not obtain AC info from device! {}", e))?; + let info = AirCondInfo::unpack_from_slice(&data) + .map_err(|e| format!("Could not unpack command from bytes! {}", e))?; return Ok(info); } @@ -66,19 +66,19 @@ impl HvacDevice { pub fn get_state(&self) -> Result { let data = self .send_command(&[], HvacDataCommand::GetState) - .expect("Could not obtain AC state from device!"); - let state = - AirCondState::unpack_from_slice(&data).expect("Could not unpack command from bytes!"); + .map_err(|e| format!("Could not obtain AC state from device! {}", e))?; + let state = AirCondState::unpack_from_slice(&data) + .map_err(|e| format!("Could not unpack command from bytes! {}", e))?; return Ok(state); } /// Set new air conditioner state based on passed structure. pub fn set_state(&self, state: &mut AirCondState) -> Result, String> { - let payload = state.prepare_and_pack().expect("Could not pack message"); - let response = self - .send_command(&payload, HvacDataCommand::SetState) - .unwrap(); + let payload = state + .prepare_and_pack() + .map_err(|e| format!("Could not pack message! {}", e))?; + let response = self.send_command(&payload, HvacDataCommand::SetState)?; return Ok(response); } @@ -98,11 +98,11 @@ impl HvacDevice { let msg = HvacDataMessage::new(command); let packed = msg .pack_with_payload(&payload) - .expect("Could not pack HVAC data message!"); + .map_err(|e| format!("Could not pack HVAC data message! {}", e))?; let response = generic_device .send_command::(&packed) - .expect("Could not send command!"); + .map_err(|e| format!("Could not send command! {}", e))?; // TODO: check if there is some relation between // msg.command and the same return field from the response diff --git a/src/network/command.rs b/src/network/command.rs index 4ce7148..622a680 100644 --- a/src/network/command.rs +++ b/src/network/command.rs @@ -145,7 +145,7 @@ impl CommandMessage { let real_checksum = checksum(&bytes); if command_header.checksum != real_checksum { return Err(format!( - "Command checksum does not match actual checksum! Expected {} got {}", + "Command checksum does not match actual checksum! Expected {:#06X} got {:#06X}", real_checksum, command_header.checksum, )); } @@ -162,7 +162,7 @@ impl CommandMessage { let real_checksum = checksum(&decrypted); if command_header.payload_checksum != real_checksum { return Err(format!( - "Payload checksum does not match actual checksum! Expected {} got {}", + "Payload checksum does not match actual checksum! Expected {:#06X} got {:#06X}", real_checksum, command_header.payload_checksum, )); } diff --git a/src/network/hvac_data.rs b/src/network/hvac_data.rs index d34e66a..e491991 100644 --- a/src/network/hvac_data.rs +++ b/src/network/hvac_data.rs @@ -135,7 +135,10 @@ impl AirCondState { // set magic values before sending self.magic1 = 0x0f.into(); - Ok(self.pack().expect("Could not pack message!").to_vec()) + Ok(self + .pack() + .map_err(|e| format!("Could not pack message! {}", e))? + .to_vec()) } /// Calculate final temperature value from internal partial fields. @@ -216,17 +219,20 @@ impl HvacDataMessage { /// Pack the HvacDataMessage with an associated payload. pub fn pack_with_payload(mut self, payload: &[u8]) -> Result, String> { // Calculate tyhe length of the payload - self.data_length += - >::try_into(payload.len()).expect("Payload is too long!"); + self.data_length += >::try_into(payload.len()) + .map_err(|e| format!("Payload is too long! {}", e))?; // Add 10 bytes for the header self.payload_length = self .data_length .checked_add(10u16) - .expect("Could not add the start buffer! Payload is too long"); + .ok_or_else(|| "Could not add the start buffer! Payload is too long")?; // Append the payload to the header - let mut result = self.pack().expect("Could not pack message!").to_vec(); + let mut result = self + .pack() + .map_err(|e| format!("Could not pack message! {}", e))? + .to_vec(); result.extend(payload); // Compute and add the final payload checksum @@ -240,14 +246,14 @@ impl HvacDataMessage { pub fn unpack_with_payload(bytes: &[u8]) -> Result, String> { // Unpack the header let command_header = HvacDataMessage::unpack_from_slice(&bytes[0..12]) - .expect("Could not unpack command from bytes!"); + .map_err(|e| format!("Could not unpack command from bytes! {}", e))?; // Check total payload length: // get real size and substract 2 bytes length field for correct comparision let real_size: u16 = (bytes.len() as u16) - 2; if real_size != command_header.payload_length { return Err(format!( - "Command checksum does not match actual checksum! Expected {} got {}", + "Command checksum does not match actual checksum! Expected {:#06X} got {:#06X}", command_header.payload_length, real_size, )); } @@ -258,7 +264,7 @@ impl HvacDataMessage { let real_checksum = compute_generic_checksum(&bytes[0x02..crc_offset]); if data_crc != real_checksum { return Err(format!( - "Data checksum does not match actual checksum! Expected {} got {}", + "Data checksum does not match actual checksum! Expected {:#06X} got {:#06X}", data_crc, real_checksum, )); }