Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
61cfb58
feat: add Homey integration documentation and code formatting
eelco2k Nov 11, 2025
8a2dab0
Updated homey_implementation.md readme
eelco2k Nov 11, 2025
86a02c0
refactor(ui): reorganize CSS classes and add Homey controller support
eelco2k Nov 11, 2025
38c74ee
fixes for m4's skip getcwd test
eelco2k Nov 12, 2025
4e2bded
feat(build): modernize CMake build system and upgrade to version 3.23
eelco2k Nov 12, 2025
ffb1b24
fix(homey): improve error handling for misconfigured scenes
eelco2k Nov 12, 2025
882b8f8
feat: add Homey backend support for scene entities
eelco2k Nov 12, 2025
65379fd
fix for compiling specifically in MacOS.
eelco2k Nov 13, 2025
06331df
created PUT requests in WebHelper.cpp. Homey makes use of PUT request…
eelco2k Nov 13, 2025
af7d98b
allow custom docker port in arguments list of docker-build bash scripts
eelco2k Nov 13, 2025
cd56217
allow fo custom docker volume binding folder in docker bash scripts
eelco2k Nov 13, 2025
908dbc3
make specific volume binding to data_volume naming convention
eelco2k Nov 13, 2025
59b3677
wrong PUT update state address url for Homey light/switch/button/ther…
eelco2k Nov 13, 2025
9f7e436
incorrect Authorization Headers for PUT requests
eelco2k Nov 13, 2025
4b299c0
refactor(homey): map light capabilities from database fields
eelco2k Nov 13, 2025
970663d
fix(homey): update mood API endpoint from trigger to activate
eelco2k Nov 13, 2025
7e091bb
fix(homey): update mood API endpoint from trigger to activate
eelco2k Nov 13, 2025
b5dca88
feat: add state management to button entities and improve Homey integ…
eelco2k Nov 13, 2025
73182ef
fix: resolve string lifetime issue in HTTP header creation
eelco2k Nov 13, 2025
d934211
fix(homey): update Homey API mood endpoint to correct path
eelco2k Nov 13, 2025
7fc001c
moods Homey API request url to /moods/mood
eelco2k Nov 13, 2025
ca6e347
fix(homey): correct API endpoint for mood activation from /activate t…
eelco2k Nov 13, 2025
d952529
updated readme's
eelco2k Nov 15, 2025
ae6acdf
small readme changes
eelco2k Nov 15, 2025
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
704 changes: 704 additions & 0 deletions HOMEY_INTEGRATION.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# NSPanelManager

Standardised Smart Home control for Home Assistant and Openhab users using the original Sonoff NSPanel.
Standardised Smart Home control for Home Assistant, Openhab and Athom Homey Pro users using the original Sonoff NSPanel.

NSPanel Manager is a custom software solution for the Sonoff NSPanel (not the NSPanel pro).
The software is designed to be easy to use on a day-to-day basis and to easily manage multiple NSPanels around
Expand Down
26 changes: 19 additions & 7 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,28 @@ SHELL ["/bin/bash", "-c"]
RUN echo "Running on $BUILDPLATFORM, building for $TARGETPLATFORM"
COPY MQTTManager/ /MQTTManager/

# Create config.site to skip problematic configure tests
RUN echo 'ac_cv_func_getcwd_path_max=yes' > /etc/config.site
ENV CONFIG_SITE=/etc/config.site

# Only build MQTTManager during Docker build if is is not a devel mode.
RUN if [ "$IS_DEVEL" != "yes" ]; then apt-get update \
&& apt-get -y install cmake build-essential curl \
&& pip install -U conan; fi
RUN if [ "$IS_DEVEL" != "yes" ]; then \
apt-get update \
&& apt-get -y install cmake build-essential curl m4 \
&& pip install -U conan; \
fi

RUN if [ "$IS_DEVEL" != "yes" ]; then conan profile detect --force && echo 'core.cache:storage_path=/MQTTManager/conan_cache/' > ~/.conan2/global.conf \
RUN if [ "$IS_DEVEL" != "yes" ]; then \
conan profile detect --force && echo 'core.cache:storage_path=/MQTTManager/conan_cache/' > ~/.conan2/global.conf \
&& sed -i "s|cppstd=gnu14|cppstd=gnu23|g" /root/.conan2/profiles/default \
&& sed -i "s|build_type=Release|build_type=Debug|g" /root/.conan2/profiles/default; fi
&& sed -i "s|build_type=Release|build_type=Debug|g" /root/.conan2/profiles/default; \
fi

RUN if [ -z "$no_mqttmanager_build" ]; then /bin/bash /MQTTManager/compile_mqttmanager.sh --target-platform "$TARGETPLATFORM" --strip; else echo "Not building MQTTManager."; fi
RUN if [ -z "$no_mqttmanager_build" ]; then \
/bin/bash /MQTTManager/compile_mqttmanager.sh --target-platform "$TARGETPLATFORM" --strip; \
else \
echo "Not building MQTTManager."; \
fi

FROM python:3.12.5-bookworm
ARG no_mqttmanager_build
Expand All @@ -36,7 +48,7 @@ COPY --from=build /MQTTManager/build /MQTTManager/build
RUN apt-get update && apt-get -y upgrade

# Install software needed to build the manager
RUN if [ "$IS_DEVEL" == "yes" ]; then apt-get install -y --no-install-recommends cmake build-essential gdb curl npm postgresql-client curl inotify-tools net-tools build-essential protobuf-c-compiler \
RUN if [ "$IS_DEVEL" == "yes" ]; then apt-get install -y --no-install-recommends cmake build-essential gdb curl npm postgresql-client curl inotify-tools net-tools build-essential protobuf-c-compiler m4 \
&& pip install conan conan-check-updates && conan profile detect --force \
&& echo 'core.cache:storage_path=/MQTTManager/conan_cache/' > ~/.conan2/global.conf \
&& sed -i "s|cppstd=gnu17|cppstd=gnu23|g" /root/.conan2/profiles/default \
Expand Down
270 changes: 186 additions & 84 deletions docker/MQTTManager/CMakeLists.txt

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion docker/MQTTManager/CMakeUserPresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"conan": {}
},
"include": [
"build/build/Debug/generators/CMakePresets.json",
"build/Debug/generators/CMakePresets.json"
]
}
13 changes: 13 additions & 0 deletions docker/MQTTManager/compile_mqttmanager.sh
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,19 @@ else
BUILD_SHARED_LIBS=OFF
fi

# Workaround: Patch m4's configure script to skip getcwd test
echo 'ac_cv_func_getcwd_path_max=yes' > /etc/config.site
export CONFIG_SITE=/etc/config.site

if [[ "$(uname)" == "Darwin" ]]; then
echo "🔧 Patching m4's configure script to skip getcwd test (avoiding hang on macOS)..."
find /MQTTManager/conan_cache -type f -path "*/src/configure" -exec grep -q "checking whether getcwd handles long file names properly" {} \; -exec sed -i '' '/checking whether getcwd handles long file names properly/ a\
gl_cv_func_getcwd_path_max=yes\
' {} \;
else
echo "✅ Skipping m4 patch (not macOS)"
fi

echo "Conan profile: "
cat /root/.conan2/profiles/default

Expand Down
27 changes: 23 additions & 4 deletions docker/MQTTManager/include/button/button.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@
#include <string>
#include <unistd.h>

