diff --git a/docker/MQTTManager/include/nspanel/nspanel.cpp b/docker/MQTTManager/include/nspanel/nspanel.cpp index 26d76535..830ef709 100644 --- a/docker/MQTTManager/include/nspanel/nspanel.cpp +++ b/docker/MQTTManager/include/nspanel/nspanel.cpp @@ -100,15 +100,18 @@ void NSPanel::update_config(nlohmann::json &init_data) { this->_is_register_accepted = true; } - bool rebuilt_mqtt = false; // Wether or not to rebuild mqtt topics and subscribe to the new topics. + bool rebuilt_mqtt = false; // Wether or not to rebuild mqtt topics and subscribe to the new topics. + bool rebuilt_ha_mqtt = false; // Wether or not to rebuild mqtt topics for HA entity discovery. if (init_data.contains("name")) { if (this->_name.compare(init_data["name"]) != 0) { rebuilt_mqtt = true; + rebuilt_ha_mqtt = true; } this->_name = init_data["name"]; } else if (init_data.contains("friendly_name")) { if (this->_name.compare(init_data["friendly_name"]) != 0) { rebuilt_mqtt = true; + rebuilt_ha_mqtt = true; } this->_name = init_data["friendly_name"]; } @@ -154,16 +157,18 @@ void NSPanel::update_config(nlohmann::json &init_data) { if (init_data.contains("relay1_is_light")) { this->_relay1_is_mqtt_light = init_data["relay1_is_light"]; - rebuilt_mqtt = true; + rebuilt_ha_mqtt = true; } else { this->_relay1_is_mqtt_light = false; + rebuilt_ha_mqtt = true; } if (init_data.contains("relay2_is_light")) { this->_relay2_is_mqtt_light = init_data["relay2_is_light"]; - rebuilt_mqtt = true; + rebuilt_ha_mqtt = true; } else { this->_relay2_is_mqtt_light = false; + rebuilt_ha_mqtt = true; } if (rebuilt_mqtt) { @@ -195,6 +200,11 @@ void NSPanel::update_config(nlohmann::json &init_data) { this->register_to_home_assistant(); } + if (!rebuilt_mqtt && rebuilt_ha_mqtt) { + this->reset_ha_mqtt_topics(); + this->register_to_home_assistant(); + } + if (this->_has_registered_to_manager) { // If this NSPanel is registered to manager, listen to state topics. MQTT_Manager::subscribe(this->_mqtt_relay1_state_topic, boost::bind(&NSPanel::mqtt_callback, this, _1, _2)); @@ -223,6 +233,11 @@ void NSPanel::reset_mqtt_topics() { // This nspanel was removed. Clear any retain on any MQTT topic. MQTT_Manager::clear_retain(this->_mqtt_status_topic); MQTT_Manager::clear_retain(this->_mqtt_command_topic); + + this->reset_ha_mqtt_topics(); +} + +void NSPanel::reset_ha_mqtt_topics() { MQTT_Manager::clear_retain(this->_mqtt_light_relay1_topic); MQTT_Manager::clear_retain(this->_mqtt_light_relay2_topic); MQTT_Manager::clear_retain(this->_mqtt_switch_relay1_topic); @@ -755,7 +770,7 @@ void NSPanel::register_to_home_assistant() { // Register screensaver brightness nlohmann::json screensaver_brightness_data = nlohmann::json(base_json); - screensaver_brightness_data["name"] = "Screen brightness"; + screensaver_brightness_data["name"] = "Screensaver brightness"; screensaver_brightness_data["command_topic"] = fmt::format("nspanel/{}/brightness_screensaver", this->_name); screensaver_brightness_data["min"] = "0"; screensaver_brightness_data["max"] = "100"; diff --git a/docker/MQTTManager/include/nspanel/nspanel.hpp b/docker/MQTTManager/include/nspanel/nspanel.hpp index e947f0a0..f7fb3aa9 100644 --- a/docker/MQTTManager/include/nspanel/nspanel.hpp +++ b/docker/MQTTManager/include/nspanel/nspanel.hpp @@ -53,6 +53,10 @@ class NSPanel { void update_config(nlohmann::json &init_data); ~NSPanel(); void reset_mqtt_topics(); + /** + * Reset MQTT topics used to register entities to HA. + */ + void reset_ha_mqtt_topics(); /** * Get the ID of this NSPanel. diff --git a/docker/MQTTManager/include/weather/weather.cpp b/docker/MQTTManager/include/weather/weather.cpp index 844822fc..8e55750c 100644 --- a/docker/MQTTManager/include/weather/weather.cpp +++ b/docker/MQTTManager/include/weather/weather.cpp @@ -1,28 +1,42 @@ #include "weather.hpp" #include "home_assistant_manager/home_assistant_manager.hpp" +#include "mqtt_manager/mqtt_manager.hpp" #include "mqtt_manager_config/mqtt_manager_config.hpp" #include "openhab_manager/openhab_manager.hpp" #include #include #include #include +#include #include #include -#include #include -#include +#include #include #include #include #include - -void MQTTManagerWeather::init() { - MQTTManagerWeather::_update_forecast_thread = std::thread(MQTTManagerWeather::_update_forecast); - - MQTTManagerWeather::_update_forecast_thread.join(); -} +#include void MQTTManagerWeather::update_config() { + if (MqttManagerConfig::weather_controller.compare("home_assistant") == 0) { + SPDLOG_INFO("Initializing weather controller for Home Assistant."); + HomeAssistantManager::attach_event_observer(MqttManagerConfig::home_assistant_weather_entity, boost::bind(&MQTTManagerWeather::home_assistant_event_callback, this, _1)); + HomeAssistantManager::attach_event_observer(MqttManagerConfig::home_assistant_sun_entity, boost::bind(&MQTTManagerWeather::home_assistant_event_callback, this, _1)); + OpenhabManager::detach_event_observer(MqttManagerConfig::openhab_current_weather_item, boost::bind(&MQTTManagerWeather::openhab_current_weather_callback, this, _1)); + OpenhabManager::detach_event_observer(MqttManagerConfig::openhab_forecast_weather_item, boost::bind(&MQTTManagerWeather::openhab_forecast_weather_callback, this, _1)); + } else if (MqttManagerConfig::weather_controller.compare("openhab") == 0) { + SPDLOG_INFO("Initializing weather controller for OpenHAB."); + SPDLOG_DEBUG("Current weather item: {}", MqttManagerConfig::openhab_current_weather_item); + SPDLOG_DEBUG("Forecast weather item: {}", MqttManagerConfig::openhab_forecast_weather_item); + HomeAssistantManager::detach_event_observer(MqttManagerConfig::home_assistant_weather_entity, boost::bind(&MQTTManagerWeather::home_assistant_event_callback, this, _1)); + HomeAssistantManager::detach_event_observer(MqttManagerConfig::home_assistant_sun_entity, boost::bind(&MQTTManagerWeather::home_assistant_event_callback, this, _1)); + OpenhabManager::attach_event_observer(MqttManagerConfig::openhab_current_weather_item, boost::bind(&MQTTManagerWeather::openhab_current_weather_callback, this, _1)); + OpenhabManager::attach_event_observer(MqttManagerConfig::openhab_forecast_weather_item, boost::bind(&MQTTManagerWeather::openhab_forecast_weather_callback, this, _1)); + } else { + SPDLOG_ERROR("Unsupported weather controller '{}'.", MqttManagerConfig::weather_controller); + } + if (MqttManagerConfig::outside_temp_sensor_provider.compare("home_assistant") == 0) { SPDLOG_INFO("Will load outside temperature from Home Assistant sensor {}", MqttManagerConfig::outside_temp_sensor_entity_id); HomeAssistantManager::attach_event_observer(MqttManagerConfig::outside_temp_sensor_entity_id, boost::bind(&MQTTManagerWeather::home_assistant_event_callback, this, _1)); @@ -37,53 +51,122 @@ void MQTTManagerWeather::update_config() { } void MQTTManagerWeather::home_assistant_event_callback(nlohmann::json event_data) { - if (MqttManagerConfig::outside_temp_sensor_provider.compare("home_assistant") == 0 && std::string(event_data["event"]["data"]["entity_id"]).compare(MqttManagerConfig::outside_temp_sensor_entity_id) == 0) { + if (std::string(event_data["event"]["data"]["entity_id"]).compare(MqttManagerConfig::home_assistant_weather_entity) == 0) { nlohmann::json new_state = event_data["event"]["data"]["new_state"]; - this->_current_temperature = atof(std::string(new_state["state"]).c_str()); - this->send_state_update(); - } -} - -static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) { - ((std::string *)userp)->append((char *)contents, size * nmemb); - return size * nmemb; -} -void MQTTManagerWeather::_update_forecast() { - SPDLOG_INFO("Starting 'Update Forecast' thread."); - while (true) { - SPDLOG_DEBUG("Fetching new weather forecast."); - CURL *curl; - CURLcode res; - curl = curl_easy_init(); - if (curl) { - std::string response_data; - curl_easy_setopt(curl, CURLOPT_URL, "https://api.openweathermap.org/data/2.5/forecast?lat=44.34&lon=10.99&appid=4ff881e8b20a81abe808164d88816a6a"); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_data); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &WriteCallback); - - /* Perform the request, res will get the return code */ - SPDLOG_DEBUG("Requesting weather."); - res = curl_easy_perform(curl); - long http_code; - SPDLOG_DEBUG("Gathering response code."); - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - /* Check for errors */ - if (res == CURLE_OK && http_code == 200) { - SPDLOG_DEBUG("Got response: {}", response_data); - } else { - SPDLOG_ERROR("Failed to fetch weather forecast. CURL error: {}", curl_easy_strerror(res)); + this->_forecast_weather_info.clear(); + for (nlohmann::json forecast : new_state["attributes"]["forecast"]) { + weather_info info; + SPDLOG_DEBUG("Loading weather forecast for {}", std::string(forecast["datetime"])); + info.condition = forecast["condition"]; + info.wind_speed = forecast["wind_speed"]; + info.precipitation = forecast["precipitation"]; + info.precipitation_probability = forecast["precipitation_probability"]; + info.temperature_low = forecast["templow"]; + info.temperature_high = forecast["temperature"]; + + // Get day of week + std::string datetime = forecast["datetime"]; + std::vector datetime_parts; + size_t pos = datetime.find("T"); + std::string date = datetime.substr(0, pos); + std::string year = date.substr(0, date.find("-")); + date.erase(0, date.find("-") + 1); + std::string month = date.substr(0, date.find("-")); + date.erase(0, date.find("-") + 1); + std::string day_of_month = date; + + std::tm tm = {01, 00, 00, atoi(day_of_month.c_str()), atoi(month.c_str()) - 1, atoi(year.c_str()) - 1900}; + std::time_t utc_time = std::mktime(&tm); + const std::tm *localtime = std::localtime(&utc_time); + info.time = *localtime; + + switch (localtime->tm_wday) { + case 0: + info.day = "Sun"; + break; + case 1: + info.day = "Mon"; + break; + case 2: + info.day = "Tue"; + break; + case 3: + info.day = "Wed"; + break; + case 4: + info.day = "Thu"; + break; + case 5: + info.day = "Fri"; + break; + case 6: + info.day = "Sat"; + break; + default: + info.day = std::to_string(localtime->tm_wday); + break; } - /* always cleanup */ - SPDLOG_DEBUG("Cleanup."); - curl_easy_cleanup(curl); + this->_forecast_weather_info.push_back(info); + } + + SPDLOG_DEBUG("Loaded forecast for {} days.", this->_forecast_weather_info.size()); + + if (this->_forecast_weather_info.size() > 0) { + if (MqttManagerConfig::outside_temp_sensor_provider.length() == 0 && MqttManagerConfig::outside_temp_sensor_entity_id.length() == 0) { + this->_current_temperature = new_state["attributes"]["temperature"]; + } + this->_current_condition = this->_forecast_weather_info[0].condition; + this->_current_precipitation_probability = this->_forecast_weather_info[0].precipitation_probability; + this->_current_wind_speed = new_state["attributes"]["wind_speed"]; + this->_windspeed_unit = new_state["attributes"]["wind_speed_unit"]; + this->_precipitation_unit = new_state["attributes"]["precipitation_unit"]; + this->_current_min_temperature = this->_forecast_weather_info[0].temperature_low; + this->_current_max_temperature = this->_forecast_weather_info[0].temperature_high; + std::time_t time = std::time({}); + this->_current_weather_time = *std::localtime(&time); + this->send_state_update(); } else { - SPDLOG_ERROR("Failed to create curl object."); + SPDLOG_ERROR("Failed to process forecast information from Home Assistant."); + return; } - SPDLOG_DEBUG("Weather fetched, will try again in 1 minute."); - std::this_thread::sleep_for(std::chrono::milliseconds(60000)); // Fetch new forecast every minute TODO: Change this to something more reasonable + + return; + } else if (std::string(event_data["event"]["data"]["entity_id"]).compare(MqttManagerConfig::home_assistant_sun_entity) == 0) { + std::string next_rising = event_data["event"]["data"]["new_state"]["attributes"]["next_rising"]; + std::string next_setting = event_data["event"]["data"]["new_state"]["attributes"]["next_setting"]; + + // Get time parts from sunrise + std::string rising_date = next_rising.substr(0, next_rising.find("T")); + next_rising.erase(0, next_rising.find("T") + 1); + std::string rising_time = next_rising.substr(0, next_rising.find("+")); + next_rising.erase(0, next_rising.find("+") + 1); + + std::string rising_hour = rising_time.substr(0, rising_time.find(":")); + rising_time.erase(0, rising_time.find(":") + 1); + std::string rising_minute = rising_time.substr(0, rising_time.find(":")); + rising_time.erase(0, rising_time.find(":") + 1); + + // Get time parts from sunrise + std::string setting_date = next_setting.substr(0, next_setting.find("T")); + next_setting.erase(0, next_setting.find("T") + 1); + std::string setting_time = next_setting.substr(0, next_setting.find("+")); + next_setting.erase(0, next_setting.find("+") + 1); + + std::string setting_hour = setting_time.substr(0, setting_time.find(":")); + setting_time.erase(0, setting_time.find(":") + 1); + std::string setting_minute = setting_time.substr(0, setting_time.find(":")); + setting_time.erase(0, setting_time.find(":") + 1); + + this->_next_sunrise_hour = atoi(rising_hour.c_str()); + this->_next_sunrise = fmt::format("{}:{}", rising_hour, rising_minute); + this->_next_sunset_hour = atoi(setting_hour.c_str()); + this->_next_sunset = fmt::format("{}:{}", setting_hour, setting_minute); + } else if (MqttManagerConfig::outside_temp_sensor_provider.compare("home_assistant") == 0 && std::string(event_data["event"]["data"]["entity_id"]).compare(MqttManagerConfig::outside_temp_sensor_entity_id) == 0) { + nlohmann::json new_state = event_data["event"]["data"]["new_state"]; + this->_current_temperature = atof(std::string(new_state["state"]).c_str()); + this->send_state_update(); } } diff --git a/docker/MQTTManager/include/weather/weather.hpp b/docker/MQTTManager/include/weather/weather.hpp index be0d3ffa..ea4f25f9 100644 --- a/docker/MQTTManager/include/weather/weather.hpp +++ b/docker/MQTTManager/include/weather/weather.hpp @@ -7,7 +7,6 @@ class MQTTManagerWeather { public: - static void init(); void update_config(); void home_assistant_event_callback(nlohmann::json event_data); void openhab_current_weather_callback(nlohmann::json event_data); @@ -17,16 +16,10 @@ class MQTTManagerWeather { private: std::string _get_icon_from_mapping(std::string &condition, uint8_t hour); - static void _update_forecast(); - static void _process_forecast_data(); - static void _update_current_weather(); - - static inline std::thread _update_current_weather_thread; - static inline std::thread _update_forecast_thread; struct weather_info { std::string condition; - int condition_id; + int condition_id; // Only used for OpenHAB/OpenWeatherMap std::string day; std::tm time; std::tm sunrise; diff --git a/docker/MQTTManager/src/main.cpp b/docker/MQTTManager/src/main.cpp index a06fd8ff..22e4a751 100644 --- a/docker/MQTTManager/src/main.cpp +++ b/docker/MQTTManager/src/main.cpp @@ -132,8 +132,6 @@ int main(void) { SPDLOG_WARN("OpenHAB address and/or token missing. Won't start OpenHAB component."); } - MQTTManagerWeather::init(); - // Wait for threads to exit mqtt_manager_thread.join(); if (home_assistant_manager_thread.joinable()) { diff --git a/docker/web/nspm_mqttmanager b/docker/web/nspm_mqttmanager index 10ec843a..485f3ef3 100755 Binary files a/docker/web/nspm_mqttmanager and b/docker/web/nspm_mqttmanager differ