Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions docker/MQTTManager/include/nspanel/nspanel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,13 @@ void NSPanel::reload_config() {
this->_mqtt_number_screen_brightness_topic = fmt::format("homeassistant/number/nspanelmanager/{}_screen_brightness/config", mqtt_register_mac);
this->_mqtt_number_screensaver_brightness_topic = fmt::format("homeassistant/number/nspanelmanager/{}_screensaver_brightness/config", mqtt_register_mac);
this->_mqtt_select_screensaver_topic = fmt::format("homeassistant/select/nspanelmanager/{}_screensaver_select/config", mqtt_register_mac);
this->_mqtt_switch_buzzer_mute_topic = fmt::format("homeassistant/switch/nspanelmanager/{}_buzzer_mute/config", mqtt_register_mac);
this->_mqtt_relay1_command_topic = fmt::format("nspanel/{}/relay1_cmd", this->_mac);
this->_mqtt_relay1_state_topic = fmt::format("nspanel/{}/relay1_state", this->_mac);
this->_mqtt_relay2_command_topic = fmt::format("nspanel/{}/relay2_cmd", this->_mac);
this->_mqtt_relay2_state_topic = fmt::format("nspanel/{}/relay2_state", this->_mac);
this->_mqtt_buzzer_mute_command_topic = fmt::format("nspanel/{}/buzzer_mute_cmd", this->_mac);
this->_mqtt_buzzer_mute_state_topic = fmt::format("nspanel/{}/buzzer_mute_state", this->_mac);
this->_mqtt_status_topic = fmt::format("nspanel/{}/status", this->_mac);
this->_mqtt_status_report_topic = fmt::format("nspanel/{}/status_report", this->_mac);
this->_mqtt_mac_log_topic = fmt::format("nspanel/{}/log", this->_mac);
Expand All @@ -256,6 +259,7 @@ void NSPanel::reload_config() {
MQTT_Manager::subscribe(this->_mqtt_mac_log_topic, boost::bind(&NSPanel::mqtt_log_callback, this, _1, _2));
MQTT_Manager::subscribe(this->_mqtt_status_topic, boost::bind(&NSPanel::mqtt_callback, this, _1, _2));
MQTT_Manager::subscribe(this->_mqtt_status_report_topic, boost::bind(&NSPanel::mqtt_callback, this, _1, _2));
MQTT_Manager::subscribe(this->_mqtt_buzzer_mute_command_topic, boost::bind(&NSPanel::mqtt_callback, this, _1, _2));
}

if (name_changed || reregister_to_ha_mqtt_discovery) {
Expand Down Expand Up @@ -482,14 +486,15 @@ void NSPanel::send_config() {
}

// Load buzzer sounds. A panel setting overrides the global one, an empty value means no sound for that event.
// While the buzzer is muted no sounds are sent, which silences every event.
const std::vector<std::tuple<NSPanelConfig_NSPanelSoundEvent, std::string, MQTT_MANAGER_SETTING>> sound_settings = {
{NSPanelConfig_NSPanelSoundEvent_TOUCH, "buzzer_sound_touch", MQTT_MANAGER_SETTING::BUZZER_SOUND_TOUCH},
{NSPanelConfig_NSPanelSoundEvent_SCENE_ACTIVATED, "buzzer_sound_scene_activated", MQTT_MANAGER_SETTING::BUZZER_SOUND_SCENE_ACTIVATED},
{NSPanelConfig_NSPanelSoundEvent_SCENE_SAVED, "buzzer_sound_scene_saved", MQTT_MANAGER_SETTING::BUZZER_SOUND_SCENE_SAVED},
};
for (const auto &[event, nspanel_setting_key, global_setting] : sound_settings) {
std::string rtttl = this->_get_nspanel_setting_with_default(nspanel_setting_key, MqttManagerConfig::get_setting_with_default<std::string>(global_setting));
if (!rtttl.empty()) {
if (!rtttl.empty() && !this->_buzzer_muted) {
NSPanelConfig_Sound *sound = config.add_sounds();
sound->set_event(event);
sound->set_rtttl(rtttl);
Expand Down Expand Up @@ -518,6 +523,7 @@ void NSPanel::reset_mqtt_topics() {
MQTT_Manager::detach_callback(this->_mqtt_mac_log_topic, boost::bind(&NSPanel::mqtt_log_callback, this, _1, _2));
MQTT_Manager::detach_callback(this->_mqtt_legacy_status_topic, boost::bind(&NSPanel::mqtt_callback, this, _1, _2));
MQTT_Manager::detach_callback(this->_mqtt_legacy_status_report_topic, boost::bind(&NSPanel::mqtt_callback, this, _1, _2));
MQTT_Manager::detach_callback(this->_mqtt_buzzer_mute_command_topic, boost::bind(&NSPanel::mqtt_callback, this, _1, _2));

// This nspanel was removed. Clear any retain on any MQTT topic.
MQTT_Manager::clear_retain(this->_mqtt_command_topic);
Expand All @@ -535,12 +541,16 @@ void NSPanel::reset_ha_mqtt_topics() {
MQTT_Manager::clear_retain(this->_mqtt_number_screen_brightness_topic);
MQTT_Manager::clear_retain(this->_mqtt_number_screensaver_brightness_topic);
MQTT_Manager::clear_retain(this->_mqtt_select_screensaver_topic);
MQTT_Manager::clear_retain(this->_mqtt_switch_buzzer_mute_topic);
}

void NSPanel::erase() {
this->reboot();
this->reset_mqtt_topics();
this->reset_ha_mqtt_topics();
// Not cleared in reset_mqtt_topics() as that runs on every config reload, and the retained command is what remembers the mute.
MQTT_Manager::clear_retain(this->_mqtt_buzzer_mute_command_topic);
MQTT_Manager::clear_retain(this->_mqtt_buzzer_mute_state_topic);
}

uint NSPanel::get_id() {
Expand All @@ -565,7 +575,19 @@ void NSPanel::mqtt_callback(std::string topic, std::string payload) {
}

try {
if (topic.compare(this->_mqtt_log_topic) == 0) {
if (topic.compare(this->_mqtt_buzzer_mute_command_topic) == 0) {
if (payload.compare("1") != 0 && payload.compare("0") != 0) {
SPDLOG_ERROR("Received unknown buzzer mute command '{}' for NSPanel {}::{}. Expected '1' or '0'.", payload, this->_id, this->_name);
return;
}
bool muted = payload.compare("1") == 0;
bool changed = this->_buzzer_muted.exchange(muted) != muted;
MQTT_Manager::publish(this->_mqtt_buzzer_mute_state_topic, payload, true);
if (changed) {
SPDLOG_INFO("NSPanel {}::{} buzzer {}. Sending config.", this->_id, this->_name, muted ? "muted" : "unmuted");
this->send_config();
}
} else if (topic.compare(this->_mqtt_log_topic) == 0) {
// Split log message by semicolon to extract MAC, log level and message.
std::string message = payload;
std::vector<std::string> message_parts;
Expand Down Expand Up @@ -1591,6 +1613,25 @@ void NSPanel::register_to_home_assistant() {
std::string screensaver_select_data_str = screensaver_select_data.dump();
SPDLOG_DEBUG("Registring screensaver mode select for NSPanel {}::{} to Home Assistant.", this->_id, this->_name);
MQTT_Manager::publish(this->_mqtt_select_screensaver_topic, screensaver_select_data_str, true);

// Register buzzer mute switch. Only the Sonoff panel has a buzzer.
if (this->_model == MQTT_MANAGER_NSPANEL_MODEL::SONOFF) {
nlohmann::json buzzer_mute_data = nlohmann::json(base_json);
buzzer_mute_data["device_class"] = "switch";
buzzer_mute_data["name"] = "Mute sounds";
buzzer_mute_data["icon"] = "mdi:volume-off";
buzzer_mute_data["state_topic"] = this->_mqtt_buzzer_mute_state_topic;
buzzer_mute_data["command_topic"] = this->_mqtt_buzzer_mute_command_topic;
buzzer_mute_data["state_on"] = "1";
buzzer_mute_data["state_off"] = "0";
buzzer_mute_data["payload_on"] = "1";
buzzer_mute_data["payload_off"] = "0";
buzzer_mute_data["retain"] = true; // The retained command is what remembers the mute across manager restarts.
buzzer_mute_data["unique_id"] = fmt::format("{}_buzzer_mute", this->_mqtt_register_mac);
std::string buzzer_mute_data_str = buzzer_mute_data.dump();
SPDLOG_DEBUG("Registring buzzer mute switch for NSPanel {}::{} to Home Assistant.", this->_id, this->_name);
MQTT_Manager::publish(this->_mqtt_switch_buzzer_mute_topic, buzzer_mute_data_str, true);
}
}

void NSPanel::set_relay_state(uint8_t relay, bool state) {
Expand Down
8 changes: 8 additions & 0 deletions docker/MQTTManager/include/nspanel/nspanel.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define MQTT_MANAGER_NSPANEL
#include "protobuf_nspanel.pb.h"
#include "websocket_server/websocket_server.hpp"
#include <atomic>
#include <command_manager/command_manager.hpp>
#include <cstdint>
#include <database_manager/database_manager.hpp>
Expand Down Expand Up @@ -236,6 +237,12 @@ class NSPanel {
std::string _mqtt_relay2_state_topic;
// Wether or not relay2 is on
bool _relay2_state = false;
// The topic Home Assistant sends the buzzer mute command to
std::string _mqtt_buzzer_mute_command_topic;
// The topic where the buzzer mute state is published
std::string _mqtt_buzzer_mute_state_topic;
// Wether or not the buzzer is muted. While muted, no buzzer sounds are sent in the config.
std::atomic<bool> _buzzer_muted = false;
// The topic to capture logs from MQTT
std::string _mqtt_log_topic;
// The topic to capture status (online/offline) from MQTT
Expand Down Expand Up @@ -268,6 +275,7 @@ class NSPanel {
std::string _mqtt_number_screen_brightness_topic;
std::string _mqtt_number_screensaver_brightness_topic;
std::string _mqtt_select_screensaver_topic;
std::string _mqtt_switch_buzzer_mute_topic;

// Topic where to send the NSPanelHomePageStatus protobuf state updates
std::string _mqtt_topic_home_page_status;
Expand Down
89 changes: 89 additions & 0 deletions docker/MQTTManager/tests/nspanel_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,95 @@ TEST_F(NSPanelTest, panel_buzzer_settings_override_the_global_sounds) {
EXPECT_EQ(config->sounds(1).rtttl(), "panel_saved:d=8,o=5,b=120:e");
}

TEST_F(NSPanelTest, muting_the_buzzer_sends_no_sounds_until_unmuted) {
OnExit reset{[] {
MqttManagerConfig::set_setting_value(MQTT_MANAGER_SETTING::BUZZER_SOUND_TOUCH, "");
}};
MqttManagerConfig::set_setting_value(MQTT_MANAGER_SETTING::BUZZER_SOUND_TOUCH, "touch:d=32,o=6,b=190:g6");
load_panel();
last_published_config();

panel->mqtt_callback("nspanel/" + PANEL_MAC + "/buzzer_mute_cmd", "1");

auto messages = MQTT_Manager::test_take_published_messages();
auto state = std::find_if(messages.begin(), messages.end(), [](auto &message) { return message.topic == "nspanel/" + PANEL_MAC + "/buzzer_mute_state"; });
ASSERT_NE(state, messages.end());
EXPECT_EQ(state->payload, "1");
EXPECT_TRUE(state->retain);
auto config = std::find_if(messages.begin(), messages.end(), [](auto &message) { return message.topic == "nspanel/" + PANEL_MAC + "/config"; });
ASSERT_NE(config, messages.end());
NSPanelConfig muted_config;
ASSERT_TRUE(muted_config.ParseFromString(config->payload));
EXPECT_EQ(muted_config.sounds_size(), 0);

panel->mqtt_callback("nspanel/" + PANEL_MAC + "/buzzer_mute_cmd", "0");

auto unmuted_config = last_published_config();
ASSERT_TRUE(unmuted_config.has_value());
ASSERT_EQ(unmuted_config->sounds_size(), 1);
EXPECT_EQ(unmuted_config->sounds(0).rtttl(), "touch:d=32,o=6,b=190:g6");
}

TEST_F(NSPanelTest, buzzer_mute_is_kept_across_config_reloads) {
OnExit reset{[] {
MqttManagerConfig::set_setting_value(MQTT_MANAGER_SETTING::BUZZER_SOUND_TOUCH, "");
}};
MqttManagerConfig::set_setting_value(MQTT_MANAGER_SETTING::BUZZER_SOUND_TOUCH, "touch:d=32,o=6,b=190:g6");
load_panel();
panel->mqtt_callback("nspanel/" + PANEL_MAC + "/buzzer_mute_cmd", "1");
last_published_config();

panel->reload_config();

auto config = last_published_config();
ASSERT_TRUE(config.has_value());
EXPECT_EQ(config->sounds_size(), 0);
}

TEST_F(NSPanelTest, unchanged_buzzer_mute_does_not_resend_the_config) {
load_panel();
panel->mqtt_callback("nspanel/" + PANEL_MAC + "/buzzer_mute_cmd", "1");
last_published_config();

// The broker replays the retained command on every resubscribe.
panel->mqtt_callback("nspanel/" + PANEL_MAC + "/buzzer_mute_cmd", "1");

EXPECT_FALSE(last_published_config().has_value());
}

TEST_F(NSPanelTest, unknown_buzzer_mute_command_is_ignored) {
load_panel();
last_published_config();
ScopedErrorLog log;

panel->mqtt_callback("nspanel/" + PANEL_MAC + "/buzzer_mute_cmd", "ON");

EXPECT_FALSE(last_published_config().has_value());
auto errors = log.errors();
EXPECT_TRUE(std::any_of(errors.begin(), errors.end(), [](auto &error) { return error.find("buzzer mute") != std::string::npos; }));
}

TEST_F(NSPanelTest, sonoff_panel_registers_a_retained_buzzer_mute_switch) {
load_panel();

auto messages = nspm_test::mqtt_published_to("homeassistant/switch/nspanelmanager/c0_49_ef_00_01_01_buzzer_mute/config");
ASSERT_FALSE(messages.empty());
auto discovery = nlohmann::json::parse(messages.back().payload);
EXPECT_EQ(discovery["command_topic"], "nspanel/" + PANEL_MAC + "/buzzer_mute_cmd");
EXPECT_EQ(discovery["state_topic"], "nspanel/" + PANEL_MAC + "/buzzer_mute_state");
EXPECT_EQ(discovery["retain"], true);
}

TEST_F(NSPanelTest, custom_panel_does_not_register_a_buzzer_mute_switch) {
update_row([](auto &settings) {
settings.model = "custom";
});

load_panel();

EXPECT_TRUE(nspm_test::mqtt_published_to("homeassistant/switch/nspanelmanager/c0_49_ef_00_01_01_buzzer_mute/config").empty());
}

TEST_F(NSPanelTest, reload_config_sends_changed_settings) {
load_panel();
last_published_config();
Expand Down
Loading