Skip to content
Draft
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
2 changes: 1 addition & 1 deletion docker/MQTTManager/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ set_target_properties(MQTTManager_Thermostat PROPERTIES PUBLIC_HEADER ${CMAKE_IN
target_include_directories(MQTTManager_Thermostat PUBLIC ${CMAKE_INCLUDE_SRC_DIRECTORY})
target_link_libraries(MQTTManager_Thermostat MQTTManager_HomeAssistantManager MQTTManager_OpenhabManager spdlog::spdlog nlohmann_json::nlohmann_json MQTT_Manager Boost::boost MQTTManager_Entity Protobuf_MQTTManager MQTTManager_CommandManager gtest::gtest)

add_library(MQTTManager_MediaPlayer STATIC ${CMAKE_INCLUDE_SRC_DIRECTORY}/media_player/media_player.cpp ${CMAKE_INCLUDE_SRC_DIRECTORY}/media_player/media_player.hpp ${CMAKE_INCLUDE_SRC_DIRECTORY}/media_player/home_assistant_media_player.hpp ${CMAKE_INCLUDE_SRC_DIRECTORY}/media_player/home_assistant_media_player.cpp ${CMAKE_INCLUDE_SRC_DIRECTORY}/media_player/home_assistant_source_volume.hpp ${CMAKE_INCLUDE_SRC_DIRECTORY}/media_player/home_assistant_source_volume.cpp)
add_library(MQTTManager_MediaPlayer STATIC ${CMAKE_INCLUDE_SRC_DIRECTORY}/media_player/media_player.cpp ${CMAKE_INCLUDE_SRC_DIRECTORY}/media_player/media_player.hpp ${CMAKE_INCLUDE_SRC_DIRECTORY}/media_player/home_assistant_media_player.hpp ${CMAKE_INCLUDE_SRC_DIRECTORY}/media_player/home_assistant_media_player.cpp ${CMAKE_INCLUDE_SRC_DIRECTORY}/media_player/home_assistant_source_volume.hpp ${CMAKE_INCLUDE_SRC_DIRECTORY}/media_player/home_assistant_source_volume.cpp ${CMAKE_INCLUDE_SRC_DIRECTORY}/media_player/home_assistant_source_player.hpp ${CMAKE_INCLUDE_SRC_DIRECTORY}/media_player/home_assistant_source_player.cpp)
set_target_properties(MQTTManager_MediaPlayer PROPERTIES PUBLIC_HEADER ${CMAKE_INCLUDE_SRC_DIRECTORY}/media_player/media_player.hpp)
target_include_directories(MQTTManager_MediaPlayer PUBLIC ${CMAKE_INCLUDE_SRC_DIRECTORY})
target_link_libraries(MQTTManager_MediaPlayer MQTTManager_HomeAssistantManager MQTTManager_WebHelper spdlog::spdlog nlohmann_json::nlohmann_json MQTT_Manager Boost::boost MQTTManager_Entity Protobuf_MQTTManager MQTTManager_CommandManager gtest::gtest)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "home_assistant_media_player.hpp"
#include "database_manager/database_manager.hpp"
#include "entity/entity.hpp"
#include "media_player/home_assistant_source_player.hpp"
#include "media_player/home_assistant_source_volume.hpp"
#include "media_player/media_player.hpp"
#include "protobuf_nspanel_entity.pb.h"
Expand Down Expand Up @@ -67,7 +68,7 @@ void HomeAssistantMediaPlayer::reload_config() {

this->_create_source_volume_strategy(entity_data.value_or(nlohmann::json::object()));
// The name or source volume might have changed, unchanged states are not sent again.
this->_update_source_volume_and_send_state();
this->_update_source_player_and_send_state();

if (home_assistant_name_changed && !this->_home_assistant_name.empty()) {
// This will also request the current state from Home Assistant.
Expand Down Expand Up @@ -113,7 +114,7 @@ void HomeAssistantMediaPlayer::_create_source_volume_strategy(const nlohmann::js
}

auto strategy = std::shared_ptr<HomeAssistantSourceVolumeStrategy>(HomeAssistantSourceVolumeStrategy::create(this->_id, entity_data, [this]() {
this->_update_source_volume_and_send_state();
this->_update_source_player_and_send_state();
}));

nlohmann::json player_attributes;
Expand All @@ -132,11 +133,12 @@ std::shared_ptr<HomeAssistantSourceVolumeStrategy> HomeAssistantMediaPlayer::_ge
return this->_source_volume_strategy;
}

void HomeAssistantMediaPlayer::_update_source_volume_and_send_state() {
void HomeAssistantMediaPlayer::_update_source_player_and_send_state() {
auto strategy = this->_get_source_volume_strategy();
std::optional<uint8_t> source_volume = strategy ? strategy->get_source_volume() : std::nullopt;
this->_has_source_volume = source_volume.has_value();
this->_source_volume = source_volume.value_or(0);
this->_set_source_player_sources(strategy ? strategy->get_source_player_sources() : std::nullopt);
this->send_state_update_to_nspanel();
}

Expand Down Expand Up @@ -169,6 +171,7 @@ void HomeAssistantMediaPlayer::home_assistant_event_callback(nlohmann::json data
// Entity was removed from Home Assistant.
this->_state = NSPanelEntityState_MediaPlayer_PlaybackState_UNKNOWN;
this->_set_album_art_source("");
this->_set_player_sources(MediaPlayerSources());
} else {
std::string new_state = new_state_data.contains("state") && new_state_data["state"].is_string() ? new_state_data["state"].get<std::string>() : "";
if (new_state.compare("playing") == 0) {
Expand Down Expand Up @@ -213,6 +216,7 @@ void HomeAssistantMediaPlayer::home_assistant_event_callback(nlohmann::json data
this->_can_previous_track = supported_features & HomeAssistantMediaPlayer::feature_previous_track;
this->_can_set_volume = supported_features & HomeAssistantMediaPlayer::feature_volume_set;
this->_can_mute = supported_features & HomeAssistantMediaPlayer::feature_volume_mute;
this->_set_player_sources(HomeAssistantSourcePlayer::sources_from_attributes(attributes));
}

std::shared_ptr<HomeAssistantSourceVolumeStrategy> strategy;
Expand All @@ -229,7 +233,7 @@ void HomeAssistantMediaPlayer::home_assistant_event_callback(nlohmann::json data
return;
}

this->_update_source_volume_and_send_state();
this->_update_source_player_and_send_state();
if (previous_icon_color != this->get_icon_color()) {
this->_signal_entity_changed();
}
Expand Down Expand Up @@ -310,3 +314,14 @@ bool HomeAssistantMediaPlayer::_download_album_art(const std::string &source, st
void HomeAssistantMediaPlayer::set_muted(bool muted) {
this->_call_service("volume_mute", {{"is_volume_muted", muted}});
}

void HomeAssistantMediaPlayer::select_player_source(const std::string &source) {
this->_call_service("select_source", {{"source", source}});
}

void HomeAssistantMediaPlayer::select_source_player_source(const std::string &source) {
auto strategy = this->_get_source_volume_strategy();
if (strategy) {
strategy->select_source_player_source(source);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class HomeAssistantMediaPlayer : public MediaPlayerEntity {
void set_volume(uint8_t volume);
void set_source_volume(uint8_t volume);
void set_muted(bool muted);
void select_player_source(const std::string &source);
void select_source_player_source(const std::string &source);

protected:
/**
Expand Down Expand Up @@ -50,9 +52,9 @@ class HomeAssistantMediaPlayer : public MediaPlayerEntity {
std::shared_ptr<HomeAssistantSourceVolumeStrategy> _get_source_volume_strategy();

/**
* Update source volume from the strategy and send the new state to the NSPanel.
* Update source volume and source player sources from the strategy and send the new state to the NSPanel.
*/
void _update_source_volume_and_send_state();
void _update_source_player_and_send_state();

/**
* Call a "media_player" domain service in Home Assistant.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#include "media_player/home_assistant_source_player.hpp"
#include <algorithm>
#include <boost/bind.hpp>
#include <cmath>
#include <home_assistant_manager/home_assistant_manager.hpp>
#include <mutex>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include <string>

HomeAssistantSourcePlayer::HomeAssistantSourcePlayer(std::string source_entity_attribute, std::function<void()> on_change) {
this->_source_entity_attribute = source_entity_attribute;
this->_on_change = on_change;
}

HomeAssistantSourcePlayer::~HomeAssistantSourcePlayer() {
std::lock_guard<std::mutex> lock_guard(this->_mutex);
if (!this->_entity_id.empty()) {
HomeAssistantManager::detach_event_observer(this->_entity_id, boost::bind(&HomeAssistantSourcePlayer::home_assistant_event_callback, this, _1));
}
}

void HomeAssistantSourcePlayer::update_from_player_attributes(const nlohmann::json &attributes) {
std::string new_entity_id;
if (attributes.contains(this->_source_entity_attribute) && attributes[this->_source_entity_attribute].is_string()) {
new_entity_id = attributes[this->_source_entity_attribute];
}

std::string old_entity_id;
{
std::lock_guard<std::mutex> lock_guard(this->_mutex);
if (new_entity_id.compare(this->_entity_id) == 0) {
return;
}
old_entity_id = this->_entity_id;
this->_entity_id = new_entity_id;
// Forget the previous source player straight away, so its sources are never offered for the new one.
this->_volume.reset();
this->_sources = MediaPlayerSources();
}

SPDLOG_DEBUG("Media player source changed from '{}' to '{}'.", old_entity_id, new_entity_id);
if (!old_entity_id.empty()) {
HomeAssistantManager::detach_event_observer(old_entity_id, boost::bind(&HomeAssistantSourcePlayer::home_assistant_event_callback, this, _1));
}
if (!new_entity_id.empty()) {
// Attaching an observer requests all states from Home Assistant, the current state of the source player will arrive through the event callback.
HomeAssistantManager::attach_event_observer(new_entity_id, boost::bind(&HomeAssistantSourcePlayer::home_assistant_event_callback, this, _1));
}
}

std::string HomeAssistantSourcePlayer::get_entity_id() {
std::lock_guard<std::mutex> lock_guard(this->_mutex);
return this->_entity_id;
}

std::optional<uint8_t> HomeAssistantSourcePlayer::get_volume() {
std::lock_guard<std::mutex> lock_guard(this->_mutex);
return this->_volume;
}

std::optional<MediaPlayerSources> HomeAssistantSourcePlayer::get_sources() {
std::lock_guard<std::mutex> lock_guard(this->_mutex);
if (this->_entity_id.empty()) {
return std::nullopt;
}
return this->_sources;
}

void HomeAssistantSourcePlayer::set_volume(uint8_t volume) {
std::string entity_id = this->get_entity_id();
if (entity_id.empty()) {
SPDLOG_WARN("Received command to set source volume but the media player does not report a source entity.");
return;
}
HomeAssistantSourcePlayer::call_service(entity_id, "volume_set", {{"volume_level", volume / 100.0}});
}

void HomeAssistantSourcePlayer::select_source(const std::string &source) {
std::string entity_id = this->get_entity_id();
if (entity_id.empty()) {
SPDLOG_WARN("Received command to select a source player source but the media player does not report a source entity.");
return;
}
HomeAssistantSourcePlayer::call_service(entity_id, "select_source", {{"source", source}});
}

void HomeAssistantSourcePlayer::home_assistant_event_callback(nlohmann::json event_data) {
try {
if (!event_data.contains("event") || !event_data["event"].contains("data")) {
return;
}
nlohmann::json data = event_data["event"]["data"];
if (!data.contains("entity_id") || !data["entity_id"].is_string()) {
return;
}

bool changed = false;
{
std::lock_guard<std::mutex> lock_guard(this->_mutex);
if (data["entity_id"].get<std::string>().compare(this->_entity_id) != 0) {
return; // Event for a previous source.
}

std::optional<uint8_t> new_volume = this->_volume;
MediaPlayerSources new_sources;
if (!data.contains("new_state") || data["new_state"].is_null()) {
new_volume.reset();
} else if (data["new_state"].contains("attributes") && data["new_state"]["attributes"].is_object()) {
const nlohmann::json &attributes = data["new_state"]["attributes"];
// Home Assistant does not report volume while a media player is off, keep the last known value in that case.
if (attributes.contains("volume_level")) {
new_volume = HomeAssistantSourcePlayer::volume_from_json(attributes["volume_level"]);
}
new_sources = HomeAssistantSourcePlayer::sources_from_attributes(attributes);
}

changed = new_volume != this->_volume || new_sources != this->_sources;
this->_volume = new_volume;
this->_sources = std::move(new_sources);
}

if (changed && this->_on_change) {
this->_on_change();
}
} catch (std::exception &e) {
SPDLOG_ERROR("Caught exception during processing of media player source event. Error: {}", e.what());
}
}

MediaPlayerSources HomeAssistantSourcePlayer::sources_from_attributes(const nlohmann::json &attributes) {
MediaPlayerSources sources;
if (attributes.contains("source_list") && attributes["source_list"].is_array()) {
for (const nlohmann::json &source : attributes["source_list"]) {
if (source.is_string()) {
sources.sources.push_back(source);
}
}
}
if (attributes.contains("source") && attributes["source"].is_string()) {
sources.current_source = attributes["source"];
}
uint32_t supported_features = 0;
if (attributes.contains("supported_features") && attributes["supported_features"].is_number_unsigned()) {
supported_features = attributes["supported_features"];
}
sources.can_select = (supported_features & HomeAssistantSourcePlayer::feature_select_source) && !sources.sources.empty();
return sources;
}

std::optional<uint8_t> HomeAssistantSourcePlayer::volume_from_json(const nlohmann::json &volume_level) {
if (!volume_level.is_number()) {
return std::nullopt;
}
return std::lround(std::clamp(volume_level.get<double>(), 0.0, 1.0) * 100);
}

void HomeAssistantSourcePlayer::call_service(const std::string &entity_id, const std::string &service, nlohmann::json service_data) {
nlohmann::json command = {
{"type", "call_service"},
{"domain", "media_player"},
{"target", {{"entity_id", entity_id}}},
{"service", service}};
if (!service_data.empty()) {
command["service_data"] = service_data;
}
HomeAssistantManager::send_json(command);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#ifndef MQTT_MANAGER_HOME_ASSISTANT_SOURCE_PLAYER_HPP
#define MQTT_MANAGER_HOME_ASSISTANT_SOURCE_PLAYER_HPP

#include "media_player/media_player.hpp"
#include <cstdint>
#include <functional>
#include <mutex>
#include <nlohmann/json.hpp>
#include <optional>
#include <string>

/**
* Follows the source player feeding a media player in Home Assistant. The media player reports the
* entity ID of its current source player in a state attribute, the source player is itself a media
* player in Home Assistant.
*/
class HomeAssistantSourcePlayer {
public:
/**
* @param source_entity_attribute: Media player state attribute holding the entity ID of the source player.
* @param on_change: Called when the source player changes state without the media player itself changing.
*/
HomeAssistantSourcePlayer(std::string source_entity_attribute, std::function<void()> on_change);
~HomeAssistantSourcePlayer();

/**
* Process new state attributes of the media player. Follows the source player when it changes.
*/
void update_from_player_attributes(const nlohmann::json &attributes);

/**
* Get the entity ID of the current source player, or an empty string if there is none.
*/
std::string get_entity_id();

/**
* Get the volume of the source player in 0-100%, if known.
*/
std::optional<uint8_t> get_volume();

/**
* Get the sources of the source player, or nothing if there is no source player.
*/
std::optional<MediaPlayerSources> get_sources();

/**
* Set the volume of the source player.
* @param volume: Volume in 0-100%
*/
void set_volume(uint8_t volume);

/**
* Select a source of the source player.
*/
void select_source(const std::string &source);

void home_assistant_event_callback(nlohmann::json event_data);

/**
* Read the sources of a media player from its Home Assistant state attributes.
*/
static MediaPlayerSources sources_from_attributes(const nlohmann::json &attributes);

/**
* Convert a Home Assistant volume level (0.0-1.0) to 0-100%.
* Home Assistant always reports volume_level as 0.0-1.0, whatever the device uses. 0-100 is only used towards the NSPanel.
*/
static std::optional<uint8_t> volume_from_json(const nlohmann::json &volume_level);

/**
* Call a "media_player" domain service in Home Assistant on the given entity.
*/
static void call_service(const std::string &entity_id, const std::string &service, nlohmann::json service_data = nlohmann::json::object());

// Media player feature flag from Home Assistant (MediaPlayerEntityFeature).
static constexpr uint32_t feature_select_source = 2048;

private:
std::string _source_entity_attribute;
std::function<void()> _on_change;

std::mutex _mutex;
std::string _entity_id;
std::optional<uint8_t> _volume;
MediaPlayerSources _sources;
};

#endif // !MQTT_MANAGER_HOME_ASSISTANT_SOURCE_PLAYER_HPP
Loading
Loading