Skip to content

Commit

Permalink
Merge pull request #6 from manio/hvac_updates
Browse files Browse the repository at this point in the history
Hvac updates
  • Loading branch information
nicholascioli authored Nov 4, 2023
2 parents 0e02e62 + 998cbdd commit c77bb31
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 23 deletions.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
73 changes: 73 additions & 0 deletions examples/hvac-cli.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
7 changes: 6 additions & 1 deletion src/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 12 additions & 12 deletions src/hvac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ impl HvacDevice {
pub fn get_info(&self) -> Result<AirCondInfo, String> {
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);
}
Expand All @@ -66,19 +66,19 @@ impl HvacDevice {
pub fn get_state(&self) -> Result<AirCondState, String> {
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<Vec<u8>, 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);
}
Expand All @@ -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::<HvacDataMessage>(&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
Expand Down
4 changes: 2 additions & 2 deletions src/network/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
));
}
Expand All @@ -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,
));
}
Expand Down
22 changes: 14 additions & 8 deletions src/network/hvac_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -216,17 +219,20 @@ impl HvacDataMessage {
/// Pack the HvacDataMessage with an associated payload.
pub fn pack_with_payload(mut self, payload: &[u8]) -> Result<Vec<u8>, String> {
// Calculate tyhe length of the payload
self.data_length +=
<usize as TryInto<u16>>::try_into(payload.len()).expect("Payload is too long!");
self.data_length += <usize as TryInto<u16>>::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
Expand All @@ -240,14 +246,14 @@ impl HvacDataMessage {
pub fn unpack_with_payload(bytes: &[u8]) -> Result<Vec<u8>, 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,
));
}
Expand All @@ -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,
));
}
Expand Down

0 comments on commit c77bb31

Please sign in to comment.