mirror of
https://codeberg.org/icewind/lightflash.git
synced 2026-06-03 10:04:07 +02:00
initial version
This commit is contained in:
commit
4254707d82
13 changed files with 1829 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
use flake
|
||||||
16
.forgejo/workflows/ci.yaml
Normal file
16
.forgejo/workflows/ci.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
name: "CI"
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
checks:
|
||||||
|
runs-on: nix
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: https://codeberg.org/icewind/attic-action@v1
|
||||||
|
with:
|
||||||
|
name: link
|
||||||
|
instance: https://cache.icewind.link
|
||||||
|
authToken: "${{ secrets.ATTIC_TOKEN }}"
|
||||||
|
- run: nix flake check --keep-going
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
result
|
||||||
|
config.toml
|
||||||
1284
Cargo.lock
generated
Normal file
1284
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "lightflash"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
thiserror = "2.0.17"
|
||||||
|
serde_json = "1.0.145"
|
||||||
|
tokio = { version = "1.48.0", features = ["rt-multi-thread", "macros"] }
|
||||||
|
secretfile = "0.1.1"
|
||||||
|
toml = "0.9.8"
|
||||||
|
clap = { version = "4.5.50", features = ["derive"] }
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-subscriber = "0.3.20"
|
||||||
|
main_error = "0.1.2"
|
||||||
|
rumqttc = "0.25.0"
|
||||||
|
hostname = "0.4.1"
|
||||||
3
README.md
Normal file
3
README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# lightflash
|
||||||
|
|
||||||
|
Flash tasmota controlled lights with a tasmota button.
|
||||||
107
flake.lock
generated
Normal file
107
flake.lock
generated
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"crane": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1748970125,
|
||||||
|
"narHash": "sha256-UDyigbDGv8fvs9aS95yzFfOKkEjx1LO3PL3DsKopohA=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "323b5746d89e04b22554b061522dfce9e4c49b18",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flakelight": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1750683087,
|
||||||
|
"narHash": "sha256-CkmobghX9K9soC4DoorFo/vlWXxpf8xuZyoCsOnWzqM=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "flakelight",
|
||||||
|
"rev": "aa93e0e1ecdccc70c7a185f60c562effbfe3c8d9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "flakelight",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mill-scale": {
|
||||||
|
"inputs": {
|
||||||
|
"crane": "crane",
|
||||||
|
"flakelight": [
|
||||||
|
"flakelight"
|
||||||
|
],
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1750254239,
|
||||||
|
"narHash": "sha256-RRhboiUiw4o4hPhxTTnUcd2+8yOLdt6hOTDraMPE/rg=",
|
||||||
|
"ref": "refs/heads/main",
|
||||||
|
"rev": "b7201d4a2220de413e6426c405b69839f6130d1c",
|
||||||
|
"revCount": 53,
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://codeberg.org/icewind/mill-scale"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://codeberg.org/icewind/mill-scale"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1750622754,
|
||||||
|
"narHash": "sha256-kMhs+YzV4vPGfuTpD3mwzibWUE6jotw5Al2wczI0Pv8=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "c7ab75210cb8cb16ddd8f290755d9558edde7ee1",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"ref": "nixos-25.05",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flakelight": "flakelight",
|
||||||
|
"mill-scale": "mill-scale",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"mill-scale",
|
||||||
|
"flakelight",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1750214276,
|
||||||
|
"narHash": "sha256-1kniuhH70q4TAC/xIvjFYH46aHiLrbIlcr6fdrRwO1A=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "f9b2b2b1327ff6beab4662b8ea41689e0a57b8d4",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
38
flake.nix
Normal file
38
flake.nix
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "nixpkgs/nixos-25.05";
|
||||||
|
flakelight = {
|
||||||
|
url = "github:nix-community/flakelight";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
mill-scale = {
|
||||||
|
url = "git+https://codeberg.org/icewind/mill-scale";
|
||||||
|
inputs.flakelight.follows = "flakelight";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
outputs = {mill-scale, ...}:
|
||||||
|
mill-scale ./. {
|
||||||
|
withOverlays = [
|
||||||
|
(import ./nix/overlay.nix)
|
||||||
|
];
|
||||||
|
|
||||||
|
packages = {
|
||||||
|
lightflash = pkgs: pkgs.lightflash;
|
||||||
|
};
|
||||||
|
|
||||||
|
nixosModules = {outputs, ...}: {
|
||||||
|
default = {
|
||||||
|
pkgs,
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
imports = [./nix/module.nix];
|
||||||
|
config = lib.mkIf config.services.lightflash.enable {
|
||||||
|
nixpkgs.overlays = [outputs.overlays.default];
|
||||||
|
services.lightflash.package = lib.mkDefault pkgs.lightflash;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
110
nix/module.nix
Normal file
110
nix/module.nix
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
with lib; let
|
||||||
|
format = pkgs.formats.toml {};
|
||||||
|
cfg = config.services.lightflash;
|
||||||
|
configFile = format.generate "lightflash.toml" {
|
||||||
|
mqtt = {
|
||||||
|
inherit (cfg.mqtt) hostname username;
|
||||||
|
password_file = "$CREDENTIALS_DIRECTORY/mqtt_password";
|
||||||
|
};
|
||||||
|
switch = cfg.switches;
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
options.services.lightflash = {
|
||||||
|
enable = mkEnableOption "Enables the lightflash service";
|
||||||
|
|
||||||
|
mqtt = mkOption {
|
||||||
|
type = types.submodule {
|
||||||
|
options = {
|
||||||
|
hostname = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "mqtt server hostname";
|
||||||
|
};
|
||||||
|
username = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "mqtt username";
|
||||||
|
};
|
||||||
|
passwordFile = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "path containing the mqtt password";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
switches = mkOption {
|
||||||
|
description = "configured triggers";
|
||||||
|
type = types.listOf (types.submodule {
|
||||||
|
options = {
|
||||||
|
name = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "switch to listen on";
|
||||||
|
};
|
||||||
|
lights = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
description = "lights to flash";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
description = "package to use";
|
||||||
|
};
|
||||||
|
|
||||||
|
logLevel = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "INFO";
|
||||||
|
description = "log level";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
systemd.services.lightflash = {
|
||||||
|
wantedBy = ["multi-user.target"];
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
RUST_LOG = cfg.logLevel;
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
LoadCredential = [
|
||||||
|
"mqtt_password:${cfg.mqtt.passwordFile}"
|
||||||
|
];
|
||||||
|
|
||||||
|
Restart = "on-failure";
|
||||||
|
ExecStart = "${getExe cfg.package} --config ${configFile}";
|
||||||
|
DynamicUser = true;
|
||||||
|
PrivateUsers = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
ProtectClock = true;
|
||||||
|
CapabilityBoundingSet = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
SystemCallArchitectures = "native";
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectHostname = true;
|
||||||
|
LockPersonality = true;
|
||||||
|
ProtectProc = "invisible";
|
||||||
|
PrivateDevices = true;
|
||||||
|
RestrictAddressFamilies = ["AF_LOCAL" "AF_INET"];
|
||||||
|
RestrictRealtime = true;
|
||||||
|
SystemCallFilter = ["~@reboot" "~@cpu-emulation" "~@obsolete" "~@debug" "~@swap" "~@clock" "~@module"];
|
||||||
|
IPAddressDeny = ["any"];
|
||||||
|
IPAddressAllow = ["localhost"];
|
||||||
|
UMask = "0007";
|
||||||
|
RuntimeDirectory = "lightflash";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
3
nix/overlay.nix
Normal file
3
nix/overlay.nix
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
final: prev: {
|
||||||
|
lightflash = final.callPackage ./package.nix {};
|
||||||
|
}
|
||||||
21
nix/package.nix
Normal file
21
nix/package.nix
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
rustPlatform,
|
||||||
|
pkg-config,
|
||||||
|
lib,
|
||||||
|
}: let
|
||||||
|
inherit (lib.sources) sourceByRegex;
|
||||||
|
inherit (builtins) fromTOML readFile;
|
||||||
|
src = sourceByRegex ../. ["Cargo.*" "(src)(/.*)?"];
|
||||||
|
version = (fromTOML (readFile ../Cargo.toml)).package.version;
|
||||||
|
in
|
||||||
|
rustPlatform.buildRustPackage rec {
|
||||||
|
pname = "lightflash";
|
||||||
|
|
||||||
|
inherit src version;
|
||||||
|
|
||||||
|
cargoLock = {
|
||||||
|
lockFile = ../Cargo.lock;
|
||||||
|
};
|
||||||
|
|
||||||
|
meta.mainProgram = "lightflash";
|
||||||
|
}
|
||||||
91
src/config.rs
Normal file
91
src/config.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
use secretfile::{load, SecretError};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fs::read_to_string;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct LightFlashConfig {
|
||||||
|
pub mqtt: MqttConfig,
|
||||||
|
pub switch: Vec<SwitchConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LightFlashConfig {
|
||||||
|
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let raw = read_to_string(path).map_err(|error| ConfigError::Read {
|
||||||
|
path: path.into(),
|
||||||
|
error,
|
||||||
|
})?;
|
||||||
|
toml::from_str(&raw).map_err(|error| ConfigError::Parse {
|
||||||
|
path: path.into(),
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct SwitchConfig {
|
||||||
|
pub name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub lights: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(try_from = "RawMqttConfig")]
|
||||||
|
pub struct MqttConfig {
|
||||||
|
pub hostname: String,
|
||||||
|
pub port: Option<u16>,
|
||||||
|
pub username: Option<String>,
|
||||||
|
pub password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct RawMqttConfig {
|
||||||
|
pub hostname: String,
|
||||||
|
pub port: Option<u16>,
|
||||||
|
pub username: Option<String>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub password: Option<MqttPassword>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone, PartialEq)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum MqttPassword {
|
||||||
|
Raw { password: String },
|
||||||
|
File { password_file: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<RawMqttConfig> for MqttConfig {
|
||||||
|
type Error = SecretError;
|
||||||
|
|
||||||
|
fn try_from(value: RawMqttConfig) -> Result<Self, Self::Error> {
|
||||||
|
let password = match value.password {
|
||||||
|
Some(MqttPassword::Raw { password }) => Some(password),
|
||||||
|
Some(MqttPassword::File { password_file }) => Some(load(&password_file)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
Ok(MqttConfig {
|
||||||
|
hostname: value.hostname,
|
||||||
|
port: value.port,
|
||||||
|
username: value.username,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
#[error("Failed to read config file {}: {error:#}", path.display())]
|
||||||
|
Read {
|
||||||
|
path: PathBuf,
|
||||||
|
error: std::io::Error,
|
||||||
|
},
|
||||||
|
#[error("Failed to parse config file {}: {error:#}", path.display())]
|
||||||
|
Parse {
|
||||||
|
path: PathBuf,
|
||||||
|
error: toml::de::Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
134
src/main.rs
Normal file
134
src/main.rs
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
use crate::config::{LightFlashConfig, SwitchConfig};
|
||||||
|
use clap::Parser;
|
||||||
|
use clap::builder::Styles;
|
||||||
|
use clap::builder::styling::{AnsiColor, Effects};
|
||||||
|
use main_error::MainResult;
|
||||||
|
use rumqttc::{AsyncClient, ClientError, Event, MqttOptions, Packet, QoS};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::spawn;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
use tracing::{debug, info};
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
|
||||||
|
fn styles() -> Styles {
|
||||||
|
Styles::styled()
|
||||||
|
.header(AnsiColor::Yellow.on_default() | Effects::BOLD)
|
||||||
|
.usage(AnsiColor::Yellow.on_default() | Effects::BOLD)
|
||||||
|
.literal(AnsiColor::Blue.on_default() | Effects::BOLD)
|
||||||
|
.placeholder(AnsiColor::Green.on_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(styles = styles())]
|
||||||
|
struct Args {
|
||||||
|
/// Config file
|
||||||
|
#[arg(long)]
|
||||||
|
config: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> MainResult {
|
||||||
|
let args: Args = Args::parse();
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
let config = LightFlashConfig::load(&args.config)?;
|
||||||
|
|
||||||
|
let hostname = hostname::get()
|
||||||
|
.map(|os_str| os_str.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut mqtt_options = MqttOptions::new(
|
||||||
|
format!("lightflash-{}", hostname),
|
||||||
|
config.mqtt.hostname.as_str(),
|
||||||
|
config.mqtt.port.unwrap_or(1883),
|
||||||
|
);
|
||||||
|
if let (Some(username), Some(password)) = (&config.mqtt.username, &config.mqtt.password) {
|
||||||
|
mqtt_options.set_credentials(username, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (mqtt_client, mut event_loop) = AsyncClient::new(mqtt_options, 10);
|
||||||
|
|
||||||
|
let switches: Vec<_> = config
|
||||||
|
.switch
|
||||||
|
.into_iter()
|
||||||
|
.map(Switch::from)
|
||||||
|
.map(Arc::new)
|
||||||
|
.collect();
|
||||||
|
for switch in &switches {
|
||||||
|
mqtt_client
|
||||||
|
.subscribe(format!("tele/{}/SENSOR", switch.name), QoS::AtMostOnce)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
let mqtt_client = Arc::new(mqtt_client);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let notification = event_loop.poll().await?;
|
||||||
|
if let Event::Incoming(Packet::Publish(packet)) = notification {
|
||||||
|
if let Some(triggered) = packet
|
||||||
|
.topic
|
||||||
|
.as_str()
|
||||||
|
.strip_prefix("tele/")
|
||||||
|
.and_then(|topic| topic.strip_suffix("/SENSOR"))
|
||||||
|
{
|
||||||
|
for switch in &switches {
|
||||||
|
if switch.name == triggered {
|
||||||
|
let payload: SwitchPayload = serde_json::from_slice(&packet.payload)?;
|
||||||
|
if payload.doorbell.starts_with("ON") {
|
||||||
|
spawn(flash(mqtt_client.clone(), switch.clone()));
|
||||||
|
} else if payload.doorbell.starts_with("OFF") {
|
||||||
|
switch.active.store(false, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Switch {
|
||||||
|
name: String,
|
||||||
|
lights: Vec<String>,
|
||||||
|
active: AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SwitchConfig> for Switch {
|
||||||
|
fn from(value: SwitchConfig) -> Self {
|
||||||
|
Switch {
|
||||||
|
name: value.name,
|
||||||
|
lights: value.lights,
|
||||||
|
active: AtomicBool::new(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct SwitchPayload<'a> {
|
||||||
|
doorbell: Cow<'a, str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn flash(mqtt_client: Arc<AsyncClient>, switch: Arc<Switch>) {
|
||||||
|
switch.active.store(true, Ordering::SeqCst);
|
||||||
|
info!(switch = switch.name, "flashing lights");
|
||||||
|
|
||||||
|
for _ in 0..10 {
|
||||||
|
for light in &switch.lights {
|
||||||
|
toggle_light(&mqtt_client, light).await.unwrap();
|
||||||
|
}
|
||||||
|
sleep(Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
for light in &switch.lights {
|
||||||
|
toggle_light(&mqtt_client, light).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn toggle_light(mqtt_client: &AsyncClient, light: &str) -> Result<(), ClientError> {
|
||||||
|
debug!(light, "toggling light");
|
||||||
|
mqtt_client.publish(format!("cmnd/{light}/POWER"), QoS::ExactlyOnce, false, "toggle").await
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue