Compare commits

...

7 Commits

Author SHA1 Message Date
21b3692295 Switch workbench light to new color temperature light
Some checks failed
Build and deploy / Build application (push) Successful in 7m21s
Build and deploy / Build container (push) Failing after 1m3s
Build and deploy / Deploy container (push) Has been skipped
2025-08-22 03:09:05 +02:00
9b7dc27121 Add color temperature light 2025-08-22 03:08:44 +02:00
3eb71ddf85 Store brightness in f32 instead of f64 2025-08-22 03:07:55 +02:00
545b20d87c Added color temperature support with ColorSetting 2025-08-22 03:06:30 +02:00
5c74d6cb74 Allow timeout to be a fraction of a second instead of always whole seconds 2025-08-22 03:04:08 +02:00
bac526100c Removed cargo config that is no longer neccesary 2025-08-22 03:02:56 +02:00
5730d9db03 Fixed struct name for temperature control 2025-08-22 02:15:26 +02:00
7 changed files with 183 additions and 17 deletions

View File

@@ -1,3 +0,0 @@
[build]
target = "x86_64-unknown-linux-gnu"
rustflags = ["--cfg", "tokio_unstable"]

View File

@@ -5,7 +5,7 @@ use automation_macro::LuaDeviceConfig;
use google_home::device::Name;
use google_home::errors::ErrorCode;
use google_home::traits::{
AvailableSpeeds, FanSpeed, HumiditySetting, OnOff, Speed, SpeedValue, TemperatureSetting,
AvailableSpeeds, FanSpeed, HumiditySetting, OnOff, Speed, SpeedValue, TemperatureControl,
TemperatureUnit,
};
use google_home::types::Type;
@@ -209,7 +209,7 @@ impl HumiditySetting for AirFilter {
}
#[async_trait]
impl TemperatureSetting for AirFilter {
impl TemperatureControl for AirFilter {
fn query_only_temperature_control(&self) -> Option<bool> {
Some(true)
}

View File

@@ -15,7 +15,7 @@ use std::ops::Deref;
use automation_cast::Cast;
use automation_lib::device::{Device, LuaDeviceCreate};
use zigbee::light::{LightBrightness, LightOnOff};
use zigbee::light::{LightBrightness, LightColorTemperature, LightOnOff};
use zigbee::outlet::{OutletOnOff, OutletPower};
pub use self::air_filter::AirFilter;
@@ -96,6 +96,26 @@ macro_rules! impl_device {
});
}
if impls::impls!($device: google_home::traits::ColorSetting) {
methods.add_async_method("set_color_temperature", |_lua, this, temperature: u32| async move {
(this.deref().cast() as Option<&dyn google_home::traits::ColorSetting>)
.expect("Cast should be valid")
.set_color(google_home::traits::Color {temperature})
.await
.unwrap();
Ok(())
});
methods.add_async_method("color_temperature", |_lua, this, _: ()| async move {
Ok((this.deref().cast() as Option<&dyn google_home::traits::ColorSetting>)
.expect("Cast should be valid")
.color()
.await
.temperature)
});
}
if impls::impls!($device: google_home::traits::OpenClose) {
// TODO: Make discrete_only_open_close and query_only_open_close static, that way we can
// add only the supported functions and drop _percet if discrete is true
@@ -124,6 +144,7 @@ macro_rules! impl_device {
impl_device!(LightOnOff);
impl_device!(LightBrightness);
impl_device!(LightColorTemperature);
impl_device!(OutletOnOff);
impl_device!(OutletPower);
impl_device!(AirFilter);
@@ -141,6 +162,7 @@ impl_device!(Washer);
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
register_device!(lua, LightOnOff);
register_device!(lua, LightBrightness);
register_device!(lua, LightColorTemperature);
register_device!(lua, OutletOnOff);
register_device!(lua, OutletPower);
register_device!(lua, AirFilter);

View File

@@ -13,7 +13,7 @@ use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::LuaDeviceConfig;
use google_home::device;
use google_home::errors::ErrorCode;
use google_home::traits::{Brightness, OnOff};
use google_home::traits::{Brightness, Color, ColorSetting, ColorTemperatureRange, OnOff};
use google_home::types::Type;
use rumqttc::{matches, Publish};
use serde::{Deserialize, Serialize};
@@ -52,7 +52,7 @@ impl LightState for StateOnOff {}
pub struct StateBrightness {
#[serde(deserialize_with = "state_deserializer")]
state: bool,
brightness: f64,
brightness: f32,
}
impl LightState for StateBrightness {}
@@ -63,6 +63,31 @@ impl From<StateBrightness> for StateOnOff {
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StateColorTemperature {
#[serde(deserialize_with = "state_deserializer")]
state: bool,
brightness: f32,
color_temp: u32,
}
impl LightState for StateColorTemperature {}
impl From<StateColorTemperature> for StateOnOff {
fn from(state: StateColorTemperature) -> Self {
StateOnOff { state: state.state }
}
}
impl From<StateColorTemperature> for StateBrightness {
fn from(state: StateColorTemperature) -> Self {
StateBrightness {
state: state.state,
brightness: state.brightness,
}
}
}
#[derive(Debug, Clone)]
pub struct Light<T: LightState> {
config: Config<T>,
@@ -72,6 +97,7 @@ pub struct Light<T: LightState> {
pub type LightOnOff = Light<StateOnOff>;
pub type LightBrightness = Light<StateBrightness>;
pub type LightColorTemperature = Light<StateColorTemperature>;
impl<T: LightState> Light<T> {
async fn state(&self) -> RwLockReadGuard<T> {
@@ -181,6 +207,47 @@ impl OnMqtt for Light<StateBrightness> {
}
}
#[async_trait]
impl OnMqtt for Light<StateColorTemperature> {
async fn on_mqtt(&self, message: Publish) {
// Check if the message is from the deviec itself or from a remote
if matches(&message.topic, &self.config.mqtt.topic) {
let state = match serde_json::from_slice::<StateColorTemperature>(&message.payload) {
Ok(state) => state,
Err(err) => {
warn!(id = Device::get_id(self), "Failed to parse message: {err}");
return;
}
};
{
let current_state = self.state().await;
// No need to do anything if the state has not changed
if state.state == current_state.state
&& state.brightness == current_state.brightness
&& state.color_temp == current_state.color_temp
{
return;
}
}
self.state_mut().await.state = state.state;
self.state_mut().await.brightness = state.brightness;
self.state_mut().await.color_temp = state.color_temp;
debug!(
id = Device::get_id(self),
"Updating state to {:?}",
self.state().await
);
self.config
.callback
.call(self, self.state().await.deref())
.await;
}
}
}
#[async_trait]
impl<T: LightState> OnPresence for Light<T> {
async fn on_presence(&self, presence: bool) {
@@ -255,7 +322,7 @@ where
}
}
const FACTOR: f64 = 30.0;
const FACTOR: f32 = 30.0;
#[async_trait]
impl<T> Brightness for Light<T>
@@ -267,14 +334,14 @@ where
let state = self.state().await;
let state: StateBrightness = state.deref().clone().into();
let brightness =
100.0 * f64::log10(state.brightness / FACTOR + 1.0) / f64::log10(254.0 / FACTOR + 1.0);
100.0 * f32::log10(state.brightness / FACTOR + 1.0) / f32::log10(254.0 / FACTOR + 1.0);
Ok(brightness.clamp(0.0, 100.0).round() as u8)
}
async fn set_brightness(&self, brightness: u8) -> Result<(), ErrorCode> {
let brightness =
FACTOR * ((FACTOR / (FACTOR + 254.0)).powf(-(brightness as f64) / 100.0) - 1.0);
FACTOR * ((FACTOR / (FACTOR + 254.0)).powf(-(brightness as f32) / 100.0) - 1.0);
let message = json!({
"brightness": brightness.clamp(0.0, 254.0).round() as u8
@@ -297,3 +364,50 @@ where
Ok(())
}
}
#[async_trait]
impl<T> ColorSetting for Light<T>
where
T: LightState,
T: Into<StateColorTemperature>,
{
fn color_temperature_range(&self) -> ColorTemperatureRange {
ColorTemperatureRange {
temperature_min_k: 2200,
temperature_max_k: 4000,
}
}
async fn color(&self) -> Color {
let state = self.state().await;
let state: StateColorTemperature = state.deref().clone().into();
let temperature = 1_000_000 / state.color_temp;
Color { temperature }
}
async fn set_color(&self, color: Color) -> Result<(), ErrorCode> {
let temperature = 1_000_000 / color.temperature;
let message = json!({
"color_temp": temperature,
});
let topic = format!("{}/set", self.config.mqtt.topic);
// TODO: Handle potential errors here
self.config
.client
.publish(
&topic,
rumqttc::QoS::AtLeastOnce,
false,
serde_json::to_string(&message).unwrap(),
)
.await
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
.ok();
Ok(())
}
}

View File

@@ -29,14 +29,14 @@ impl mlua::UserData for Timeout {
methods.add_async_method(
"start",
|_lua, this, (timeout, callback): (u64, ActionCallback<mlua::Value, bool>)| async move {
|_lua, this, (timeout, callback): (f32, ActionCallback<mlua::Value, bool>)| async move {
if let Some(handle) = this.state.write().await.handle.take() {
handle.abort();
}
debug!("Running timeout callback after {timeout}s");
let timeout = Duration::from_secs(timeout);
let timeout = Duration::from_secs_f32(timeout);
this.state.write().await.handle = Some(tokio::spawn({
async move {

View File

@@ -250,7 +250,7 @@ automation.device_manager:add(OutletOnOff.new({
client = mqtt_client,
}))
local workbench_light = LightBrightness.new({
local workbench_light = LightColorTemperature.new({
name = "Light",
room = "Workbench",
topic = mqtt_z2m("workbench/light"),
@@ -258,13 +258,26 @@ local workbench_light = LightBrightness.new({
})
automation.device_manager:add(workbench_light)
local delay_color_temp = Timeout.new()
automation.device_manager:add(IkeaRemote.new({
name = "Remote",
room = "Workbench",
client = mqtt_client,
topic = mqtt_z2m("workbench/remote"),
callback = function(_, on)
workbench_light:set_on(on)
delay_color_temp:cancel()
if on then
workbench_light:set_brightness(82)
-- NOTE: This light does NOT support changing both the brightness and color
-- temperature at the same time, so we first change the brightness and once
-- that is complete we change the color temperature, as that is less likely
-- to have to actually change.
delay_color_temp:start(0.5, function()
workbench_light:set_color_temperature(3333)
end)
else
workbench_light:set_on(false)
end
end,
}))

View File

@@ -1,7 +1,7 @@
#![allow(non_snake_case)]
use automation_cast::Cast;
use google_home_macro::traits;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use crate::errors::ErrorCode;
use crate::Device;
@@ -26,6 +26,12 @@ traits! {
async fn brightness(&self) -> Result<u8, ErrorCode>,
"action.devices.commands.BrightnessAbsolute" => async fn set_brightness(&self, brightness: u8) -> Result<(), ErrorCode>,
},
"action.devices.traits.ColorSetting" => trait ColorSetting {
color_temperature_range: ColorTemperatureRange,
async fn color(&self) -> Color,
"action.devices.commands.ColorAbsolute" => async fn set_color(&self, color: Color) -> Result<(), ErrorCode>,
},
"action.devices.traits.Scene" => trait Scene {
scene_reversible: Option<bool>,
@@ -47,7 +53,7 @@ traits! {
async fn humidity_ambient_percent(&self) -> Result<isize, ErrorCode>,
},
"action.devices.traits.TemperatureControl" => trait TemperatureSetting {
"action.devices.traits.TemperatureControl" => trait TemperatureControl {
query_only_temperature_control: Option<bool>,
// TODO: Add rename
temperatureUnitForUX: TemperatureUnit,
@@ -56,6 +62,20 @@ traits! {
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ColorTemperatureRange {
pub temperature_min_k: u32,
pub temperature_max_k: u32,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Color {
#[serde(rename(serialize = "temperatureK"))]
pub temperature: u32,
}
#[derive(Debug, Serialize)]
pub struct SpeedValue {
pub speed_synonym: Vec<String>,