Skip to content

Commit

Permalink
Merge pull request #5 from manio/hvac-support
Browse files Browse the repository at this point in the history
Add HVAC/Air Conditioner support
  • Loading branch information
nicholascioli authored Sep 16, 2023
2 parents 3db13ac + d3f11e3 commit 7fd404b
Show file tree
Hide file tree
Showing 6 changed files with 434 additions and 1 deletion.
12 changes: 12 additions & 0 deletions src/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ use packed_struct::prelude::{ PackedStruct, PackedStructSlice };

use crate::{
REMOTE_CODES,
HVAC_CODES,

DeviceInfo,
RemoteDevice,
HvacDevice,

network::{
AuthenticationMessage,
Expand Down Expand Up @@ -42,6 +44,8 @@ use crate::{
pub enum Device {
/// A device capable of transmitting IR / RF codes.
Remote { remote: RemoteDevice },
/// Air Conditioner/HVAC device.
Hvac { hvac: HvacDevice },
}

/// Represents a generic device. See the different implementations for more specific info.
Expand Down Expand Up @@ -158,6 +162,7 @@ impl DeviceTrait for Device {
fn get_info(&self) -> DeviceInfo {
return match self {
Device::Remote { remote } => remote.info.clone(),
Device::Hvac { hvac } => hvac.info.clone(),
};
}

Expand All @@ -168,6 +173,10 @@ impl DeviceTrait for Device {
remote.info.auth_id = id;
remote.info.key = key;
},
Device::Hvac { hvac } => {
hvac.info.auth_id = id;
hvac.info.key = key;
},
};
}
}
Expand Down Expand Up @@ -215,6 +224,9 @@ fn create_device_from_packet(addr: SocketAddr, bytes_received: usize, bytes: &[u
_ if REMOTE_CODES.contains_key(&response.model_code) => Device::Remote {
remote: RemoteDevice::new(name, addr_ip, response)
},
_ 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)),
};

Expand Down
112 changes: 112 additions & 0 deletions src/hvac.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use std::net::Ipv4Addr;

use packed_struct::PackedStructSlice;
use phf::phf_map;

use crate::{
constants,
network::{
util::reverse_mac, AirCondInfo, AirCondState, DiscoveryResponse, HvacDataCommand,
HvacDataMessage,
},
Device, DeviceInfo,
};

/// A mapping of hvac device codes to their friendly model equivalent.
pub const HVAC_CODES: phf::Map<u16, &'static str> = phf_map! {
0x4E2Au16 => "Licensed manufacturer",
};

/// A broadlink HVAC/Air Conditioner device.
#[derive(Debug, Clone)]
pub struct HvacDevice {
/// Base information about the device.
pub info: DeviceInfo,
}

impl HvacDevice {
/// Create a new HvacDevice.
///
/// Note: This should not be called directly. Please use [Device::from_ip] or
/// [Device::list] instead.
pub fn new(name: &str, addr: Ipv4Addr, response: DiscoveryResponse) -> HvacDevice {
// Get the name of air conditioner
let friendly_model: String = HVAC_CODES
.get(&response.model_code)
.unwrap_or(&"Unknown")
.to_string();

return Self {
info: DeviceInfo {
address: addr,
mac: reverse_mac(response.mac),
model_code: response.model_code,
friendly_type: "HVAC".into(),
friendly_model: friendly_model,
name: name.into(),
auth_id: 0, // This will be populated when authenticated.
key: constants::INITIAL_KEY,
is_locked: response.is_locked,
},
};
}

/// Get basic information from the air conditioner.
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!");

return Ok(info);
}

/// Get current air conditioner state into AirCondState structure.
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!");

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();

return Ok(response);
}

/// Sends a raw command to the device.
/// Note: Try to avoid using this method in favor of [HvacDevice::get_info], [HvacDevice::set_state], etc.
pub fn send_command(
&self,
payload: &[u8],
command: HvacDataCommand,
) -> Result<Vec<u8>, String> {
// We cast this object to a generic device in order to make use of the shared
// helper utilities.
let generic_device = Device::Hvac { hvac: self.clone() };

// Construct the data message
let msg = HvacDataMessage::new(command);
let packed = msg
.pack_with_payload(&payload)
.expect("Could not pack HVAC data message!");

let response = generic_device
.send_command::<HvacDataMessage>(&packed)
.expect("Could not send command!");

// TODO: check if there is some relation between
// msg.command and the same return field from the response

return HvacDataMessage::unpack_with_payload(&response);
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod constants;
mod device;
mod device_info;
mod remote;
mod hvac;

// Manage exports
pub mod network;
Expand All @@ -13,3 +14,4 @@ pub mod traits;
pub use device::*;
pub use device_info::*;
pub use remote::*;
pub use hvac::*;
Loading

0 comments on commit 7fd404b

Please sign in to comment.