Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/SantaClaas/embedded-fan-control.
Something went wrong. Try again.
12 kB · 290 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291//! This build script copies the `memory.x` file from the crate root into//! a directory where the linker can always find it at build time.//! For many projects this is optional, as the linker always searches the//! project root directory -- wherever `Cargo.toml` is. However, if you//! are using a workspace or have a more complicated build setup, this//! build script becomes required. Additionally, by requesting that//! Cargo re-run the build script whenever `memory.x` is changed,//! updating `memory.x` ensures a rebuild of the application with the//! new memory settings.use std::collections::BTreeMap;use std::env::{self, VarError};use std::fs::File;use std::io::Write;use std::path::PathBuf;use std::process::Command;use std::rc::Rc;use std::str::Utf8Error;
use home_assistant_discovery::{ Component, Device, DeviceClass, DiscoveryPayload, ListOrString, Origin, StateClass,};
#[derive(Debug, thiserror::Error)]enum GitHashError { #[error("Failed to execute git command: {0}")] CommandExecution(std::io::Error), #[error("Failed to parse git hash command output: {0}")] ParseHash(#[from] Utf8Error),}
#[derive(Debug, thiserror::Error)]enum BuildError { #[error("Failed to write memory.x file: {0}")] WriteMemoryXFile(#[from] std::io::Error),
#[error("Failed to load .env file for Wifi credentials: {0}")] LoadEnvFile(#[from] dotenvy::Error),
#[error("Missing variable {1} in .env file: {0}")] MissingEnvVar(VarError, &'static str),
#[error("Failed to get git hash: {0}")] GitHash(#[from] GitHashError),}
fn ensure_memory_x_file() -> Result<(), BuildError> { // Put `memory.x` in our output directory and ensure it's // on the linker search path. let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); File::create(out.join("memory.x")) .unwrap() .write_all(include_bytes!("memory.x"))?; println!("cargo:rustc-link-search={}", out.display());
// By default, Cargo will re-run a build script whenever // any file in the project changes. By specifying `memory.x` // here, we ensure the build script is only re-run when // `memory.x` is changed. println!("cargo:rerun-if-changed=memory.x");
println!("cargo:rustc-link-arg-bins=--nmagic"); println!("cargo:rustc-link-arg-bins=-Tlink.x"); println!("cargo:rustc-link-arg-bins=-Tlink-rp.x"); println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
Ok(())}
fn setup_configuration() -> Result<(), BuildError> { const WIFI_NETWORK: &str = "FAN_CONTROL_WIFI_NETWORK"; const WIFI_PASSWORD: &str = "FAN_CONTROL_WIFI_PASSWORD"; const MQTT_BROKER_USERNAME: &str = "FAN_CONTROL_MQTT_BROKER_USERNAME"; const MQTT_BROKER_PASSWORD: &str = "FAN_CONTROL_MQTT_BROKER_PASSWORD"; const MQTT_BROKER_ADDRESS: &str = "FAN_CONTROL_MQTT_BROKER_ADDRESS"; const MQTT_BROKER_PORT: &str = "FAN_CONTROL_MQTT_BROKER_PORT";
dotenvy::dotenv()?; let wifi_network = env::var(WIFI_NETWORK).map_err(|error| BuildError::MissingEnvVar(error, WIFI_NETWORK))?; let wifi_password = env::var(WIFI_PASSWORD).map_err(|error| BuildError::MissingEnvVar(error, WIFI_PASSWORD))?;
let mqtt_broker_username = env::var(MQTT_BROKER_USERNAME) .map_err(|error| BuildError::MissingEnvVar(error, MQTT_BROKER_USERNAME))?; let mqtt_broker_password = env::var(MQTT_BROKER_PASSWORD) .map_err(|error| BuildError::MissingEnvVar(error, MQTT_BROKER_PASSWORD))?; let mqtt_broker_address = env::var(MQTT_BROKER_ADDRESS) .map_err(|error| BuildError::MissingEnvVar(error, MQTT_BROKER_ADDRESS))?; let mqtt_broker_port = env::var(MQTT_BROKER_PORT) .map_err(|error| BuildError::MissingEnvVar(error, MQTT_BROKER_PORT))?;
println!("cargo:rustc-env=FAN_CONTROL_WIFI_NETWORK={wifi_network}"); println!("cargo:rustc-env=FAN_CONTROL_WIFI_PASSWORD={wifi_password}"); println!("cargo:rustc-env=FAN_CONTROL_MQTT_BROKER_USERNAME={mqtt_broker_username}"); println!("cargo:rustc-env=FAN_CONTROL_MQTT_BROKER_PASSWORD={mqtt_broker_password}"); println!("cargo:rustc-env=FAN_CONTROL_MQTT_BROKER_ADDRESS={mqtt_broker_address}"); println!("cargo:rustc-env=FAN_CONTROL_MQTT_BROKER_PORT={mqtt_broker_port}");
Ok(())}
fn get_git_hash() -> Result<Rc<str>, GitHashError> { let output = Command::new("git") .args(["rev-parse", "--short", "HEAD"]) .output() .map_err(GitHashError::CommandExecution)?;
println!("cargo:rerun-if-changed=../.git/HEAD"); println!("cargo:rerun-if-changed=../.git/refs");
Ok(Rc::from(str::from_utf8(&output.stdout)?.trim()))}
/// Which identifiers one fan's four sensors are announced under. All of them are composed from/// that fan's identifier in the `topic` crate, so the two fans differ only in what is passed herestruct SensorIdentifiers { /// The topic all five values arrive on, as one JSON object state: &'static str, speed: &'static str, motor_temperature: &'static str, electronics_temperature: &'static str, power: &'static str,}
/// The four values a fan reports about itself.////// Every one of them reads the same topic and picks its value out of the JSON object published/// there, so a poll costs one publish rather than four. The keys the templates use are the field/// names `fan_sensor::Reading` serializes, which is the one place they have to agree.////// Built per fan rather than written out twice, because only the identifiers and the name differfn create_fan_sensor_components( fan_name: &str, identifiers: SensorIdentifiers,) -> [(String, Component); 4] { [ ( identifiers.speed.to_string(), Component::Sensor { name: Some(format!("{fan_name} speed")), state_topic: Some(identifiers.state), // Home Assistant has no device class for a rotation rate, so the unit carries it device_class: None, state_class: Some(StateClass::Measurement), unit_of_measurement: Some("rpm"), value_template: Some("{{ value_json.speed }}"), unique_id: Some(identifiers.speed), }, ), ( identifiers.motor_temperature.to_string(), Component::Sensor { name: Some(format!("{fan_name} motor temperature")), state_topic: Some(identifiers.state), device_class: Some(DeviceClass::Temperature), state_class: Some(StateClass::Measurement), unit_of_measurement: Some("°C"), value_template: Some("{{ value_json.motor_temperature }}"), unique_id: Some(identifiers.motor_temperature), }, ), ( identifiers.electronics_temperature.to_string(), Component::Sensor { name: Some(format!("{fan_name} electronics temperature")), state_topic: Some(identifiers.state), device_class: Some(DeviceClass::Temperature), state_class: Some(StateClass::Measurement), unit_of_measurement: Some("°C"), value_template: Some("{{ value_json.electronics_temperature }}"), unique_id: Some(identifiers.electronics_temperature), }, ), ( identifiers.power.to_string(), Component::Sensor { name: Some(format!("{fan_name} power")), state_topic: Some(identifiers.state), device_class: Some(DeviceClass::Power), state_class: Some(StateClass::Measurement), unit_of_measurement: Some("W"), value_template: Some("{{ value_json.power }}"), unique_id: Some(identifiers.power), }, ), ]}
/// Everything the fan controller announces to Home Assistant: the two fans, and the four sensors/// each of them reportsfn create_components() -> BTreeMap<String, Component> { let mut components = BTreeMap::from([ // Fan 1 ( topic::fan_controller::fan_1::UNIQUE_ID.to_string(), Component::Fan { name: Some("Fan 1"), unique_id: Some(topic::fan_controller::fan_1::UNIQUE_ID), state_topic: Some(topic::fan_controller::fan_1::state::STATE), command_topic: topic::fan_controller::fan_1::state::COMMAND, percentage_state_topic: Some(topic::fan_controller::fan_1::percentage::STATE), percentage_command_topic: Some( topic::fan_controller::fan_1::percentage::COMMAND, ), speed_range_max: Some(32_000), }, ), // Fan 2 ( topic::fan_controller::fan_2::UNIQUE_ID.to_string(), Component::Fan { name: Some("Fan 2"), unique_id: Some(topic::fan_controller::fan_2::UNIQUE_ID), state_topic: Some(topic::fan_controller::fan_2::state::STATE), command_topic: topic::fan_controller::fan_2::state::COMMAND, percentage_state_topic: Some(topic::fan_controller::fan_2::percentage::STATE), percentage_command_topic: Some( topic::fan_controller::fan_2::percentage::COMMAND, ), speed_range_max: Some(32_000), }, ), ]);
components.extend(create_fan_sensor_components( "Fan 1", SensorIdentifiers { state: topic::fan_controller::fan_1::sensor::STATE, speed: topic::fan_controller::fan_1::sensor::SPEED, motor_temperature: topic::fan_controller::fan_1::sensor::MOTOR_TEMPERATURE, electronics_temperature: topic::fan_controller::fan_1::sensor::ELECTRONICS_TEMPERATURE, power: topic::fan_controller::fan_1::sensor::POWER, }, ));
components.extend(create_fan_sensor_components( "Fan 2", SensorIdentifiers { state: topic::fan_controller::fan_2::sensor::STATE, speed: topic::fan_controller::fan_2::sensor::SPEED, motor_temperature: topic::fan_controller::fan_2::sensor::MOTOR_TEMPERATURE, electronics_temperature: topic::fan_controller::fan_2::sensor::ELECTRONICS_TEMPERATURE, power: topic::fan_controller::fan_2::sensor::POWER, }, ));
components}
fn set_discovery_payload(git_hash: &str) { let package_version = env!("CARGO_PKG_VERSION"); // Following semantic versioning build metadata let version: Option<Rc<str>> = Option::from(Rc::from(format!("{package_version}+{git_hash}"))); println!("Setting version to {version:?}"); let payload = DiscoveryPayload { device: Device { identifiers: Some(ListOrString::String(topic::fan_controller::OBJECT_ID)), name: Some("New Fan Controller"), model: Some("Raspberry Pi Pico W 1"), manufacturer: Some("claas.dev"), hardware_version: Some("1.0"), software_version: version.clone(), ..Default::default() }, origin: Origin { name: "fan-controller", software_version: version, support_url: Some("https://github.com/SantaClaas/embedded-fan-control"), }, components: create_components(), quality_of_service: None, state_topic: Some(topic::fan_controller::STATE), command_topic: Some(topic::fan_controller::COMMAND), encoding: None, };
let payload = serde_json::to_string(&payload).unwrap();
println!("cargo:rustc-env=FAN_CONTROLLER_DISCOVERY_PAYLOAD={payload}",);}
fn main() -> Result<(), BuildError> { ensure_memory_x_file()?; setup_configuration()?; let git_hash = get_git_hash()?; set_discovery_payload(&git_hash); Ok(())}