From cd6b47d9332f7872c065eeeef5ca7908727f4efc Mon Sep 17 00:00:00 2001 From: James Mulcahy Date: Sun, 3 May 2026 07:29:47 -0700 Subject: [PATCH 1/3] Always emit triggering room context when activating HA scenes/scripts When activating a Home Assistant script, always include scene_name, scene_id, and (where available) triggering_room_id/name and scene_room_id/name in the nspanelmanager variables context. Previously room context was only sent for room-specific scenes. Global scenes now also receive the room of the panel that triggered them, allowing HA scripts to take room-specific actions while remaining globally defined. Co-Authored-By: Claude Sonnet 4.6 --- .../include/entity_manager/entity_manager.cpp | 15 +++++++++++- .../include/entity_manager/entity_manager.hpp | 1 + .../MQTTManager/include/nspanel/nspanel.cpp | 15 ++++++++++-- .../include/scenes/home_assistant_scene.cpp | 23 +++++++++++++++---- .../include/scenes/home_assistant_scene.hpp | 2 +- .../MQTTManager/include/scenes/nspm_scene.cpp | 2 +- .../MQTTManager/include/scenes/nspm_scene.hpp | 2 +- .../include/scenes/openhab_scene.cpp | 2 +- .../include/scenes/openhab_scene.hpp | 2 +- docker/MQTTManager/include/scenes/scene.cpp | 4 +++- docker/MQTTManager/include/scenes/scene.hpp | 5 ++-- 11 files changed, 57 insertions(+), 16 deletions(-) diff --git a/docker/MQTTManager/include/entity_manager/entity_manager.cpp b/docker/MQTTManager/include/entity_manager/entity_manager.cpp index 983a442c..299839e9 100644 --- a/docker/MQTTManager/include/entity_manager/entity_manager.cpp +++ b/docker/MQTTManager/include/entity_manager/entity_manager.cpp @@ -783,7 +783,12 @@ void EntityManager::_command_callback(NSPanelMQTTManagerCommand &command) { SPDLOG_ERROR("Received command to toggle light entity in slot {} in page with ID {} but entity could not be cast to a light.", command.toggle_entity_from_entities_page().entity_slot(), command.toggle_entity_from_entities_page().entity_page_id()); } } else { - (*entity)->toggle(); + auto scene = std::dynamic_pointer_cast(*entity); + if (scene) { + scene->activate(EntityManager::get_room_id_for_panel_id(command.nspanel_id())); + } else { + (*entity)->toggle(); + } } } else { SPDLOG_DEBUG("Received command to toggle entity in slot {} in page with ID {} bot did not find such an entity.", command.toggle_entity_from_entities_page().entity_slot(), command.toggle_entity_from_entities_page().entity_page_id()); @@ -893,6 +898,14 @@ std::expected, EntityManager::EntityError> EntityManage return std::unexpected(EntityManager::EntityError::NOT_FOUND); } +std::optional EntityManager::get_room_id_for_panel_id(uint32_t nspanel_id) { + auto nspanel = EntityManager::get_nspanel_by_id(nspanel_id); + if (nspanel.has_value()) { + return (*nspanel)->get_default_room_id(); + } + return std::nullopt; +} + std::expected, EntityManager::EntityError> EntityManager::get_nspanel_by_mac(std::string mac) { SPDLOG_TRACE("Trying to find NSPanel by MAC {}", mac); std::lock_guard mutex_guard(EntityManager::_nspanels_mutex); diff --git a/docker/MQTTManager/include/entity_manager/entity_manager.hpp b/docker/MQTTManager/include/entity_manager/entity_manager.hpp index 6153c2aa..868d8bf2 100644 --- a/docker/MQTTManager/include/entity_manager/entity_manager.hpp +++ b/docker/MQTTManager/include/entity_manager/entity_manager.hpp @@ -206,6 +206,7 @@ class EntityManager { static std::expected, EntityError> get_nspanel_by_id(uint id); static std::expected, EntityError> get_nspanel_by_mac(std::string mac); + static std::optional get_room_id_for_panel_id(uint32_t nspanel_id); private: static inline std::vector> _entities; diff --git a/docker/MQTTManager/include/nspanel/nspanel.cpp b/docker/MQTTManager/include/nspanel/nspanel.cpp index 8a3b0f81..22b5727e 100644 --- a/docker/MQTTManager/include/nspanel/nspanel.cpp +++ b/docker/MQTTManager/include/nspanel/nspanel.cpp @@ -1,4 +1,5 @@ #include "nspanel.hpp" +#include #include "database_manager/database_manager.hpp" #include "entity_manager/entity_manager.hpp" #include "mqtt_manager/mqtt_manager.hpp" @@ -1401,7 +1402,12 @@ void NSPanel::command_callback(NSPanelMQTTManagerCommand &command) { auto entity = EntityManager::get_entity_by_id(MQTT_MANAGER_ENTITY_TYPE::ANY, this->_settings.button1_detached_mode_entity_id.value()); if (entity) { if ((*entity)->can_toggle()) { - (*entity)->toggle(); + auto scene = std::dynamic_pointer_cast(*entity); + if (scene) { + scene->activate(this->get_default_room_id()); + } else { + (*entity)->toggle(); + } } } else SPDLOG_ERROR("Tried to toggle detached entity via panel but no entity was found with configured ID '{}'.", this->_settings.button1_detached_mode_entity_id.value()); @@ -1430,7 +1436,12 @@ void NSPanel::command_callback(NSPanelMQTTManagerCommand &command) { auto entity = EntityManager::get_entity_by_id(MQTT_MANAGER_ENTITY_TYPE::ANY, this->_settings.button2_detached_mode_entity_id.value()); if (entity) { if ((*entity)->can_toggle()) { - (*entity)->toggle(); + auto scene = std::dynamic_pointer_cast(*entity); + if (scene) { + scene->activate(this->get_default_room_id()); + } else { + (*entity)->toggle(); + } } } else SPDLOG_ERROR("Tried to toggle detached entity via panel but no entity was found with configured ID '{}'.", this->_settings.button2_detached_mode_entity_id.value()); diff --git a/docker/MQTTManager/include/scenes/home_assistant_scene.cpp b/docker/MQTTManager/include/scenes/home_assistant_scene.cpp index e3443259..5de4dae6 100644 --- a/docker/MQTTManager/include/scenes/home_assistant_scene.cpp +++ b/docker/MQTTManager/include/scenes/home_assistant_scene.cpp @@ -36,8 +36,9 @@ void HomeAssistantScene::reload_config() { } } -void HomeAssistantScene::activate() { +void HomeAssistantScene::activate(std::optional triggering_room_id) { SPDLOG_INFO("Activating scene {}::{}.", this->_id, this->_name); + if (this->_entity_id.starts_with("scene.")) { nlohmann::json service_data; service_data["type"] = "call_service"; @@ -50,11 +51,23 @@ void HomeAssistantScene::activate() { nlohmann::json context; context["scene_name"] = this->_name; context["scene_id"] = this->_id; - auto room = EntityManager::get_room(this->_room_id); - if (!this->_is_global && room.has_value()) { - context["room_name"] = (*room)->get_name(); - context["room_id"] = this->_room_id; + + if (triggering_room_id.has_value()) { + auto triggering_room = EntityManager::get_room(*triggering_room_id); + if (triggering_room.has_value()) { + context["triggering_room_id"] = *triggering_room_id; + context["triggering_room_name"] = (*triggering_room)->get_name(); + } } + + if (!this->_is_global) { + auto scene_room = EntityManager::get_room(this->_room_id); + if (scene_room.has_value()) { + context["scene_room_id"] = this->_room_id; + context["scene_room_name"] = (*scene_room)->get_name(); + } + } + service_data["service_data"]["variables"]["nspanelmanager"] = context; SPDLOG_DEBUG("Sending script with context: {}", context.dump()); service_data["type"] = "call_service"; diff --git a/docker/MQTTManager/include/scenes/home_assistant_scene.hpp b/docker/MQTTManager/include/scenes/home_assistant_scene.hpp index e2acc260..d35b885c 100644 --- a/docker/MQTTManager/include/scenes/home_assistant_scene.hpp +++ b/docker/MQTTManager/include/scenes/home_assistant_scene.hpp @@ -7,7 +7,7 @@ class HomeAssistantScene : public Scene { public: HomeAssistantScene(uint32_t id); void reload_config(); - void activate(); + void activate(std::optional triggering_room_id = std::nullopt); void save(); void remove(); uint16_t get_id(); diff --git a/docker/MQTTManager/include/scenes/nspm_scene.cpp b/docker/MQTTManager/include/scenes/nspm_scene.cpp index 6019280e..82e25960 100644 --- a/docker/MQTTManager/include/scenes/nspm_scene.cpp +++ b/docker/MQTTManager/include/scenes/nspm_scene.cpp @@ -47,7 +47,7 @@ void NSPMScene::reload_config() { } } -void NSPMScene::activate() { +void NSPMScene::activate(std::optional triggering_room_id) { SPDLOG_INFO("Activating scene {}::{}.", this->_id, this->_name); try { auto light_states = database_manager::database.get_all(sqlite_orm::where(sqlite_orm::c(&database_manager::SceneLightState::scene_id) == this->_id)); diff --git a/docker/MQTTManager/include/scenes/nspm_scene.hpp b/docker/MQTTManager/include/scenes/nspm_scene.hpp index 58f1ba48..099cf586 100644 --- a/docker/MQTTManager/include/scenes/nspm_scene.hpp +++ b/docker/MQTTManager/include/scenes/nspm_scene.hpp @@ -15,7 +15,7 @@ class NSPMScene : public Scene { ~NSPMScene(); void reload_config(); - void activate(); + void activate(std::optional triggering_room_id = std::nullopt); void save(); void remove(); uint16_t get_id(); diff --git a/docker/MQTTManager/include/scenes/openhab_scene.cpp b/docker/MQTTManager/include/scenes/openhab_scene.cpp index a039d8c6..6ace767b 100644 --- a/docker/MQTTManager/include/scenes/openhab_scene.cpp +++ b/docker/MQTTManager/include/scenes/openhab_scene.cpp @@ -36,7 +36,7 @@ void OpenhabScene::reload_config() { } } -void OpenhabScene::activate() { +void OpenhabScene::activate(std::optional triggering_room_id) { SPDLOG_INFO("Activating scene {}::{}.", this->_id, this->_name); std::string openhab_trigger_scene_url = fmt::format("{}/rest/rules/{}/runnow", MqttManagerConfig::get_setting_with_default(MQTT_MANAGER_SETTING::OPENHAB_ADDRESS), this->_entity_id); diff --git a/docker/MQTTManager/include/scenes/openhab_scene.hpp b/docker/MQTTManager/include/scenes/openhab_scene.hpp index 93282c56..995ce811 100644 --- a/docker/MQTTManager/include/scenes/openhab_scene.hpp +++ b/docker/MQTTManager/include/scenes/openhab_scene.hpp @@ -8,7 +8,7 @@ class OpenhabScene : public Scene { public: OpenhabScene(uint32_t id); void reload_config(); - void activate(); + void activate(std::optional triggering_room_id = std::nullopt); void save(); void remove(); uint16_t get_id(); diff --git a/docker/MQTTManager/include/scenes/scene.cpp b/docker/MQTTManager/include/scenes/scene.cpp index 573b8c69..52b9da73 100644 --- a/docker/MQTTManager/include/scenes/scene.cpp +++ b/docker/MQTTManager/include/scenes/scene.cpp @@ -15,7 +15,9 @@ bool Scene::can_toggle() { } void Scene::toggle() { - this->activate(); + // Satisfies MqttManagerEntity's pure virtual. In practice all scene activations go through + // activate(triggering_room_id) directly, so this path should never be reached. + this->activate(std::nullopt); } bool Scene::is_global() { diff --git a/docker/MQTTManager/include/scenes/scene.hpp b/docker/MQTTManager/include/scenes/scene.hpp index aee11bd9..e70a591c 100644 --- a/docker/MQTTManager/include/scenes/scene.hpp +++ b/docker/MQTTManager/include/scenes/scene.hpp @@ -1,10 +1,11 @@ #ifndef MQTT_MANAGER_SCENE_BASE_H #define MQTT_MANAGER_SCENE_BASE_H #include "entity/entity.hpp" +#include class Scene : public MqttManagerEntity { public: - virtual void activate() = 0; + virtual void activate(std::optional triggering_room_id = std::nullopt) = 0; virtual void save() = 0; virtual void remove() = 0; virtual void reload_config() = 0; @@ -17,7 +18,7 @@ class Scene : public MqttManagerEntity { bool is_global(); // The same as activate() - void toggle(); + void toggle() override; virtual std::string get_name() = 0; virtual bool can_save() = 0; From 79f74c357e89f1b3f582011e068b3dcd26d034d2 Mon Sep 17 00:00:00 2001 From: James Mulcahy Date: Sun, 3 May 2026 14:39:58 -0700 Subject: [PATCH 2/3] Use std::expected instead of std::optional in get_room_id_for_panel_id Aligns with the existing pattern used by all other EntityManager lookups. Co-Authored-By: Claude Sonnet 4.6 --- .../MQTTManager/include/entity_manager/entity_manager.cpp | 7 ++++--- .../MQTTManager/include/entity_manager/entity_manager.hpp | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docker/MQTTManager/include/entity_manager/entity_manager.cpp b/docker/MQTTManager/include/entity_manager/entity_manager.cpp index 299839e9..061789f7 100644 --- a/docker/MQTTManager/include/entity_manager/entity_manager.cpp +++ b/docker/MQTTManager/include/entity_manager/entity_manager.cpp @@ -785,7 +785,8 @@ void EntityManager::_command_callback(NSPanelMQTTManagerCommand &command) { } else { auto scene = std::dynamic_pointer_cast(*entity); if (scene) { - scene->activate(EntityManager::get_room_id_for_panel_id(command.nspanel_id())); + auto room_id = EntityManager::get_room_id_for_panel_id(command.nspanel_id()); + scene->activate(room_id ? std::optional(*room_id) : std::nullopt); } else { (*entity)->toggle(); } @@ -898,12 +899,12 @@ std::expected, EntityManager::EntityError> EntityManage return std::unexpected(EntityManager::EntityError::NOT_FOUND); } -std::optional EntityManager::get_room_id_for_panel_id(uint32_t nspanel_id) { +std::expected EntityManager::get_room_id_for_panel_id(uint32_t nspanel_id) { auto nspanel = EntityManager::get_nspanel_by_id(nspanel_id); if (nspanel.has_value()) { return (*nspanel)->get_default_room_id(); } - return std::nullopt; + return std::unexpected(EntityManager::EntityError::NOT_FOUND); } std::expected, EntityManager::EntityError> EntityManager::get_nspanel_by_mac(std::string mac) { diff --git a/docker/MQTTManager/include/entity_manager/entity_manager.hpp b/docker/MQTTManager/include/entity_manager/entity_manager.hpp index 868d8bf2..19dbf67b 100644 --- a/docker/MQTTManager/include/entity_manager/entity_manager.hpp +++ b/docker/MQTTManager/include/entity_manager/entity_manager.hpp @@ -206,7 +206,7 @@ class EntityManager { static std::expected, EntityError> get_nspanel_by_id(uint id); static std::expected, EntityError> get_nspanel_by_mac(std::string mac); - static std::optional get_room_id_for_panel_id(uint32_t nspanel_id); + static std::expected get_room_id_for_panel_id(uint32_t nspanel_id); private: static inline std::vector> _entities; From 7cfc74691010a01d13b079e2b713f150c8fb8878 Mon Sep 17 00:00:00 2001 From: James Mulcahy Date: Tue, 5 May 2026 13:46:54 -0700 Subject: [PATCH 3/3] Address review comments -- swap from std::optional to std::expected --- docker/MQTTManager/include/entity_manager/entity_manager.cpp | 3 +-- docker/MQTTManager/include/scenes/home_assistant_scene.cpp | 2 +- docker/MQTTManager/include/scenes/home_assistant_scene.hpp | 2 +- docker/MQTTManager/include/scenes/nspm_scene.cpp | 2 +- docker/MQTTManager/include/scenes/nspm_scene.hpp | 2 +- docker/MQTTManager/include/scenes/openhab_scene.cpp | 2 +- docker/MQTTManager/include/scenes/openhab_scene.hpp | 2 +- docker/MQTTManager/include/scenes/scene.cpp | 2 +- docker/MQTTManager/include/scenes/scene.hpp | 5 +++-- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docker/MQTTManager/include/entity_manager/entity_manager.cpp b/docker/MQTTManager/include/entity_manager/entity_manager.cpp index 061789f7..0784a17f 100644 --- a/docker/MQTTManager/include/entity_manager/entity_manager.cpp +++ b/docker/MQTTManager/include/entity_manager/entity_manager.cpp @@ -785,8 +785,7 @@ void EntityManager::_command_callback(NSPanelMQTTManagerCommand &command) { } else { auto scene = std::dynamic_pointer_cast(*entity); if (scene) { - auto room_id = EntityManager::get_room_id_for_panel_id(command.nspanel_id()); - scene->activate(room_id ? std::optional(*room_id) : std::nullopt); + scene->activate(EntityManager::get_room_id_for_panel_id(command.nspanel_id())); } else { (*entity)->toggle(); } diff --git a/docker/MQTTManager/include/scenes/home_assistant_scene.cpp b/docker/MQTTManager/include/scenes/home_assistant_scene.cpp index 5de4dae6..82cc3c8e 100644 --- a/docker/MQTTManager/include/scenes/home_assistant_scene.cpp +++ b/docker/MQTTManager/include/scenes/home_assistant_scene.cpp @@ -36,7 +36,7 @@ void HomeAssistantScene::reload_config() { } } -void HomeAssistantScene::activate(std::optional triggering_room_id) { +void HomeAssistantScene::activate(std::expected triggering_room_id) { SPDLOG_INFO("Activating scene {}::{}.", this->_id, this->_name); if (this->_entity_id.starts_with("scene.")) { diff --git a/docker/MQTTManager/include/scenes/home_assistant_scene.hpp b/docker/MQTTManager/include/scenes/home_assistant_scene.hpp index d35b885c..5c301370 100644 --- a/docker/MQTTManager/include/scenes/home_assistant_scene.hpp +++ b/docker/MQTTManager/include/scenes/home_assistant_scene.hpp @@ -7,7 +7,7 @@ class HomeAssistantScene : public Scene { public: HomeAssistantScene(uint32_t id); void reload_config(); - void activate(std::optional triggering_room_id = std::nullopt); + void activate(std::expected triggering_room_id = std::unexpected(EntityManager::EntityError::NOT_FOUND)); void save(); void remove(); uint16_t get_id(); diff --git a/docker/MQTTManager/include/scenes/nspm_scene.cpp b/docker/MQTTManager/include/scenes/nspm_scene.cpp index 82e25960..e0e5bab4 100644 --- a/docker/MQTTManager/include/scenes/nspm_scene.cpp +++ b/docker/MQTTManager/include/scenes/nspm_scene.cpp @@ -47,7 +47,7 @@ void NSPMScene::reload_config() { } } -void NSPMScene::activate(std::optional triggering_room_id) { +void NSPMScene::activate(std::expected triggering_room_id) { SPDLOG_INFO("Activating scene {}::{}.", this->_id, this->_name); try { auto light_states = database_manager::database.get_all(sqlite_orm::where(sqlite_orm::c(&database_manager::SceneLightState::scene_id) == this->_id)); diff --git a/docker/MQTTManager/include/scenes/nspm_scene.hpp b/docker/MQTTManager/include/scenes/nspm_scene.hpp index 099cf586..0bbd2cc9 100644 --- a/docker/MQTTManager/include/scenes/nspm_scene.hpp +++ b/docker/MQTTManager/include/scenes/nspm_scene.hpp @@ -15,7 +15,7 @@ class NSPMScene : public Scene { ~NSPMScene(); void reload_config(); - void activate(std::optional triggering_room_id = std::nullopt); + void activate(std::expected triggering_room_id = std::unexpected(EntityManager::EntityError::NOT_FOUND)); void save(); void remove(); uint16_t get_id(); diff --git a/docker/MQTTManager/include/scenes/openhab_scene.cpp b/docker/MQTTManager/include/scenes/openhab_scene.cpp index 6ace767b..bc7e5e76 100644 --- a/docker/MQTTManager/include/scenes/openhab_scene.cpp +++ b/docker/MQTTManager/include/scenes/openhab_scene.cpp @@ -36,7 +36,7 @@ void OpenhabScene::reload_config() { } } -void OpenhabScene::activate(std::optional triggering_room_id) { +void OpenhabScene::activate(std::expected triggering_room_id) { SPDLOG_INFO("Activating scene {}::{}.", this->_id, this->_name); std::string openhab_trigger_scene_url = fmt::format("{}/rest/rules/{}/runnow", MqttManagerConfig::get_setting_with_default(MQTT_MANAGER_SETTING::OPENHAB_ADDRESS), this->_entity_id); diff --git a/docker/MQTTManager/include/scenes/openhab_scene.hpp b/docker/MQTTManager/include/scenes/openhab_scene.hpp index 995ce811..ba47893e 100644 --- a/docker/MQTTManager/include/scenes/openhab_scene.hpp +++ b/docker/MQTTManager/include/scenes/openhab_scene.hpp @@ -8,7 +8,7 @@ class OpenhabScene : public Scene { public: OpenhabScene(uint32_t id); void reload_config(); - void activate(std::optional triggering_room_id = std::nullopt); + void activate(std::expected triggering_room_id = std::unexpected(EntityManager::EntityError::NOT_FOUND)); void save(); void remove(); uint16_t get_id(); diff --git a/docker/MQTTManager/include/scenes/scene.cpp b/docker/MQTTManager/include/scenes/scene.cpp index 52b9da73..4ef9e3ad 100644 --- a/docker/MQTTManager/include/scenes/scene.cpp +++ b/docker/MQTTManager/include/scenes/scene.cpp @@ -17,7 +17,7 @@ bool Scene::can_toggle() { void Scene::toggle() { // Satisfies MqttManagerEntity's pure virtual. In practice all scene activations go through // activate(triggering_room_id) directly, so this path should never be reached. - this->activate(std::nullopt); + this->activate(std::unexpected(EntityManager::EntityError::NOT_FOUND)); } bool Scene::is_global() { diff --git a/docker/MQTTManager/include/scenes/scene.hpp b/docker/MQTTManager/include/scenes/scene.hpp index e70a591c..b917f9a5 100644 --- a/docker/MQTTManager/include/scenes/scene.hpp +++ b/docker/MQTTManager/include/scenes/scene.hpp @@ -1,11 +1,12 @@ #ifndef MQTT_MANAGER_SCENE_BASE_H #define MQTT_MANAGER_SCENE_BASE_H #include "entity/entity.hpp" -#include +#include "entity_manager/entity_manager.hpp" +#include class Scene : public MqttManagerEntity { public: - virtual void activate(std::optional triggering_room_id = std::nullopt) = 0; + virtual void activate(std::expected triggering_room_id = std::unexpected(EntityManager::EntityError::NOT_FOUND)) = 0; virtual void save() = 0; virtual void remove() = 0; virtual void reload_config() = 0;