Compare commits
6 Commits
master
...
21b3692295
| Author | SHA1 | Date | |
|---|---|---|---|
|
21b3692295
|
|||
|
9b7dc27121
|
|||
|
3eb71ddf85
|
|||
|
545b20d87c
|
|||
|
5c74d6cb74
|
|||
|
bac526100c
|
@@ -1,3 +0,0 @@
|
|||||||
[build]
|
|
||||||
target = "x86_64-unknown-linux-gnu"
|
|
||||||
rustflags = ["--cfg", "tokio_unstable"]
|
|
||||||
@@ -15,7 +15,7 @@ use std::ops::Deref;
|
|||||||
|
|
||||||
use automation_cast::Cast;
|
use automation_cast::Cast;
|
||||||
use automation_lib::device::{Device, LuaDeviceCreate};
|
use automation_lib::device::{Device, LuaDeviceCreate};
|
||||||
use zigbee::light::{LightBrightness, LightOnOff};
|
use zigbee::light::{LightBrightness, LightColorTemperature, LightOnOff};
|
||||||
use zigbee::outlet::{OutletOnOff, OutletPower};
|
use zigbee::outlet::{OutletOnOff, OutletPower};
|
||||||
|
|
||||||
pub use self::air_filter::AirFilter;
|
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) {
|
if impls::impls!($device: google_home::traits::OpenClose) {
|
||||||
// TODO: Make discrete_only_open_close and query_only_open_close static, that way we can
|
// 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
|
// 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!(LightOnOff);
|
||||||
impl_device!(LightBrightness);
|
impl_device!(LightBrightness);
|
||||||
|
impl_device!(LightColorTemperature);
|
||||||
impl_device!(OutletOnOff);
|
impl_device!(OutletOnOff);
|
||||||
impl_device!(OutletPower);
|
impl_device!(OutletPower);
|
||||||
impl_device!(AirFilter);
|
impl_device!(AirFilter);
|
||||||
@@ -141,6 +162,7 @@ impl_device!(Washer);
|
|||||||
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
|
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
|
||||||
register_device!(lua, LightOnOff);
|
register_device!(lua, LightOnOff);
|
||||||
register_device!(lua, LightBrightness);
|
register_device!(lua, LightBrightness);
|
||||||
|
register_device!(lua, LightColorTemperature);
|
||||||
register_device!(lua, OutletOnOff);
|
register_device!(lua, OutletOnOff);
|
||||||
register_device!(lua, OutletPower);
|
register_device!(lua, OutletPower);
|
||||||
register_device!(lua, AirFilter);
|
register_device!(lua, AirFilter);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use automation_lib::mqtt::WrappedAsyncClient;
|
|||||||
use automation_macro::LuaDeviceConfig;
|
use automation_macro::LuaDeviceConfig;
|
||||||
use google_home::device;
|
use google_home::device;
|
||||||
use google_home::errors::ErrorCode;
|
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 google_home::types::Type;
|
||||||
use rumqttc::{matches, Publish};
|
use rumqttc::{matches, Publish};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -52,7 +52,7 @@ impl LightState for StateOnOff {}
|
|||||||
pub struct StateBrightness {
|
pub struct StateBrightness {
|
||||||
#[serde(deserialize_with = "state_deserializer")]
|
#[serde(deserialize_with = "state_deserializer")]
|
||||||
state: bool,
|
state: bool,
|
||||||
brightness: f64,
|
brightness: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LightState for StateBrightness {}
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Light<T: LightState> {
|
pub struct Light<T: LightState> {
|
||||||
config: Config<T>,
|
config: Config<T>,
|
||||||
@@ -72,6 +97,7 @@ pub struct Light<T: LightState> {
|
|||||||
|
|
||||||
pub type LightOnOff = Light<StateOnOff>;
|
pub type LightOnOff = Light<StateOnOff>;
|
||||||
pub type LightBrightness = Light<StateBrightness>;
|
pub type LightBrightness = Light<StateBrightness>;
|
||||||
|
pub type LightColorTemperature = Light<StateColorTemperature>;
|
||||||
|
|
||||||
impl<T: LightState> Light<T> {
|
impl<T: LightState> Light<T> {
|
||||||
async fn state(&self) -> RwLockReadGuard<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]
|
#[async_trait]
|
||||||
impl<T: LightState> OnPresence for Light<T> {
|
impl<T: LightState> OnPresence for Light<T> {
|
||||||
async fn on_presence(&self, presence: bool) {
|
async fn on_presence(&self, presence: bool) {
|
||||||
@@ -255,7 +322,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const FACTOR: f64 = 30.0;
|
const FACTOR: f32 = 30.0;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T> Brightness for Light<T>
|
impl<T> Brightness for Light<T>
|
||||||
@@ -267,14 +334,14 @@ where
|
|||||||
let state = self.state().await;
|
let state = self.state().await;
|
||||||
let state: StateBrightness = state.deref().clone().into();
|
let state: StateBrightness = state.deref().clone().into();
|
||||||
let brightness =
|
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)
|
Ok(brightness.clamp(0.0, 100.0).round() as u8)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_brightness(&self, brightness: u8) -> Result<(), ErrorCode> {
|
async fn set_brightness(&self, brightness: u8) -> Result<(), ErrorCode> {
|
||||||
let brightness =
|
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!({
|
let message = json!({
|
||||||
"brightness": brightness.clamp(0.0, 254.0).round() as u8
|
"brightness": brightness.clamp(0.0, 254.0).round() as u8
|
||||||
@@ -297,3 +364,50 @@ where
|
|||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ impl mlua::UserData for Timeout {
|
|||||||
|
|
||||||
methods.add_async_method(
|
methods.add_async_method(
|
||||||
"start",
|
"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() {
|
if let Some(handle) = this.state.write().await.handle.take() {
|
||||||
handle.abort();
|
handle.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Running timeout callback after {timeout}s");
|
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({
|
this.state.write().await.handle = Some(tokio::spawn({
|
||||||
async move {
|
async move {
|
||||||
|
|||||||
17
config.lua
17
config.lua
@@ -250,7 +250,7 @@ automation.device_manager:add(OutletOnOff.new({
|
|||||||
client = mqtt_client,
|
client = mqtt_client,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
local workbench_light = LightBrightness.new({
|
local workbench_light = LightColorTemperature.new({
|
||||||
name = "Light",
|
name = "Light",
|
||||||
room = "Workbench",
|
room = "Workbench",
|
||||||
topic = mqtt_z2m("workbench/light"),
|
topic = mqtt_z2m("workbench/light"),
|
||||||
@@ -258,13 +258,26 @@ local workbench_light = LightBrightness.new({
|
|||||||
})
|
})
|
||||||
automation.device_manager:add(workbench_light)
|
automation.device_manager:add(workbench_light)
|
||||||
|
|
||||||
|
local delay_color_temp = Timeout.new()
|
||||||
automation.device_manager:add(IkeaRemote.new({
|
automation.device_manager:add(IkeaRemote.new({
|
||||||
name = "Remote",
|
name = "Remote",
|
||||||
room = "Workbench",
|
room = "Workbench",
|
||||||
client = mqtt_client,
|
client = mqtt_client,
|
||||||
topic = mqtt_z2m("workbench/remote"),
|
topic = mqtt_z2m("workbench/remote"),
|
||||||
callback = function(_, on)
|
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,
|
end,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
use automation_cast::Cast;
|
use automation_cast::Cast;
|
||||||
use google_home_macro::traits;
|
use google_home_macro::traits;
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::errors::ErrorCode;
|
use crate::errors::ErrorCode;
|
||||||
use crate::Device;
|
use crate::Device;
|
||||||
@@ -26,6 +26,12 @@ traits! {
|
|||||||
async fn brightness(&self) -> Result<u8, ErrorCode>,
|
async fn brightness(&self) -> Result<u8, ErrorCode>,
|
||||||
"action.devices.commands.BrightnessAbsolute" => async fn set_brightness(&self, brightness: u8) -> Result<(), 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 {
|
"action.devices.traits.Scene" => trait Scene {
|
||||||
scene_reversible: Option<bool>,
|
scene_reversible: Option<bool>,
|
||||||
|
|
||||||
@@ -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)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct SpeedValue {
|
pub struct SpeedValue {
|
||||||
pub speed_synonym: Vec<String>,
|
pub speed_synonym: Vec<String>,
|
||||||
|
|||||||
Reference in New Issue
Block a user