ButtonEntity::ButtonEntity(uint32_t light_id) {
this->_id = light_id;
ButtonEntity::ButtonEntity(uint32_t button_id)
{
this->_id = button_id;
this->reload_config();

// Build MQTT Topics
std::string mqtt_base_topic = fmt::format("nspanel/entities/button/{}/", this->_id);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see that this is used anywhere. Is there a reason for it being here?

this->_current_state = false;
this->_requested_state = false;

CommandManager::attach_callback(boost::bind(&ButtonEntity::command_callback, this, _1));

SPDLOG_DEBUG("Button {}::{} base loaded.", this->_id, this->_name);
Expand All @@ -45,9 +51,17 @@ void ButtonEntity::reload_config() {
std::string controller = entity_data["controller"];
if (controller.compare("home_assistant") == 0) {
this->_controller = MQTT_MANAGER_ENTITY_CONTROLLER::HOME_ASSISTANT;
} else if (controller.compare("nspm") == 0) {
}
else if (controller.compare("homey") == 0)
{
this->_controller = MQTT_MANAGER_ENTITY_CONTROLLER::HOMEY;
}
else if (controller.compare("nspm") == 0)
{
this->_controller = MQTT_MANAGER_ENTITY_CONTROLLER::NSPM;
} else {
}
else
{
SPDLOG_ERROR("Got unknown controller ({}) for light {}::{}. Will default to HOME_ASSISTANT.", std::string(controller), this->_id, this->_name);
this->_controller = MQTT_MANAGER_ENTITY_CONTROLLER::HOME_ASSISTANT;
}
Expand Down Expand Up @@ -104,7 +118,12 @@ bool ButtonEntity::can_toggle() {
}

void ButtonEntity::toggle() {
this->_requested_state = !this->_requested_state;
this->send_state_update_to_controller();
if (MqttManagerConfig::get_setting_with_default<bool>(MQTT_MANAGER_SETTING::OPTIMISTIC_MODE))
{
this->_current_state = !this->_current_state;
}
}

std::string_view ButtonEntity::get_icon() {
Expand Down
8 changes: 8 additions & 0 deletions docker/MQTTManager/include/button/button.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ class ButtonEntity : public MqttManagerEntity {
*/
uint16_t get_id();

/**
* Get the on/off state of the switch.
*/
bool get_state();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function has not been implemented in the .cpp file.


/**
* Get the friendly name for the button.
*/
Expand Down Expand Up @@ -96,6 +101,9 @@ class ButtonEntity : public MqttManagerEntity {
std::mutex _entity_data_mutex;
nlohmann::json _entity_data;

bool _current_state;
bool _requested_state;

boost::signals2::signal<void(ButtonEntity *)> _button_destroyed_callbacks;
};

Expand Down
123 changes: 123 additions & 0 deletions docker/MQTTManager/include/button/homey_button.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#include "homey_button.hpp"
#include "database_manager/database_manager.hpp"
#include "entity/entity.hpp"
#include "web_helper/WebHelper.hpp"
#include "mqtt_manager/mqtt_manager.hpp"
#include "mqtt_manager_config/mqtt_manager_config.hpp"
#include <boost/algorithm/string/predicate.hpp>
#include <boost/bind.hpp>
#include <boost/exception/diagnostic_information.hpp>
#include <chrono>
#include <cstdint>
#include <homey_manager/homey_manager.hpp>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include <string>
#include <switch/switch.hpp>

HomeyButton::HomeyButton(uint32_t button_id) : ButtonEntity(button_id)
{
if (this->_controller != MQTT_MANAGER_ENTITY_CONTROLLER::HOMEY)
{
SPDLOG_ERROR("HomeyButton has not been recognized as controlled by HOMEY. Will stop processing button.");
return;
}

nlohmann::json entity_data;
try
{
auto button_entity = database_manager::database.get<database_manager::Entity>(this->_id);
entity_data = button_entity.get_entity_data_json();
}
catch (const std::exception &e)
{
SPDLOG_ERROR("Failed to load button {}: {}", this->_id, e.what());
return;
}

if (entity_data.contains("homey_device_id"))
{
this->_homey_device_id = entity_data["homey_device_id"];
}
else
{
SPDLOG_ERROR("No homey_device_id defined for Button {}::{}", this->_id, this->_name);
return;
}

SPDLOG_DEBUG("Loaded Homey button {}::{}, device ID: {}", this->_id, this->_name, this->_homey_device_id);
HomeyManager::attach_event_observer(this->_homey_device_id, boost::bind(&HomeyButton::homey_event_callback, this, _1));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you've written below, buttons are one way only. Perhaps comment out this line and the on in the destructor that attaches event observers as they are not needed?

}

HomeyButton::~HomeyButton()
{
HomeyManager::detach_event_observer(this->_homey_device_id, boost::bind(&HomeyButton::homey_event_callback, this, _1));
}

void HomeyButton::send_state_update_to_controller()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason this is done over HTTP PUT? Does Homey not support doing this over websockets? Websockets would be preferable as we reuse an already established connection and that is therefore faster.

{
SPDLOG_DEBUG("Homey button {}::{} send_state_update_to_controller (trigger button press). State: {}", this->_id, this->_name, this->_requested_state);

// Get Homey connection settings
auto homey_address = MqttManagerConfig::get_setting_with_default<std::string>(MQTT_MANAGER_SETTING::HOMEY_ADDRESS);
auto homey_token = MqttManagerConfig::get_setting_with_default<std::string>(MQTT_MANAGER_SETTING::HOMEY_TOKEN);

if (homey_address.empty() || homey_token.empty())
{
SPDLOG_ERROR("Homey address or token not configured for button {}::{}", this->_id, this->_name);
return;
}

// Construct URL: http://{homey_address}/api/manager/devices/device/{device_id}/capability/onoff
std::string url = fmt::format("http://{}/api/manager/devices/device/{}/capability/onoff", homey_address, this->_homey_device_id);

// Create request body - button trigger uses null value
nlohmann::json request_body;
request_body["value"] = this->_requested_state;

// Send HTTP PUT request with bearer token authentication
try
{
// Create header strings with proper lifetime management
std::string auth_header = fmt::format("Authorization: Bearer {}", homey_token);
std::list<const char *> headers = {
auth_header.c_str(),
"Content-Type: application/json"};

std::string response_data;
std::string put_data = request_body.dump();

if (WebHelper::perform_put_request(&url, &response_data, &headers, &put_data))
{
SPDLOG_DEBUG("Homey button {}::{} trigger response: {}", this->_id, this->_name, response_data);
}
else
{
SPDLOG_ERROR("Failed to trigger Homey button {}::{}", this->_id, this->_name);
}
}
catch (const std::exception &e)
{
SPDLOG_ERROR("Failed to trigger Homey button {}::{}: {}", this->_id, this->_name, e.what());
}

// Buttons don't have persistent state, so no optimistic mode handling needed
}

void HomeyButton::homey_event_callback(nlohmann::json data)
{
try
{
// Homey WebSocket sends: {"id": "device-uuid", "capabilitiesObj": {...}}
// Buttons typically don't send state updates, but we'll handle them if they do
SPDLOG_DEBUG("Got event update for Homey button {}::{}.", this->_id, this->_name);

// Buttons are typically one-way (trigger only), so no state to process
// But we keep this callback for potential future use
}
catch (std::exception &e)
{
SPDLOG_ERROR("Caught exception when processing Homey event for button {}::{}: {}",
this->_id, this->_name, boost::diagnostic_information(e, true));
}
}
19 changes: 19 additions & 0 deletions docker/MQTTManager/include/button/homey_button.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#ifndef MQTT_MANAGER_HOMEY_BUTTON
#define MQTT_MANAGER_HOMEY_BUTTON

#include "button.hpp"
#include <string>

class HomeyButton : public ButtonEntity
{
public:
HomeyButton(uint32_t button_id);
~HomeyButton();
void send_state_update_to_controller();
void homey_event_callback(nlohmann::json event_data);

private:
std::string _homey_device_id;
};

#endif // !MQTT_MANAGER_HOMEY_BUTTON
6 changes: 4 additions & 2 deletions docker/MQTTManager/include/entity/entity.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ enum MQTT_MANAGER_ENTITY_TYPE {
SCENE,
};

enum MQTT_MANAGER_ENTITY_CONTROLLER {
enum MQTT_MANAGER_ENTITY_CONTROLLER
{
NONE, // None is only used to indicate that an entity is not set.
NSPM, // We control the entity using the NSPanel Manager.
HOME_ASSISTANT, // Home assistant is the owner of this entity.
OPENHAB // OpenHAB is the owner of this entity.
OPENHAB, // OpenHAB is the owner of this entity.
HOMEY, // Homey is the owner of this entity.
};

class MqttManagerEntity {
Expand Down
7 changes: 5 additions & 2 deletions docker/MQTTManager/include/entity/entity_icons.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <cstdint>

class EntityIcons {
class EntityIcons
{
public:
// Entity Icons
static constexpr const char *entity_icon_switch_on = "s";
Expand All @@ -11,6 +12,7 @@ class EntityIcons {
static constexpr const char *save_icon = "w";
static constexpr const char *home_assistant_icon = "x";
static constexpr const char *openhab_icon = "y";
static constexpr const char *homey_icon = "{";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you added this to the font? This is somewhat specially managed by @cablesandcoffee. It needs to be added to the font used by the NSPanel firmware in order to show up correctly.


// Thermostat Icons
static constexpr const char *heating = "!";
Expand All @@ -37,7 +39,8 @@ class EntityIcons {
static constexpr const char *fan3 = "6";
};

class GUI_Colors {
class GUI_Colors
{
public:
static constexpr const uint16_t icon_color_off = 65535;
static constexpr const uint16_t icon_color_on = 65024;
Expand Down
Loading