From 8e016ee4dc01522348e862669b30ded46d7cfd5e Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 18 Jun 2023 23:26:50 +0200 Subject: [PATCH 001/105] Set "homeassistant" as discovery prefix default. --- lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp b/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp index fe50b489..ec84222e 100644 --- a/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp +++ b/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp @@ -72,7 +72,7 @@ const char* HomeAssistantMqtt::KEY_HA_DISCOVERY_PREFIX = "ha_dp"; const char* HomeAssistantMqtt::NAME_HA_DISCOVERY_PREFIX = "Home Assistant Discovery Prefix"; /* Initialize Home Assistant discovery prefix default value. */ -const char* HomeAssistantMqtt::DEFAULT_HA_DISCOVERY_PREFIX = ""; +const char* HomeAssistantMqtt::DEFAULT_HA_DISCOVERY_PREFIX = "homeassistant"; /****************************************************************************** * Public Methods From 8ed795aba0d6c933b8d9fa9f9eaa9b07cf436eda Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 18 Jun 2023 23:27:25 +0200 Subject: [PATCH 002/105] MQTT service for Ulanzi TC001 was missing. --- config/configNormal.ini | 2 +- config/configSmall.ini | 2 +- config/configSmallNoI2s.ini | 2 +- config/configSmallUlanzi.ini | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/configNormal.ini b/config/configNormal.ini index e96bf216..125534c9 100644 --- a/config/configNormal.ini +++ b/config/configNormal.ini @@ -12,7 +12,7 @@ lib_deps = MqttService @ ~0.1.0 # ********** Topic handlers ********** RestApiTopicHandler @ ~0.1.0 # Mandatory, can not be removed. Used by webinterface. - MqttApiTopicHandler @ ~0.1.0 + MqttApiTopicHandler @ ~0.1.0 # Requires MqttService # ********** Plugins ********** ;BatteryPlugin @ ~0.1.0 BTCQuotePlugin @ ~0.1.0 diff --git a/config/configSmall.ini b/config/configSmall.ini index b844f118..725be8d4 100644 --- a/config/configSmall.ini +++ b/config/configSmall.ini @@ -12,7 +12,7 @@ lib_deps = ;MqttService @ ~0.1.0 # ********** Topic handlers ********** RestApiTopicHandler @ ~0.1.0 # Mandatory, can not be removed. Used by webinterface. - ;MqttApiTopicHandler @ ~0.1.0 + ;MqttApiTopicHandler @ ~0.1.0 # Requires MqttService # ********** Plugins ********** ;BatteryPlugin @ ~0.1.0 BTCQuotePlugin @ ~0.1.0 diff --git a/config/configSmallNoI2s.ini b/config/configSmallNoI2s.ini index d2ad23ff..a4a6a425 100644 --- a/config/configSmallNoI2s.ini +++ b/config/configSmallNoI2s.ini @@ -12,7 +12,7 @@ lib_deps = ;MqttService @ ~0.1.0 # ********** Topic handlers ********** RestApiTopicHandler @ ~0.1.0 # Mandatory, can not be removed. Used by webinterface. - ;MqttApiTopicHandler @ ~0.1.0 + ;MqttApiTopicHandler @ ~0.1.0 # Requires MqttService # ********** Plugins ********** ;BatteryPlugin @ ~0.1.0 BTCQuotePlugin @ ~0.1.0 diff --git a/config/configSmallUlanzi.ini b/config/configSmallUlanzi.ini index 0edc695e..59f4f678 100644 --- a/config/configSmallUlanzi.ini +++ b/config/configSmallUlanzi.ini @@ -9,10 +9,10 @@ lib_deps = TopicHandlerService @ ~0.1.0 # Mandatory, can not be removed. SettingsService @ ~0.1.0 # Mandatory, can not be removed. ;AudioService @ ~0.1.0 # No I2S interface available - ;MqttService @ ~0.1.0 + MqttService @ ~0.1.0 # ********** Topic handlers ********** RestApiTopicHandler @ ~0.1.0 # Mandatory, can not be removed. Used by webinterface. - MqttApiTopicHandler @ ~0.1.0 + MqttApiTopicHandler @ ~0.1.0 # Requires MqttService # ********** Plugins ********** BatteryPlugin @ ~0.1.0 ;BTCQuotePlugin @ ~0.1.0 From 144b6536c40e7e6cc0c0bb11b99eb03048c25df3 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Thu, 22 Jun 2023 21:19:55 +0200 Subject: [PATCH 003/105] Home Assistant discovery by default disabled. It can be enabled in the settings. --- .../src/HomeAssistantMqtt.cpp | 45 ++++++++++++++----- .../src/HomeAssistantMqtt.h | 22 +++++++-- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp b/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp index ec84222e..fee3b18e 100644 --- a/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp +++ b/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp @@ -74,6 +74,15 @@ const char* HomeAssistantMqtt::NAME_HA_DISCOVERY_PREFIX = "Home Assistant Di /* Initialize Home Assistant discovery prefix default value. */ const char* HomeAssistantMqtt::DEFAULT_HA_DISCOVERY_PREFIX = "homeassistant"; +/* Initialize Home Assistant discovery enable flag key */ +const char* HomeAssistantMqtt::KEY_HA_DISCOVERY_ENABLE = "ha_ena"; + +/* Initialize Home Assistant discovery enable flag name */ +const char* HomeAssistantMqtt::NAME_HA_DISCOVERY_ENABLE = "Enable Home Assistant Discovery"; + +/* Initialize Home Assistant discovery enable flag default value */ +const bool HomeAssistantMqtt::DEFAULT_HA_DISCOVERY_ENABLE = false; + /****************************************************************************** * Public Methods *****************************************************************************/ @@ -86,13 +95,18 @@ void HomeAssistantMqtt::start() { LOG_ERROR("Couldn't register HA discovery prefix setting."); } + else if (false == settings.registerSetting(&m_haDiscoveryEnabledSetting)) + { + LOG_ERROR("Couldn't register HA discovery enable setting."); + } else if (false == settings.open(true)) { LOG_ERROR("Couldn't open settings."); } else { - m_haDiscoveryPrefix = m_haDiscoveryPrefixSetting.getValue(); + m_haDiscoveryPrefix = m_haDiscoveryPrefixSetting.getValue(); + m_haDiscoveryEnabled = m_haDiscoveryEnabledSetting.getValue(); settings.close(); } @@ -103,21 +117,26 @@ void HomeAssistantMqtt::stop() SettingsService& settings = SettingsService::getInstance(); settings.unregisterSetting(&m_haDiscoveryPrefixSetting); + settings.unregisterSetting(&m_haDiscoveryEnabledSetting); clearMqttDiscoveryInfoList(); } void HomeAssistantMqtt::process(bool isConnected) { - if (true == isConnected) + /* The Home Assistant discovery must be enabled. */ + if (true == m_haDiscoveryEnabled) { - if (false == m_isConnected) + if (true == isConnected) { - publishAllAutoDiscoveryInfo(true); - } - else - { - publishAllAutoDiscoveryInfo(false); + if (false == m_isConnected) + { + publishAllAutoDiscoveryInfo(true); + } + else + { + publishAllAutoDiscoveryInfo(false); + } } } @@ -126,10 +145,11 @@ void HomeAssistantMqtt::process(bool isConnected) void HomeAssistantMqtt::registerMqttDiscovery(const String& nodeId, const String& objectId, const String& stateTopic, const String& cmdTopic, const String& availabilityTopic, JsonObjectConst& extra) { - /* The Home Assistant discovery prefix must be available, otherwise this + /* The Home Assistant discovery must be enabled and the prefix must be available, otherwise this * feature is disabled. */ - if (false == m_haDiscoveryPrefix.isEmpty()) + if ((true == m_haDiscoveryEnabled) && + (false == m_haDiscoveryPrefix.isEmpty())) { JsonVariantConst jsonHomeAssistant = extra["ha"]; @@ -181,10 +201,11 @@ void HomeAssistantMqtt::registerMqttDiscovery(const String& nodeId, const String void HomeAssistantMqtt::unregisterMqttDiscovery(const String& nodeId, const String& objectId, const String& stateTopic, const String& cmdTopic) { - /* The Home Assistant discovery prefix must be available, otherwise this + /* The Home Assistant discovery must be enabled and the prefix must be available, otherwise this * feature is disabled. */ - if (false == m_haDiscoveryPrefix.isEmpty()) + if ((true == m_haDiscoveryEnabled) && + (false == m_haDiscoveryPrefix.isEmpty())) { ListOfMqttDiscoveryInfo::iterator listOfMqttDiscoveryInfoIt = m_mqttDiscoveryInfoList.begin(); diff --git a/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.h b/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.h index 693f178e..766f9546 100644 --- a/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.h +++ b/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.h @@ -45,6 +45,7 @@ *****************************************************************************/ #include #include +#include #include #include @@ -69,7 +70,9 @@ class HomeAssistantMqtt */ HomeAssistantMqtt() : m_haDiscoveryPrefixSetting(KEY_HA_DISCOVERY_PREFIX, NAME_HA_DISCOVERY_PREFIX, DEFAULT_HA_DISCOVERY_PREFIX, MIN_VALUE_HA_DISCOVERY_PREFIX, MAX_VALUE_HA_DISCOVERY_PREFIX), + m_haDiscoveryEnabledSetting(KEY_HA_DISCOVERY_ENABLE, NAME_HA_DISCOVERY_ENABLE, DEFAULT_HA_DISCOVERY_ENABLE), m_haDiscoveryPrefix(), + m_haDiscoveryEnabled(false), m_isConnected(false) { } @@ -140,6 +143,15 @@ class HomeAssistantMqtt /** Home Assistant discovery prefix max. length */ static const size_t MAX_VALUE_HA_DISCOVERY_PREFIX = 64U; + /** Home Assistant discovery enable flag key */ + static const char* KEY_HA_DISCOVERY_ENABLE; + + /** Home Assistant discovery enable flag name */ + static const char* NAME_HA_DISCOVERY_ENABLE; + + /** Home Assistant discovery enable flag default value */ + static const bool DEFAULT_HA_DISCOVERY_ENABLE; + /** Information necessary for Home Assistant MQTT discovery. */ struct MqttDiscoveryInfo { @@ -173,10 +185,12 @@ class HomeAssistantMqtt /** List of Home Assistant MQTT discovery information. */ typedef std::vector ListOfMqttDiscoveryInfo; - KeyValueString m_haDiscoveryPrefixSetting; /**< Setting for the Home Assistant MQTT discovery prefix. */ - String m_haDiscoveryPrefix; /**< Home Assistant MQTT discovery prefix. */ - ListOfMqttDiscoveryInfo m_mqttDiscoveryInfoList; /**< List of Home Assistant MQTT discovery informations. */ - bool m_isConnected; /**< Is MQTT broker connection established? */ + KeyValueString m_haDiscoveryPrefixSetting; /**< Setting for the Home Assistant MQTT discovery prefix. */ + KeyValueBool m_haDiscoveryEnabledSetting; /**< Setting for the Home Assistant MQTT discovery enable flag. */ + String m_haDiscoveryPrefix; /**< Home Assistant MQTT discovery prefix. */ + bool m_haDiscoveryEnabled; /**< Is the Home Assistant MQTT discovery enabled or not. */ + ListOfMqttDiscoveryInfo m_mqttDiscoveryInfoList; /**< List of Home Assistant MQTT discovery informations. */ + bool m_isConnected; /**< Is MQTT broker connection established? */ HomeAssistantMqtt(const HomeAssistantMqtt& ext); HomeAssistantMqtt& operator=(const HomeAssistantMqtt& ext); From 8c1bf56e8b2b461a1ff8fbf8e350e58238ac65dd Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 25 Jun 2023 01:04:11 +0200 Subject: [PATCH 004/105] With the sequence \xHH a character code in hex can be specified. This is useful e.g. to get the degree character 0x8E by using \x8E --- lib/YAWidgets/src/TextWidget.cpp | 34 +++++++++++++++++++++++++++++++- lib/YAWidgets/src/TextWidget.h | 12 +++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/lib/YAWidgets/src/TextWidget.cpp b/lib/YAWidgets/src/TextWidget.cpp index 7cba00f2..9811dcee 100644 --- a/lib/YAWidgets/src/TextWidget.cpp +++ b/lib/YAWidgets/src/TextWidget.cpp @@ -68,7 +68,8 @@ const YAFont& TextWidget::DEFAULT_FONT = Fonts::getFontByTy TextWidget::KeywordHandler TextWidget::m_keywordHandlers[] = { &TextWidget::handleColor, - &TextWidget::handleAlignment + &TextWidget::handleAlignment, + &TextWidget::handleCharCode }; /* Set default scroll pause in ms. */ @@ -539,6 +540,37 @@ bool TextWidget::handleAlignment(YAGfx* gfx, YAGfxText* gfxText, bool noAction, return status; } +bool TextWidget::handleCharCode(YAGfx* gfx, YAGfxText* gfxText, bool noAction, const String& formatStr, bool isScrolling, uint8_t& overstep) const +{ + bool status = false; + + UTIL_NOT_USED(isScrolling); + + if (('x' == formatStr[0]) || + ('X' == formatStr[0])) + { + const uint8_t CHAR_CODE_LEN = 2U; + String charCodeStr = String("0x") + formatStr.substring(1, 1 + CHAR_CODE_LEN); + uint8_t charCode = 0U; + bool convStatus = Util::strToUInt8(charCodeStr, charCode); + + if (true == convStatus) + { + if ((false == noAction) && + (nullptr != gfx) && + (nullptr != gfxText)) + { + gfxText->drawChar(*gfx, static_cast(charCode)); + } + + overstep = 1U + CHAR_CODE_LEN; + status = true; + } + } + + return status; +} + /****************************************************************************** * External Functions *****************************************************************************/ diff --git a/lib/YAWidgets/src/TextWidget.h b/lib/YAWidgets/src/TextWidget.h index af0f67c4..9bd55bab 100644 --- a/lib/YAWidgets/src/TextWidget.h +++ b/lib/YAWidgets/src/TextWidget.h @@ -452,6 +452,18 @@ class TextWidget : public Widget * @return If keyword is handled successful, it returns true otherwise false. */ bool handleAlignment(YAGfx* gfx, YAGfxText* gfxText, bool noAction, const String& formatStr, bool isScrolling, uint8_t& overstep) const; + + /** + * Handles the keyword for character code. + * + * @param[in] gfx Graphics interface, only necessary if actions shall take place. + * @param[in] noAction The handler shall take no action. This is only used to get rid of the keywords in the text. + * @param[in] formatStr String which may contain keywords. + * @param[out] overstep Number of characters, which must be overstepped before the next normal character comes. + * + * @return If keyword is handled successful, it returns true otherwise false. + */ + bool handleCharCode(YAGfx* gfx, YAGfxText* gfxText, bool noAction, const String& formatStr, bool isScrolling, uint8_t& overstep) const; }; /****************************************************************************** From b02a84e92ddb98c22f911360ab2799e2ab29a122 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 25 Jun 2023 01:12:13 +0200 Subject: [PATCH 005/105] Debug log removed and max. payload size for sending MQTT data increased to 2k. --- lib/MqttService/src/MqttService.cpp | 2 -- lib/MqttService/src/MqttService.h | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/MqttService/src/MqttService.cpp b/lib/MqttService/src/MqttService.cpp index 27399757..ad41b48f 100644 --- a/lib/MqttService/src/MqttService.cpp +++ b/lib/MqttService/src/MqttService.cpp @@ -367,8 +367,6 @@ void MqttService::rxCallback(char* topic, uint8_t* payload, uint32_t length) { SubscriberList::const_iterator it; - LOG_DEBUG("MQTT Rx: %s", topic); - for(it = m_subscriberList.begin(); it != m_subscriberList.end(); ++it) { if (nullptr != (*it)) diff --git a/lib/MqttService/src/MqttService.h b/lib/MqttService/src/MqttService.h index 40b76e0c..eff53d25 100644 --- a/lib/MqttService/src/MqttService.h +++ b/lib/MqttService/src/MqttService.h @@ -215,7 +215,7 @@ class MqttService : public IService * Max. MQTT client buffer size in byte. * Received MQTT messages greather than this will be skipped. */ - static const size_t MAX_BUFFER_SIZE = 1024U; + static const size_t MAX_BUFFER_SIZE = 2048U; KeyValueString m_mqttBrokerUrlSetting; /**< URL of the MQTT broker setting */ String m_url; /**< URL of the MQTT broker */ From 45e5ce4707f0890d80cf3b35e0c69bba9673c1a8 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 25 Jun 2023 01:12:48 +0200 Subject: [PATCH 006/105] Push URL moved in a separate function. --- src/StateMachine/ConnectedState.cpp | 79 +++++++++++++++-------------- src/StateMachine/ConnectedState.h | 6 +++ 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/src/StateMachine/ConnectedState.cpp b/src/StateMachine/ConnectedState.cpp index bea2b777..ffc8b4fb 100644 --- a/src/StateMachine/ConnectedState.cpp +++ b/src/StateMachine/ConnectedState.cpp @@ -138,43 +138,7 @@ void ConnectedState::entry(StateMachine& sm) SysMsg::getInstance().show(infoStr, DURATION_NON_SCROLLING, SCROLLING_REPEAT_NUM); } - /* If a push URL is set, notify about the online status. */ - if (false == notifyURL.isEmpty()) - { - String url = notifyURL; - const char* GET_CMD = "get "; - const char* POST_CMD = "post "; - bool isGet = true; - - /* URL prefix might indicate the kind of request. */ - url.toLowerCase(); - if (0U != url.startsWith(GET_CMD)) - { - url = url.substring(strlen(GET_CMD)); - isGet = true; - } - else if (0U != url.startsWith(POST_CMD)) - { - url = url.substring(strlen(POST_CMD)); - isGet = false; - } - else - { - ; - } - - if (true == m_client.begin(url)) - { - if (false == m_client.GET()) - { - LOG_WARNING("GET %s failed.", url.c_str()); - } - else - { - LOG_INFO("Notification triggered."); - } - } - } + pushUrl(notifyURL); } } @@ -237,6 +201,47 @@ void ConnectedState::exit(StateMachine& sm) * Private Methods *****************************************************************************/ +void ConnectedState::pushUrl(const String& pushUrl) +{ + /* If a push URL is set, notify about the online status. */ + if (false == pushUrl.isEmpty()) + { + String url = pushUrl; + const char* GET_CMD = "get "; + const char* POST_CMD = "post "; + bool isGet = true; + + /* URL prefix might indicate the kind of request. */ + url.toLowerCase(); + if (0U != url.startsWith(GET_CMD)) + { + url = url.substring(strlen(GET_CMD)); + isGet = true; + } + else if (0U != url.startsWith(POST_CMD)) + { + url = url.substring(strlen(POST_CMD)); + isGet = false; + } + else + { + ; + } + + if (true == m_client.begin(url)) + { + if (false == m_client.GET()) + { + LOG_WARNING("GET %s failed.", url.c_str()); + } + else + { + LOG_INFO("Notification triggered."); + } + } + } +} + /****************************************************************************** * External Functions *****************************************************************************/ diff --git a/src/StateMachine/ConnectedState.h b/src/StateMachine/ConnectedState.h index 4dc7439a..011404fd 100644 --- a/src/StateMachine/ConnectedState.h +++ b/src/StateMachine/ConnectedState.h @@ -125,6 +125,12 @@ class ConnectedState : public AbstractState */ void initHttpClient(void); + /** + * Notify via URL that the system is online. + * + * @param[in] pushUrl Push URL + */ + void pushUrl(const String& pushUrl); }; /****************************************************************************** From a5d70f4b477418b94f35c66308d5900e0c3c9d21 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 25 Jun 2023 01:14:34 +0200 Subject: [PATCH 007/105] Page time limited to min. 10s and fixed the problem, that the slot time is only considered in the plugin activation. But if the slot time was changed during active period, nothing happened. --- lib/TempHumidPlugin/src/TempHumidPlugin.cpp | 15 ++++++++++----- lib/TempHumidPlugin/src/TempHumidPlugin.h | 9 +++++++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/TempHumidPlugin/src/TempHumidPlugin.cpp b/lib/TempHumidPlugin/src/TempHumidPlugin.cpp index d74d15f9..733ce71c 100644 --- a/lib/TempHumidPlugin/src/TempHumidPlugin.cpp +++ b/lib/TempHumidPlugin/src/TempHumidPlugin.cpp @@ -172,18 +172,23 @@ void TempHumidPlugin::process(bool isConnected) m_sensorUpdateTimer.start(SENSOR_UPDATE_PERIOD); } -} - -void TempHumidPlugin::active(YAGfx& gfx) -{ - MutexGuard guard(m_mutex); /* Set time to show page - either 10s or slot_time / 4 * read here because otherwise we do not get config changes during runtime in slot_time. */ if (nullptr != m_slotInterf) { m_pageTime = m_slotInterf->getDuration() / 4U; + + if (DEFAULT_PAGE_TIME > m_pageTime) + { + m_pageTime = DEFAULT_PAGE_TIME; + } } +} + +void TempHumidPlugin::active(YAGfx& gfx) +{ + MutexGuard guard(m_mutex); gfx.fillScreen(ColorDef::BLACK); m_iconCanvas.update(gfx); diff --git a/lib/TempHumidPlugin/src/TempHumidPlugin.h b/lib/TempHumidPlugin/src/TempHumidPlugin.h index 9728fc9d..25602095 100644 --- a/lib/TempHumidPlugin/src/TempHumidPlugin.h +++ b/lib/TempHumidPlugin/src/TempHumidPlugin.h @@ -83,7 +83,7 @@ class TempHumidPlugin : public Plugin m_bitmapWidget(), m_textWidget("\\calign?"), m_page(TEMPERATURE), - m_pageTime(10000U), + m_pageTime(DEFAULT_PAGE_TIME), m_timer(), m_mutex(), m_humidity(0.0F), @@ -240,7 +240,12 @@ class TempHumidPlugin : public Plugin /** * Read sensor only every N milliseconds (currently 90 seconds) */ - static const uint32_t SENSOR_UPDATE_PERIOD = SIMPLE_TIMER_SECONDS(90U); + static const uint32_t SENSOR_UPDATE_PERIOD = SIMPLE_TIMER_SECONDS(90U); + + /** + * Default time in ms how long one page will be shown until the next page. + */ + static const uint32_t DEFAULT_PAGE_TIME = SIMPLE_TIMER_SECONDS(10U); Fonts::FontType m_fontType; /**< Font type which shall be used if there is no conflict with the layout. */ WidgetGroup m_textCanvas; /**< Canvas used for the text widget. */ From 4baa88be5cb32015adb778d677c34d543e2403b0 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 25 Jun 2023 21:04:10 +0200 Subject: [PATCH 008/105] Bumblebee, github and plug icon added. Copied most of the icons to /data/configuration to provide them in the filesystem. --- data/configuration/bumblebee.bmp | Bin 0 -> 246 bytes data/configuration/earth.bmp | Bin 0 -> 246 bytes .../web => data/configuration}/github.bmp | Bin data/configuration/hacker.bmp | Bin 0 -> 246 bytes data/configuration/lightning.bmp | Bin 0 -> 246 bytes .../web => data/configuration}/plug.bmp | Bin data/configuration/santaclaus.bmp | Bin 0 -> 246 bytes data/configuration/sun.bmp | Bin 0 -> 246 bytes doc/images/icons/bumblebee.bmp | Bin 0 -> 246 bytes doc/images/icons/github.bmp | Bin 0 -> 246 bytes doc/images/icons/plug.bmp | Bin 0 -> 246 bytes 11 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/configuration/bumblebee.bmp create mode 100644 data/configuration/earth.bmp rename {lib/GithubPlugin/web => data/configuration}/github.bmp (100%) create mode 100644 data/configuration/hacker.bmp create mode 100644 data/configuration/lightning.bmp rename {lib/ShellyPlugSPlugin/web => data/configuration}/plug.bmp (100%) create mode 100644 data/configuration/santaclaus.bmp create mode 100644 data/configuration/sun.bmp create mode 100644 doc/images/icons/bumblebee.bmp create mode 100644 doc/images/icons/github.bmp create mode 100644 doc/images/icons/plug.bmp diff --git a/data/configuration/bumblebee.bmp b/data/configuration/bumblebee.bmp new file mode 100644 index 0000000000000000000000000000000000000000..b04e2f10d6981e0716559ffcd35ceacf47c65301 GIT binary patch literal 246 zcmZ?r{l)+RWp?JB0GZIa`X7!U;t1~2{|sP+ECOUh1c4-k gjZA_}0E X5OKH^kO2X3aUd7jd|cu%eOPD!O?4kJ literal 0 HcmV?d00001 diff --git a/data/configuration/lightning.bmp b/data/configuration/lightning.bmp new file mode 100644 index 0000000000000000000000000000000000000000..2fd726bcded0d910b4ced7dab35bc4ad8aeed992 GIT binary patch literal 246 zcmZ?r{l)+RWe1O y?uH8j8DK6xaiCmsUN2BP1c8*o08kvrgqsXx|NjrP6hy)SOf8T`I0noH$p8Qr^gnX| literal 0 HcmV?d00001 diff --git a/data/configuration/sun.bmp b/data/configuration/sun.bmp new file mode 100644 index 0000000000000000000000000000000000000000..aa54ce18a92f0ad492efc7e46a2f4c86ad6caa90 GIT binary patch literal 246 zcmZ?r{l)+RWzjv#6vT;5af;RvD; oA`28$-Fgv-fDE`sAOlStVm?G1*&K)#WQ%|z5XEpU5Lq}00F$Ff+W-In literal 0 HcmV?d00001 diff --git a/doc/images/icons/bumblebee.bmp b/doc/images/icons/bumblebee.bmp new file mode 100644 index 0000000000000000000000000000000000000000..b04e2f10d6981e0716559ffcd35ceacf47c65301 GIT binary patch literal 246 zcmZ?r{l)+RWp?JB0GZIa`X7!U;t1~2{|sP+ECOUh1c4-k gjZA_}0EuGU literal 0 HcmV?d00001 diff --git a/doc/images/icons/plug.bmp b/doc/images/icons/plug.bmp new file mode 100644 index 0000000000000000000000000000000000000000..9d40fbb4c1003943e0b3b43e860e0c53b6d09bf3 GIT binary patch literal 246 zcmZ?r{l)+RW Date: Sun, 25 Jun 2023 21:05:34 +0200 Subject: [PATCH 009/105] GithubPlugin and ShellyPlugSPlugin replaced by GrabViaRestPlugin. The new plugin can be configured. For the MQTT the GrabViaMqttPlugin can be used. --- config/configNormal.ini | 4 +- config/configSmall.ini | 4 +- config/configSmallNoI2s.ini | 4 +- config/configSmallUlanzi.ini | 6 +- doc/PLUGINS.md | 43 +- lib/GithubPlugin/web/GithubPlugin.jpg | Bin 6741 -> 0 bytes .../library.json | 2 +- .../src/GrabViaMqttPlugin.cpp | 557 +++++++++++++ .../src/GrabViaMqttPlugin.h} | 778 ++++++++---------- .../web/GrabViaMqttPlugin.html} | 74 +- .../web/GrabViaMqttPlugin.jpg | Bin 0 -> 15282 bytes .../library.json | 2 +- .../src/GrabViaRestPlugin.cpp} | 370 +++++++-- .../src/GrabViaRestPlugin.h} | 108 +-- .../web/GrabViaRestPlugin.html} | 428 +++++----- .../web/GrabViaRestPlugin.jpg | Bin 0 -> 15282 bytes .../src/ShellyPlugSPlugin.cpp | 508 ------------ .../web/ShellyPlugSPlugin.jpg | Bin 9258 -> 0 bytes 18 files changed, 1573 insertions(+), 1315 deletions(-) delete mode 100644 lib/GithubPlugin/web/GithubPlugin.jpg rename lib/{GithubPlugin => GrabViaMqttPlugin}/library.json (88%) create mode 100644 lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp rename lib/{ShellyPlugSPlugin/src/ShellyPlugSPlugin.h => GrabViaMqttPlugin/src/GrabViaMqttPlugin.h} (65%) rename lib/{GithubPlugin/web/GithubPlugin.html => GrabViaMqttPlugin/web/GrabViaMqttPlugin.html} (67%) create mode 100644 lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.jpg rename lib/{ShellyPlugSPlugin => GrabViaRestPlugin}/library.json (88%) rename lib/{GithubPlugin/src/GithubPlugin.cpp => GrabViaRestPlugin/src/GrabViaRestPlugin.cpp} (50%) rename lib/{GithubPlugin/src/GithubPlugin.h => GrabViaRestPlugin/src/GrabViaRestPlugin.h} (73%) rename lib/{ShellyPlugSPlugin/web/ShellyPlugSPlugin.html => GrabViaRestPlugin/web/GrabViaRestPlugin.html} (63%) create mode 100644 lib/GrabViaRestPlugin/web/GrabViaRestPlugin.jpg delete mode 100644 lib/ShellyPlugSPlugin/src/ShellyPlugSPlugin.cpp delete mode 100644 lib/ShellyPlugSPlugin/web/ShellyPlugSPlugin.jpg diff --git a/config/configNormal.ini b/config/configNormal.ini index 125534c9..b6f3ecea 100644 --- a/config/configNormal.ini +++ b/config/configNormal.ini @@ -21,7 +21,8 @@ lib_deps = DDPPlugin @ ~0.1.0 FirePlugin @ ~0.1.0 GameOfLifePlugin @ ~0.1.0 - GithubPlugin @ ~0.1.0 + GrabViaMqttPlugin @ ~0.1.0 # Requires MqttService + GrabViaRestPlugin @ ~0.1.0 GruenbeckPlugin @ ~0.1.0 IconTextLampPlugin @ ~0.1.0 IconTextPlugin @ ~0.1.0 @@ -30,7 +31,6 @@ lib_deps = OpenWeatherPlugin @ ~0.1.0 RainbowPlugin @ ~0.1.0 SensorPlugin @ ~0.1.0 - ShellyPlugSPlugin @ ~0.1.0 SignalDetectorPlugin @ ~0.1.0 # Requires AudioService SoundReactivePlugin @ ~0.1.0 # Requires AudioService SunrisePlugin @ ~0.1.0 diff --git a/config/configSmall.ini b/config/configSmall.ini index 725be8d4..3986fa71 100644 --- a/config/configSmall.ini +++ b/config/configSmall.ini @@ -21,7 +21,8 @@ lib_deps = ;DDPPlugin @ ~0.1.0 FirePlugin @ ~0.1.0 ;GameOfLifePlugin @ ~0.1.0 - GithubPlugin @ ~0.1.0 + ;GrabViaMqttPlugin @ ~0.1.0 # Requires MqttService + GrabViaRestPlugin @ ~0.1.0 ;GruenbeckPlugin @ ~0.1.0 IconTextLampPlugin @ ~0.1.0 IconTextPlugin @ ~0.1.0 @@ -30,7 +31,6 @@ lib_deps = OpenWeatherPlugin @ ~0.1.0 ;RainbowPlugin @ ~0.1.0 SensorPlugin @ ~0.1.0 - ShellyPlugSPlugin @ ~0.1.0 ;SignalDetectorPlugin @ ~0.1.0 # Requires AudioService ;SoundReactivePlugin @ ~0.1.0 # Requires AudioService SunrisePlugin @ ~0.1.0 diff --git a/config/configSmallNoI2s.ini b/config/configSmallNoI2s.ini index a4a6a425..5cb124db 100644 --- a/config/configSmallNoI2s.ini +++ b/config/configSmallNoI2s.ini @@ -21,7 +21,8 @@ lib_deps = ;DDPPlugin @ ~0.1.0 FirePlugin @ ~0.1.0 GameOfLifePlugin @ ~0.1.0 - GithubPlugin @ ~0.1.0 + ;GrabViaMqttPlugin @ ~0.1.0 # Requires MqttService + GrabViaRestPlugin @ ~0.1.0 ;GruenbeckPlugin @ ~0.1.0 IconTextLampPlugin @ ~0.1.0 IconTextPlugin @ ~0.1.0 @@ -30,7 +31,6 @@ lib_deps = OpenWeatherPlugin @ ~0.1.0 ;RainbowPlugin @ ~0.1.0 SensorPlugin @ ~0.1.0 - ;ShellyPlugSPlugin @ ~0.1.0 ;SignalDetectorPlugin @ ~0.1.0 # Requires AudioService ;SoundReactivePlugin @ ~0.1.0 # Requires AudioService SunrisePlugin @ ~0.1.0 diff --git a/config/configSmallUlanzi.ini b/config/configSmallUlanzi.ini index 59f4f678..ebe78388 100644 --- a/config/configSmallUlanzi.ini +++ b/config/configSmallUlanzi.ini @@ -21,7 +21,8 @@ lib_deps = ;DDPPlugin @ ~0.1.0 FirePlugin @ ~0.1.0 GameOfLifePlugin @ ~0.1.0 - GithubPlugin @ ~0.1.0 + GrabViaMqttPlugin @ ~0.1.0 # Requires MqttService + GrabViaRestPlugin @ ~0.1.0 ;GruenbeckPlugin @ ~0.1.0 ;IconTextLampPlugin @ ~0.1.0 IconTextPlugin @ ~0.1.0 @@ -30,13 +31,12 @@ lib_deps = OpenWeatherPlugin @ ~0.1.0 ;RainbowPlugin @ ~0.1.0 SensorPlugin @ ~0.1.0 - ;ShellyPlugSPlugin @ ~0.1.0 ;SignalDetectorPlugin @ ~0.1.0 # Requires AudioService ;SoundReactivePlugin @ ~0.1.0 # Requires AudioService SunrisePlugin @ ~0.1.0 SysMsgPlugin @ ~0.1.0 # Mandatory, can not be removed. TempHumidPlugin @ ~0.1.0 - ThreeIconPlugin @ ~0.1.0 + ;ThreeIconPlugin @ ~0.1.0 ;VolumioPlugin @ ~0.1.0 ;WifiStatusPlugin @ ~0.1.0 WormPlugin @ ~0.1.0 diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index e7a635cb..6222a816 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -8,6 +8,8 @@ The content of the display can be configured by installing an individual set of Each plugin is identified by its unique UID. * [Generic plugins](#generic-plugins) + * [GrabViaMqttPlugin](#grabviamqttplugin) + * [GrabViaRestPlugin](#grabviarestplugin) * [IconTextPlugin](#icontextplugin) * [IconTextLampPlugin](#icontextlampplugin) * [JustTextPlugin](#justtextplugin) @@ -21,13 +23,11 @@ Each plugin is identified by its unique UID. * [xlights Configuration](#xlights-configuration) * [FirePlugin](#fireplugin) * [GameOfLifePlugin](#gameoflifeplugin) - * [GithubPlugin](#githubplugin) * [GruenbeckPlugin](#gruenbeckplugin) * [MatrixPlugin](#matrixplugin) * [OpenWeatherPlugin](#openweatherplugin) * [RainbowPlugin](#rainbowplugin) * [SensorPlugin](#sensorplugin) - * [ShellyPlugSPlugin](#shellyplugsplugin) * [SignalDetectorPlugin](#signaldetectorplugin) * [SoundReactivePlugin](#soundreactiveplugin) * [SunrisePlugin](#sunriseplugin) @@ -44,27 +44,35 @@ Each plugin is identified by its unique UID. # Generic plugins The generic plugins allow the user to control the different UI elements described in the plugin name via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.3.0). +## GrabViaMqttPlugin +The plugin can grab information in JSON format via MQTT and shows it on the display. +Each part can be set separately via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/GrabViaMqttPlugin). + +## GrabViaRestPlugin +The plugin can grab information in JSON format via REST API and shows it on the display. +Each part can be set separately via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/GrabViaRestPlugin). + ## IconTextPlugin The IconTextPlugin shows an icon on left side, text on right side. If no text is set, the plugin will be skipped in the slot.\ -Each part can be set separately via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.3.0#/IconTextPlugin). +Each part can be set separately via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/IconTextPlugin). If MQTT is built in and enabled, it will support Home Assistant MQTT discovery. ## IconTextLampPlugin The IconTextLampPlugin shows an icon on left side, text on right side and lamps at the bottom.\ -Each part can be set separately via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.3.0#/IconTextLampPlugin). +Each part can be set separately via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/IconTextLampPlugin). If MQTT is built in and enabled, it will support Home Assistant MQTT discovery. ## JustTextPlugin The JustTextPlugin shows only text on the whole display. If no text is set, the plugin will be skipped in the slot.\ -The text to be displayed can be set via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.3.0#/JustTextPlugin). +The text to be displayed can be set via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/JustTextPlugin). If MQTT is built in and enabled, it will support Home Assistant MQTT discovery. ## ThreeIconPlugin The ThreeIconPlugin shows three icons on the display.\ -Each icon can be set separately via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.3.0#/ThreeIconPlugin). +Each icon can be set separately via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/ThreeIconPlugin). # Dedicated plugins Dedicated plugins are plugins which only serves one single purpose thy are only internally configurable. @@ -78,7 +86,7 @@ Powered by [CoinDesk](https://www.coindesk.com/price/bitcoin). ## CountdownPlugin The CountdownPlugin shows the remaining days until a configured target date.\ -Target date and the description of the target day (plural/singular form) can be set via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.3.0#/CountdownPlugin). +Target date and the description of the target day (plural/singular form) can be set via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/CountdownPlugin). ## DateTimePlugin The plugin shows the current time and/or date. @@ -91,7 +99,7 @@ Configure the date and time format in the plugin configuration JSON file. The fo By default the local time (see timezone in the settings) is used. It can be overwritten by the plugin configuration. -It can be set what shall be shown via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.3.0#/DateTimePlugin). +It can be set what shall be shown via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/DateTimePlugin). ## DDPPlugin The plugin setup a server supporting the Distributed Display Protocol (DDP), which is used e.g. by [xlights](https://www.xlights.org) or [LedFx](https://www.ledfx.app). @@ -128,12 +136,9 @@ The FirePlugin shows a animated fire on the display. ## GameOfLifePlugin The GameOfLifePlugin shows the game of life game on the display. -## GithubPlugin -The plugin shows the stargazers count of a github repository. - ## GruenbeckPlugin The GruenbeckPlugin shows the remaining system capacity (parameter = D_Y_10_1 ) of the Gruenbeck softliQ SC18 via the system's RESTful webservice.\ -The IP address of the Gruenbeck webserver can be set via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.3.0#/GruenbeckPlugin). +The IP address of the Gruenbeck webserver can be set via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/GruenbeckPlugin). ## MatrixPlugin The plugin shows the effect from the film "Matrix" over the whole display. @@ -142,7 +147,7 @@ The plugin shows the effect from the film "Matrix" over the whole display. The OpenWeatherPlugin shows the current weather condition (icon and temperature) and one additional information (uvIndex, humidity or windspeed) .\ Information provided by [OpenWeather](https://openweathermap.org/).\ In order to use the plugin an API key is necessary, see https://openweathermap.org/appid for further information.\ -The coordinates (latitude & longitude) of your location, your API key and the desired additional information to be displayed can be set via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.3.0#/OpenWeatherPlugin). +The coordinates (latitude & longitude) of your location, your API key and the desired additional information to be displayed can be set via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/OpenWeatherPlugin). ## RainbowPlugin The RainbowPlugin shows an animated rainbow on the display. @@ -150,26 +155,22 @@ The RainbowPlugin shows an animated rainbow on the display. ## SensorPlugin The plugin shows sensor values of the selected sensor channel. -## ShellyPlugSPlugin -The ShellyPlugSPlugin shows the current AC power being drawn via a Shelly PlugS, in watts.\ -The IP address of the Shelly PlugS webserver can be set via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.3.0#/ShellyPlugSPlugin). - ## SignalDetectorPlugin The plugin is able to detect a signal, which can be combined with up to 2 frequencies.\ Each frequency must be detected for a specific configureable time.\ As long as nothing is detected, the plugin will disable itself.\ If a signal is detected, it will be shown on the display for the configured slot duration. After slot duration timeout or user changed the slot, the plugin will be disabled until next signal detection. \ Additional a push notification can be configured. By default a GET is triggered. Using "GET" or "POST" as prefix its configureable. Example: "POST http://..." -Each part can be set separately via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.3.0#/SignalDetectorPlugin). +Each part can be set separately via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/SignalDetectorPlugin). ## SoundReactivePlugin The plugin shows octave frequency bands, depended on the environment sound. Required: A digital microphone (INMP441) is required, connected to the I2S port. -The number of shown frequency bands can be set via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.3.0#/SoundReactivePlugin). +The number of shown frequency bands can be set via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/SoundReactivePlugin). ## SunrisePlugin The SunrisePlugin shows the current sunrise / sunset times for a configured location.\ -The coordinates (latitude & longitude) of your location can be set via the [REST API]([REST.md#endpoint-base-uridisplayuidplugin-uidlocation](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.3.0#/SunrisePlugin)).\ +The coordinates (latitude & longitude) of your location can be set via the [REST API]([REST.md#endpoint-base-uridisplayuidplugin-uidlocation](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/SunrisePlugin)).\ Powered by sunrise-sunset.org Configure the time format in the plugin configuration JSON file. The format itself is according to strftime(). For colorization text properties can be added. @@ -186,7 +187,7 @@ The TestPlugin can be used to check whether the LED matrix topology (layout) is ## VolumioPlugin The VolumioPlugin shows the current VOLUMIO state as icon and the played artist/title.\ If the VOLUMIO server is offline, the plugin gets automatically disabled, otherwise enabled.\ -The host address of the Volumio webserver can be set via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.3.0#/VolumioPlugin). +The host address of the Volumio webserver can be set via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/VolumioPlugin). ## WifiStatusPlugin The WifiStatusPlugin shows the current wireless signal strength. diff --git a/lib/GithubPlugin/web/GithubPlugin.jpg b/lib/GithubPlugin/web/GithubPlugin.jpg deleted file mode 100644 index 1605cf605986c4d92013a26345934e88b788eba5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6741 zcmeHLc{r49+rP$OEY)PqJ|asdd9tJ=Lqd|Jh04-Yk`PnL+AyR=A|XVDDEm^`$~MX} zD3N{1PO@dD7-P)L{oTFKbG+aC_P)<^e9OP@eIMs>AIH61_w~EZ-+7+D^Md-IQLyo# zsktdYAP~SGJ^?fci~)*=hnI&N#mmFXheGiQ2nh=C^9zWfH?9+s5R;OW5R=#>EvK?s zT2@JRlZ3){1trz3*lk!Tc?~TMHBA*YtlG~(5GX!A0e%5dK|xV983`G+|MdaY0wF%& zji^8(6abeH0x5)m>H!7-1P?shpA-J$KyV?sd3aHL`~rgT3#A(Y7Xpdo;zsiDaC5_N z2f@z)w-Ar;<{d`7B8ShQ6fTQuhQ>VQQ{4CRt=N%I)Gb4uHAaZ`%O&E4wxT2dd$}D_=%JD&gWdtyIyc}_rB`m>*pU37qI_H_zrByuwC@QD-iT zC}@WAiSCPe`tmKmqSg_r*x4(e1jM&!k15f9Li-!C{~56B|0iUB2ln5%2EaNb0(Kr! z2;jhKS>?#}K&#Y3qSL1(_SuipgS)F1HTe^-lqp=%G)R*g&8uQ;%{5AAId*7#%Xl5D zS~c|E6WO}?gVnD6+$q&JK@b4~0*`7jOR6(C#=6q)6v_hn5Ot-&d|rEx#5Q&D9`TK} zp~r+bSYc=J40}BlHj-rof$LZ@M;EWnatpJeQ5vMHjq3uHU;1#_*WTOkNK;QnF88?N z-r}AR{n&_M2;2~97+J#3;TRf;5V(`Qz>*%nR(Un0v#z3q`nsiA_CaL9lDxN}M2TH1 zC9KNQb<-oiu^~IY6*WR^i;48Z5Z(i`=HOa#B4?xZCQ^G*J_JT<@Eqlk-!2ryjgE*2 zmX;dGcAL~~jyb#Yh?g24Pe|h>ex$+p^I!j*hJK23B_kNe>+yoE^vbjo_@SVRV?PQv zuNyRUT$l1W`5WsbwX(J+MX=|aClUL+L0gab-AutDXU0UdLzGjUqtPvw8t35TWMaU~zXYdm%q9c6{7!Q0DOF z^WI;Qj{vHFVb~6m>*A_96T>ctz$cAUo=%HzmAj6P)9(#zv+Ed+-t*ofM^X`2L7dFq zayg|7v7&P&8O?e?7KcDD7Q+@_YAa#whd>(s76hmUB#w?_Pt@g!rxOqmk+`}5K0lmi zO?mWRSM6K#P1R6|oR+si<@Bmh$sF^PyAW`6fPk?(VIWw6)UHuSVqkC(kSg>To1)xH z9OJ}lv-cQYBdD%=>dd!HdhIog3|(zUnbC|n`}-g;zcmX2Gmf@>#~-kaU(??E+wwlm zy08^Zk4jjlTdd`^cIcYvI_Idqwla_GHS2n7CJzG+SL8GVCXN9oQuFyv+&qsD1Tw}7 zE2?t8Oi*yt(vt53zM=HC`D-EJ8t1>a1bjLdH#NPxk$100*1rs~$VJMh;n=so;g&=T zFjAzZuyOPnuOkG$W)@=Jmdoi0``?}q^bM(>JDOBA*nexwK$2ki$KzQlsDUE?SJM>A z+rYlcPNMz`Sp$+zM$N+)c=cC?JAjoca$HI^Jw)KojESY2L*RaN1Zgh5{6}n!PtjF& z(lZ*j->?E4v|fW&ubpMdPv|ql^1SC7hjRNDdqok;L03rYATYVF6aqzgZEUrKNA%S2 z)Fnac2nW>$fyTVrrM3aBV^o)t8Vq4W|L4;9?sp|*_tTQeuFlbknI|H^3Y>mMzFYa= z@^Q@F*u0`RqWRjPSpx+s`(vWYt%cJk>E}(}oHgxT-$_{0$$`Lnw_H+to*M*0Jt0uv z&YQRZU*!y7-V`R{TJ%=at2d3OHp~@8Y@CT1a{Fxt0{sh$TkoA+TUnU9zaxrV5YP>R z&a7>PK=bZy^1SK@Q%#S)m&7rtw%}ANL7;uTKWctRH^P~^{h9u5*Jo`6cKYA|^UHxg z-Gs*=yH0`4Kdexo5`hk)`KVw&RY+dGv|t)8g$Y` zIHAGP5U5>+0Hd4Oxpu(3z07b9Z0Q80Rbs;^8-Km7QIU~`wFkEcsPz@KNYKQddO z^*kE_N@QaQltF;0H1~NM={%mx(T(0|u3S)aAbxO@MdW?-3ibx0oVs|3p_An%r$AMw zXL#gPPwZ1pa0$wFzBn-@Lr+8Jcsyt)O+IIobKabQDOp|=O&;?m!UI2j1py8dPtB#W zj?-GJms=+dl&UT6rd@YKNM1_2*(MvRjaYepg(L{mCFv`9xq-a2vnUz@zkE%{h_Sq% z_cgd=F%sy47fcnS_})cMcMgXb)H9w!K;a^ZF4_r!r9!oN>}VbYB5HPZtDDq39k=Ko z6W$Uk6X|8TE-ULi;E1_$HtE*mn9>>$ScJ=t0AMobI2JpTAaEL6#Kx@m>82hf+R@(6 z#>}dZJ9GxfsB>AyyjI+JaOvy8Fota^!*OIIjPN2E0yT*&RpdPS&jYjQPJscI@ul|T zMV7_2O#{kqQv+sgn;TEix<-$FtJ%(=(?5DR6p7+3xM?L)N#x02{V-O6k9Hg zU(@y%Uom}Ek!)w_05Ph`>)ikbuXEhP*Skk28cq((slQmiN#AAqb-co2eZ&4`Motcm{W#(o6BFR}J zPGz{|@q|o}B?xlDrx>U~Acg{G|00-oOU}nX#( zo%dy)b#C=kYinEb`zvWXPEOhcPdlo%RRiKnlOVGRT>r@1K-3L_4pvgfSYLT>jY~`X z>N8t?&n4w@(pVx334y&jB$fc4qYz?%ORtWbpuB5Cvv!qz!qm0;H?DpWW@p+!;F9u= zn-Xh{RbC+mFW3)Y-aMWip^FmOVzSrdV6P|)!)`ql3;T$-N|5eJotTs3?#i_EJKnj^ zeOroC#Ma-YN!xPM{j(jX9|tu86Z08R|FIf3*GB`VXP^ct0j-yOX(y8e{8^)-i8|1>`vl zHRsiNDX$$&Fge|i{eH9(qdMBvM-XNz82Xqr0&_l#e zYvyk18H^RH(B(bs{bi4fzKk3!oLi$vaH~#4!vZb%-?{W3?xEZFYBU&NoD^CeoH~s* z%F7R%QCjxSHIm)cT@Q6tiDZ~%x$jK$s<`##gvxRG0MBO7`)VdwoVw;FesMrN**uhA z`J+Mb;e}H=YlklkoPRX>M9OlHWl4!|*Un-iuBVFcz4*C849=uSFwN*EI<$wZ&1ll@ z_CW>x=A(RG@+Wsr;WN!d5kGvym~fKfE4oeoip(U9%8`{J`G*T! zr9X9**gOP^#Zz5!h$dZUyFBG9qpng;$JG}ieke|J`xNDn$Hb^BY;d)Wg^Y&d>_S2waG*NKB8i&lsqqukNnLqa;385C#LkAInuzxIq9>Ah7(bN9_U;ZHzPmxc*&Yr-J@kj&^@$NPZn(8{KASCGBVBjU>4VVsI5^Db&D)D{TXuAVV%g z;3gol*N(Xl_-46|#bk~Q3_YBDGpzmv24NOCO6#ccF3P~UN2yqKZewX)9Wv-?Z&v#F zgCIbul@>@Mrfm5UT9&|MkVFH4t;TGb;ZwwKmdrkx^a}mpOJG;tAF$JY2MSZD^36k&y zZ{6GaPh73z&cE?mESuTTjreY@j}BzGSE6r}QHO{Q)X$p^n`vvF%J!eMX{k%$+j{}I zF!Jj%tPN4hcdd%c zOQK2|&fedj&HrP|H1J77i>F7Zg1<22w*_5_aAy1kGBunLUsfA-Wxy=#`#rNoY!lfH zFJw?Wx}c!azU-;3#97qj@R$&WUes=m)>$%$hk(3?26F{2l}2LY_)-!pyh4M0BbpQ8 z4pxs;r;eI65YWV3ouv=;_mxMkysD=KC2yP%kdT?Won0i69%4`nfm>-*H0wtL<;nZ)A3bGFyu(nIuc;d%>McQ~6p^m>xFh?kcupSY1%`oeHl)8jGR z9js*;(@I|YldK~B&FZ7edXzpV#N6*QYGO|x{5~>ZR!_+HM_Gq1$vRsJX6LPzx2q`z zFEk?71xzm$U4v=Ex~qq02Q|Hj0=k&Bhf znCdIx{n}9$M}9b$bD!AvO7v~qqRTC<6oXq;YX4jx)6lSlyfI5)im)tyCqJ!aReWVE zD~f2AJ2J~OyG58TuB>`89+cO%C?V9FEg$`^=1ZaVSSl;rj=a1a0DmPgkClmJN)eggG!4; zmX}^p(GAvU$AXd-Q)~l4dC2$x+u!CuGs`2cuyb^Um7oDD^xp3zd$5N7=Sg6{L=RZl zc{)=nt}bTcXBw_fS*tOX%m~A~n-I}5oOrq{@zI(sER+v)z-^H@nYr^Ipw9kT|M18C zy{|^-&uBrV`8LY&8S!_xxSZp*Afgw$JyN9^x9Qh=wWCq)7MU}rKmMRxUT1a5PI;I7 z%ce#T$}bJ0q3kqE^HdHjKWwu&T)GetJL!un-kEW2T>ZE?Uf4dvEl*JUR(CjQ$BD@G zpPfB}TpTmQV2!KMKwfTvSJ2&^gxYOpU8*z2>A(wP=(9N!XNdOnnSuhxhjUZ}*~^dQwK z&89Bt*po&TNi^Gq5mDTvWi;nqGKmx4-~^}7B#rq`*=FN&LEMbkt}6PGQ~Wg_`Vg`r zUb=oGsqacjW3kKDOsN7KRfx>UT@fr=-8SIL4k;ur5p{ZDc2}s(w`R{-=5W)L^6{r@ zVq1$ZQUVOK{DvnMujVXA_j@1d(B89P&a(7S>mM|~WMltgn~;U`p6a|27C)!B9s+dC zYq*^`QbKftwGuiUH!tZ8f%Ig!vcF5ie9Q**np9`Ypm0LImrUz)SjGLKcAOfc*) z+kCMqayoJ`H{oGi* z`Lp$IXU8X^;IPSG+cCQCN}$WQ(@gJCdq>KXIHS#Gj*&7#O`~0+HoPk=mEfPdFBvD{ z-Ip6=maS=uCQaW@xD69tdM^1icdYt+XsWEH_eZUUv!4v5N2pI2m7H5qt`OiAhJoA4 zk|V3E-MsGG``@*@E-GelXE@ zjQs1b!%Tpu@q~uRU_h!XO}t=J@)z@+rA5yfv!t3^#E+qBaQn1>AHbX{9z}zMa$t zQ)e+0c3My}dUXQBrmC)KV}3N^)<)r)f;k_?@rUyWQNW-T#iB)X7_gN6&#?blgx{Hf Hf%^XoJ80WC diff --git a/lib/GithubPlugin/library.json b/lib/GrabViaMqttPlugin/library.json similarity index 88% rename from lib/GithubPlugin/library.json rename to lib/GrabViaMqttPlugin/library.json index 38605d28..250cd88d 100644 --- a/lib/GithubPlugin/library.json +++ b/lib/GrabViaMqttPlugin/library.json @@ -1,5 +1,5 @@ { - "name": "GithubPlugin", + "name": "GrabViaMqttPlugin", "version": "0.1.0", "description": "....", "authors": [{ diff --git a/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp b/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp new file mode 100644 index 00000000..19d08395 --- /dev/null +++ b/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp @@ -0,0 +1,557 @@ +/* MIT License + * + * Copyright (c) 2019 - 2023 Andreas Merkle + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/******************************************************************************* + DESCRIPTION +*******************************************************************************/ +/** + * @brief Grab information via REST API plugin + * @author Andreas Merkle + */ + +/****************************************************************************** + * Includes + *****************************************************************************/ +#include "GrabViaMqttPlugin.h" + +#include +#include +#include + +/****************************************************************************** + * Compiler Switches + *****************************************************************************/ + +/****************************************************************************** + * Macros + *****************************************************************************/ + +/****************************************************************************** + * Types and classes + *****************************************************************************/ + +/****************************************************************************** + * Prototypes + *****************************************************************************/ + +/****************************************************************************** + * Local Variables + *****************************************************************************/ + +/* Initialize plugin topic. */ +const char* GrabViaMqttPlugin::TOPIC_CONFIG = "/config"; + +/****************************************************************************** + * Public Methods + *****************************************************************************/ + +void GrabViaMqttPlugin::getTopics(JsonArray& topics) const +{ + (void)topics.add(TOPIC_CONFIG); +} + +bool GrabViaMqttPlugin::getTopic(const String& topic, JsonObject& value) const +{ + bool isSuccessful = false; + + if (0U != topic.equals(TOPIC_CONFIG)) + { + getConfiguration(value); + isSuccessful = true; + } + + return isSuccessful; +} + +bool GrabViaMqttPlugin::setTopic(const String& topic, const JsonObject& value) +{ + bool isSuccessful = false; + + if (0U != topic.equals(TOPIC_CONFIG)) + { + const size_t JSON_DOC_SIZE = 1024U; + DynamicJsonDocument jsonDoc(JSON_DOC_SIZE); + JsonObject jsonCfg = jsonDoc.to(); + JsonVariantConst jsonPath = value["path"]; + JsonVariantConst jsonFilter = value["filter"]; + JsonVariantConst jsonIconPath = value["iconPath"]; + JsonVariantConst jsonFormat = value["format"]; + JsonVariantConst jsonMultiplier = value["multiplier"]; + JsonVariantConst jsonOffset = value["offset"]; + + /* The received configuration may not contain all single key/value pair. + * Therefore read first the complete internal configuration and + * overwrite them with the received ones. + */ + getConfiguration(jsonCfg); + + /* Note: + * Check only for the key/value pair availability. + * The type check will follow in the setConfiguration(). + */ + + if (false == jsonPath.isNull()) + { + jsonCfg["path"] = jsonPath.as(); + isSuccessful = true; + } + + if (false == jsonFilter.isNull()) + { + if (true == jsonFilter.is()) + { + jsonCfg["filter"] = jsonFilter.as(); + isSuccessful = true; + } + else if (true == jsonFilter.is()) + { + const size_t JSON_DOC_FILTER_SIZE = 256U; + DynamicJsonDocument jsonDocFilter(JSON_DOC_SIZE); + DeserializationError result = deserializeJson(jsonDocFilter, jsonFilter.as()); + + if (DeserializationError::Ok == result) + { + jsonCfg["filter"] = jsonDocFilter.as(); + isSuccessful = true; + } + } + else + { + ; + } + } + + if (false == jsonIconPath.isNull()) + { + jsonCfg["iconPath"] = jsonIconPath.as(); + isSuccessful = true; + } + + if (false == jsonFormat.isNull()) + { + jsonCfg["format"] = jsonFormat.as(); + isSuccessful = true; + } + + if (false == jsonMultiplier.isNull()) + { + jsonCfg["multiplier"] = jsonMultiplier.as(); + isSuccessful = true; + } + + if (false == jsonOffset.isNull()) + { + jsonCfg["offset"] = jsonOffset.as(); + isSuccessful = true; + } + + if (true == isSuccessful) + { + JsonObjectConst jsonCfgConst = jsonCfg; + + isSuccessful = setConfiguration(jsonCfgConst); + + if (true == isSuccessful) + { + requestStoreToPersistentMemory(); + } + } + } + + return isSuccessful; +} + +bool GrabViaMqttPlugin::hasTopicChanged(const String& topic) +{ + MutexGuard guard(m_mutex); + bool hasTopicChanged = m_hasTopicChanged; + + /* Only a single topic, therefore its not necessary to check. */ + PLUGIN_NOT_USED(topic); + + m_hasTopicChanged = false; + + return hasTopicChanged; +} + +void GrabViaMqttPlugin::start(uint16_t width, uint16_t height) +{ + MutexGuard guard(m_mutex); + + m_layoutLeft.setPosAndSize(0, 0, ICON_WIDTH, ICON_HEIGHT); + (void)m_layoutLeft.addWidget(m_iconWidget); + + /* The text canvas is left aligned to the icon canvas and it spans over + * the whole display height. + */ + m_layoutRight.setPosAndSize(ICON_WIDTH, 0, width - ICON_WIDTH, height); + (void)m_layoutRight.addWidget(m_textWidgetRight); + + /* If only text is used, it will span over the whole display. */ + m_layoutTextOnly.setPosAndSize(0, 0, width, height); + (void)m_layoutTextOnly.addWidget(m_textWidgetTextOnly); + + /* Choose font. */ + m_textWidgetRight.setFont(Fonts::getFontByType(m_fontType)); + m_textWidgetTextOnly.setFont(Fonts::getFontByType(m_fontType)); + + /* The text widget inside the text canvas is left aligned on x-axis and + * aligned to the center of y-axis. + */ + if (height > m_textWidgetRight.getFont().getHeight()) + { + uint16_t diffY = height - m_textWidgetRight.getFont().getHeight(); + uint16_t offsY = diffY / 2U; + + m_textWidgetRight.move(0, offsY); + m_textWidgetTextOnly.move(0, offsY); + } + + /* Try to load configuration. If there is no configuration available, a default configuration + * will be created. + */ + if (false == loadConfiguration()) + { + if (false == saveConfiguration()) + { + LOG_WARNING("Failed to create initial configuration file %s.", getFullPathToConfiguration().c_str()); + } + } + else + { + /* Remember current timestamp to detect updates of the configuration in the + * filesystem without using the plugin API. + */ + updateTimestampLastUpdate(); + } + + if (false == m_iconPath.isEmpty()) + { + (void)m_iconWidget.load(FILESYSTEM, m_iconPath); + } + + m_cfgReloadTimer.start(CFG_RELOAD_PERIOD); + + subscribe(); +} + +void GrabViaMqttPlugin::stop() +{ + String configurationFilename = getFullPathToConfiguration(); + MutexGuard guard(m_mutex); + + m_cfgReloadTimer.stop(); + unsubscribe(); + + if (false != FILESYSTEM.remove(configurationFilename)) + { + LOG_INFO("File %s removed", configurationFilename.c_str()); + } +} + +void GrabViaMqttPlugin::process(bool isConnected) +{ + MutexGuard guard(m_mutex); + + /* Configuration in persistent memory updated? */ + if ((true == m_cfgReloadTimer.isTimerRunning()) && + (true == m_cfgReloadTimer.isTimeout())) + { + if (true == isConfigurationUpdated()) + { + m_reloadConfigReq = true; + } + + m_cfgReloadTimer.restart(); + } + + if (true == m_storeConfigReq) + { + if (false == saveConfiguration()) + { + LOG_WARNING("Failed to save configuration: %s", getFullPathToConfiguration().c_str()); + } + + m_storeConfigReq = false; + } + else if (true == m_reloadConfigReq) + { + LOG_INFO("Reload configuration: %s", getFullPathToConfiguration().c_str()); + + if (true == loadConfiguration()) + { + updateTimestampLastUpdate(); + } + + m_reloadConfigReq = false; + } + else + { + ; + } +} + +void GrabViaMqttPlugin::update(YAGfx& gfx) +{ + MutexGuard guard(m_mutex); + + gfx.fillScreen(ColorDef::BLACK); + + /* If a icon is available, the icon/text layout will be used otherwise the text only layout. */ + if (false == m_iconPath.isEmpty()) + { + m_layoutLeft.update(gfx); + m_layoutRight.update(gfx); + } + else + { + m_layoutTextOnly.update(gfx); + } +} + +/****************************************************************************** + * Protected Methods + *****************************************************************************/ + +/****************************************************************************** + * Private Methods + *****************************************************************************/ + +void GrabViaMqttPlugin::requestStoreToPersistentMemory() +{ + MutexGuard guard(m_mutex); + + m_storeConfigReq = true; +} + +void GrabViaMqttPlugin::getConfiguration(JsonObject& jsonCfg) const +{ + MutexGuard guard(m_mutex); + + jsonCfg["path"] = m_path; + jsonCfg["filter"] = m_filter; + jsonCfg["iconPath"] = m_iconPath; + jsonCfg["format"] = m_format; + jsonCfg["multiplier"] = m_multiplier; + jsonCfg["offset"] = m_offset; +} + +bool GrabViaMqttPlugin::setConfiguration(JsonObjectConst& jsonCfg) +{ + bool status = false; + JsonVariantConst jsonPath = jsonCfg["path"]; + JsonVariantConst jsonFilter = jsonCfg["filter"]; + JsonVariantConst jsonIconPath = jsonCfg["iconPath"]; + JsonVariantConst jsonFormat = jsonCfg["format"]; + JsonVariantConst jsonMultiplier = jsonCfg["multiplier"]; + JsonVariantConst jsonOffset = jsonCfg["offset"]; + + if (false == jsonPath.is()) + { + LOG_WARNING("JSON path not found or invalid type."); + } + else if (false == jsonFilter.is()) + { + LOG_WARNING("JSON filter not found or invalid type."); + } + else if (false == jsonIconPath.is()) + { + LOG_WARNING("JSON icon path not found or invalid type."); + } + else if (false == jsonFormat.is()) + { + LOG_WARNING("JSON format not found or invalid type."); + } + else if (false == jsonMultiplier.is()) + { + LOG_WARNING("JSON multiplier not found or invalid type."); + } + else if (false == jsonOffset.is()) + { + LOG_WARNING("JSON offset not found or invalid type."); + } + else + { + bool reqInit = false; + bool reqIcon = false; + MutexGuard guard(m_mutex); + + if (m_path != jsonPath.as()) + { + unsubscribe(); + reqInit = true; + } + + if (m_iconPath != jsonIconPath.as()) + { + reqIcon = true; + } + + m_path = jsonPath.as(); + m_filter = jsonFilter.as(); + m_iconPath = jsonIconPath.as(); + m_format = jsonFormat.as(); + m_multiplier = jsonMultiplier.as(); + m_offset = jsonOffset.as(); + + if (true == reqInit) + { + subscribe(); + } + + /* Load icon immediately */ + if (true == reqIcon) + { + if (true == m_iconPath.endsWith(".sprite")) + { + String textureFileName = m_iconPath; + + textureFileName.replace(".sprite", ".bmp"); + + if (false == m_iconWidget.loadSpriteSheet(FILESYSTEM, m_iconPath, textureFileName)) + { + LOG_WARNING("Failed to load animation %s / %s.", m_iconPath.c_str(), textureFileName.c_str()); + } + } + else if (true == m_iconPath.endsWith(".bmp")) + { + if (false == m_iconWidget.load(FILESYSTEM, m_iconPath)) + { + LOG_WARNING("Failed to load bitmap %s.", m_iconPath.c_str()); + } + } + else + { + m_iconWidget.clear(ColorDef::BLACK); + } + } + + m_hasTopicChanged = true; + + status = true; + } + + return status; +} + +void GrabViaMqttPlugin::getJsonValueByFilter(JsonObjectConst src, JsonObjectConst filter, JsonVariantConst& value) +{ + for (JsonPairConst pair : filter) + { + if (true == pair.value().is()) + { + getJsonValueByFilter(src[pair.key()], filter[pair.key()], value); + } + else + { + value = src[pair.key()]; + } + + /* Break immediately as its assumed that the filter only contains one + * single object. + */ + break; + } +} + +void GrabViaMqttPlugin::subscribe() +{ + MqttService& mqttService = MqttService::getInstance(); + + if (false == m_path.isEmpty()) + { + (void)mqttService.subscribe(m_path, + [this](const String& topic, const uint8_t* payload, size_t size) + { + this->mqttTopicCallback(topic, payload, size); + }); + } +} + +void GrabViaMqttPlugin::unsubscribe() +{ + MqttService& mqttService = MqttService::getInstance(); + + if (false == m_path.isEmpty()) + { + mqttService.unsubscribe(m_path); + } +} + +void GrabViaMqttPlugin::mqttTopicCallback(const String& topic, const uint8_t* payload, size_t size) +{ + const size_t JSON_DOC_SIZE = 1024U; + DynamicJsonDocument jsonDoc(JSON_DOC_SIZE); + DeserializationError error = deserializeJson(jsonDoc, payload, size); + + if (DeserializationError::Ok != error) + { + LOG_WARNING("MQTT payload contains invalid JSON."); + } + else + { + JsonVariantConst jsonValue; + + getJsonValueByFilter(jsonDoc.as(), m_filter.as(), jsonValue); + + /* Is it a number? */ + if (true == jsonValue.is()) + { + const size_t BUFFER_SIZE = 128U; + char buffer[BUFFER_SIZE]; + float value = jsonValue.as(); + + value *= m_multiplier; + value += m_offset; + + (void)snprintf(buffer, sizeof(buffer), m_format.c_str(), value); + + m_textWidgetRight.setFormatStr(buffer); + m_textWidgetTextOnly.setFormatStr(buffer); + } + /* Is it a string? */ + else if (true == jsonValue.is()) + { + const size_t BUFFER_SIZE = 40U; + char buffer[BUFFER_SIZE]; + + (void)snprintf(buffer, sizeof(buffer), m_format.c_str(), jsonValue.as().c_str()); + + m_textWidgetRight.setFormatStr(buffer); + m_textWidgetTextOnly.setFormatStr(buffer); + } + else + { + m_textWidgetRight.setFormatStr("\\calign-"); + m_textWidgetTextOnly.setFormatStr("\\calign-"); + } + } +} + +/****************************************************************************** + * External Functions + *****************************************************************************/ + +/****************************************************************************** + * Local Functions + *****************************************************************************/ diff --git a/lib/ShellyPlugSPlugin/src/ShellyPlugSPlugin.h b/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.h similarity index 65% rename from lib/ShellyPlugSPlugin/src/ShellyPlugSPlugin.h rename to lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.h index b6a27cd4..d4596eef 100644 --- a/lib/ShellyPlugSPlugin/src/ShellyPlugSPlugin.h +++ b/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.h @@ -1,415 +1,363 @@ -/* MIT License - * - * Copyright (c) 2019 - 2023 Andreas Merkle - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -/******************************************************************************* - DESCRIPTION -*******************************************************************************/ -/** - * @brief ShellyPlugSPlugin plugin - * @author Yann Le Glaz - - * - * @addtogroup plugin - * - * @{ - */ - -#ifndef SHELLYPLUGSPLUGIN_H -#define SHELLYPLUGSPLUGIN_H - -/****************************************************************************** - * Compile Switches - *****************************************************************************/ - -/****************************************************************************** - * Includes - *****************************************************************************/ -#include "AsyncHttpClient.h" -#include "Plugin.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -/****************************************************************************** - * Macros - *****************************************************************************/ - -/****************************************************************************** - * Types and Classes - *****************************************************************************/ - -/** - * Shows the current AC power being drawn via a Shelly PlugS, in watts. - */ -class ShellyPlugSPlugin : public Plugin, private PluginConfigFsHandler -{ -public: - - /** - * Constructs the plugin. - * - * @param[in] name Plugin name - * @param[in] uid Unique id - */ - ShellyPlugSPlugin(const String& name, uint16_t uid) : - Plugin(name, uid), - PluginConfigFsHandler(uid, FILESYSTEM), - m_fontType(Fonts::FONT_TYPE_DEFAULT), - m_textCanvas(), - m_iconCanvas(), - m_bitmapWidget(), - m_textWidget("?"), - m_ipAddress("192.168.1.123"), /* Example data */ - m_client(), - m_mutex(), - m_requestTimer(), - m_cfgReloadTimer(), - m_storeConfigReq(false), - m_reloadConfigReq(false), - m_hasTopicChanged(false), - m_taskProxy() - { - (void)m_mutex.create(); - } - - /** - * Destroys the plugin. - */ - ~ShellyPlugSPlugin() - { - m_client.regOnResponse(nullptr); - m_client.regOnClosed(nullptr); - m_client.regOnError(nullptr); - - /* Abort any pending TCP request to avoid getting a callback after the - * object is destroyed. - */ - m_client.end(); - - clearQueue(); - - m_mutex.destroy(); - } - - /** - * Plugin creation method, used to register on the plugin manager. - * - * @param[in] name Plugin name - * @param[in] uid Unique id - * - * @return If successful, it will return the pointer to the plugin instance, otherwise nullptr. - */ - static IPluginMaintenance* create(const String& name, uint16_t uid) - { - return new ShellyPlugSPlugin(name, uid); - } - - /** - * Get font type. - * - * @return The font type the plugin uses. - */ - Fonts::FontType getFontType() const final - { - return m_fontType; - } - - /** - * Set font type. - * The plugin may skip the font type in case it gets conflicts with the layout. - * - * A font type change will only be considered if it is set before the start() - * method is called! - * - * @param[in] fontType The font type which the plugin shall use. - */ - void setFontType(Fonts::FontType fontType) final - { - m_fontType = fontType; - return; - } - - /** - * Get plugin topics, which can be get/set via different communication - * interfaces like REST, websocket, MQTT, etc. - * - * Example: - * { - * "topics": [ - * "/text" - * ] - * } - * - * By default a topic is readable and writeable. - * This can be set explicit with the "access" key with the following possible - * values: - * - Only readable: "r" - * - Only writeable: "w" - * - Readable and writeable: "rw" - * - * Example: - * { - * "topics": [{ - * "name": "/text", - * "access": "r" - * }] - * } - * - * @param[out] topics Topis in JSON format - */ - void getTopics(JsonArray& topics) const final; - - /** - * Get a topic data. - * Note, currently only JSON format is supported. - * - * @param[in] topic The topic which data shall be retrieved. - * @param[out] value The topic value in JSON format. - * - * @return If successful it will return true otherwise false. - */ - bool getTopic(const String& topic, JsonObject& value) const final; - - /** - * Set a topic data. - * Note, currently only JSON format is supported. - * - * @param[in] topic The topic which data shall be retrieved. - * @param[in] value The topic value in JSON format. - * - * @return If successful it will return true otherwise false. - */ - bool setTopic(const String& topic, const JsonObject& value) final; - - /** - * Is the topic content changed since last time? - * Every readable volatile topic shall support this. Otherwise the topic - * handlers might not be able to provide updated information. - * - * @param[in] topic The topic which to check. - * - * @return If the topic content changed since last time, it will return true otherwise false. - */ - bool hasTopicChanged(const String& topic) final; - - /** - * Start the plugin. This is called only once during plugin lifetime. - * It can be used as deferred initialization (after the constructor) - * and provides the canvas size. - * - * If your display layout depends on canvas or font size, calculate it - * here. - * - * Overwrite it if your plugin needs to know that it was installed. - * - * @param[in] width Display width in pixel - * @param[in] height Display height in pixel - */ - void start(uint16_t width, uint16_t height) final; - - /** - * Stop the plugin. This is called only once during plugin lifetime. - * It can be used as a first clean-up, before the plugin will be destroyed. - * - * Overwrite it if your plugin needs to know that it will be uninstalled. - */ - void stop() final; - - /** - * Process the plugin. - * Overwrite it if your plugin has cyclic stuff to do without being in a - * active slot. - * - * @param[in] isConnected The network connection status. If network - * connection is established, it will be true otherwise false. - */ - void process(bool isConnected) final; - - /** - * Update the display. - * The scheduler will call this method periodically. - * - * @param[in] gfx Display graphics interface - */ - void update(YAGfx& gfx) final; - - /** - * Get ip-address. - * - * @return IP-address - */ - String getIPAddress() const; - - /** - * Set ip-address. - * - * @param[in] ipAddress IP-address - */ - void setIPAddress(const String& ipAddress); - -private: - - /** - * Icon width in pixels. - */ - static const int16_t ICON_WIDTH = 8; - - /** - * Icon height in pixels. - */ - static const int16_t ICON_HEIGHT = 8; - - /** - * Image path within the filesystem. - */ - static const char* IMAGE_PATH; - - /** - * Plugin topic, used to read/write the configuration. - */ - static const char* TOPIC_CONFIG; - - /** - * Period in ms for requesting power consumption from the Shelly PlugS. - * This is used in case the last request to the server was successful. - */ - static const uint32_t UPDATE_PERIOD = SIMPLE_TIMER_SECONDS(15U); - - /** - * Short period in ms for requesting power consumption from the Shelly PlugS. - * This is used in case the request to the server failed. - */ - static const uint32_t UPDATE_PERIOD_SHORT = SIMPLE_TIMER_SECONDS(10U); - - /** - * The configuration in the persistent memory shall be cyclic loaded. - * This mechanism ensure that manual changes in the file are considered. - * This is the reload period in ms. - */ - static const uint32_t CFG_RELOAD_PERIOD = SIMPLE_TIMER_SECONDS(30U); - - Fonts::FontType m_fontType; /**< Font type which shall be used if there is no conflict with the layout. */ - WidgetGroup m_textCanvas; /**< Canvas used for the text widget. */ - WidgetGroup m_iconCanvas; /**< Canvas used for the bitmap widget. */ - BitmapWidget m_bitmapWidget; /**< Bitmap widget, used to show the icon. */ - TextWidget m_textWidget; /**< Text widget, used for showing the text. */ - String m_ipAddress; /**< IP-address of the ShellyPlugS server. */ - AsyncHttpClient m_client; /**< Asynchronous HTTP client. */ - mutable MutexRecursive m_mutex; /**< Mutex to protect against concurrent access. */ - SimpleTimer m_requestTimer; /**< Timer is used for cyclic ShellyPlugS http request. */ - SimpleTimer m_cfgReloadTimer; /**< Timer is used to cyclic reload the configuration from persistent memory. */ - bool m_storeConfigReq; /**< Is requested to store the configuration in persistent memory? */ - bool m_reloadConfigReq; /**< Is requested to reload the configuration from persistent memory? */ - bool m_hasTopicChanged; /**< Has the topic content changed? */ - - /** - * Defines the message types, which are necessary for HTTP client/server handling. - */ - enum MsgType - { - MSG_TYPE_INVALID = 0, /**< Invalid message type. */ - MSG_TYPE_RSP /**< A response, caused by a previous request. */ - }; - - /** - * A message for HTTP client/server handling. - */ - struct Msg - { - MsgType type; /**< Message type */ - DynamicJsonDocument* rsp; /**< Response, only valid if message type is a response. */ - - /** - * Constructs a message. - */ - Msg() : - type(MSG_TYPE_INVALID), - rsp(nullptr) - { - } - }; - - /** - * Task proxy used to decouple server responses, which happen in a different task context. - */ - TaskProxy m_taskProxy; - - /** - * Request to store configuration to persistent memory. - */ - void requestStoreToPersistentMemory(); - - /** - * Get configuration in JSON. - * - * @param[out] cfg Configuration - */ - void getConfiguration(JsonObject& cfg) const final; - - /** - * Set configuration in JSON. - * - * @param[in] cfg Configuration - * - * @return If successful set, it will return true otherwise false. - */ - bool setConfiguration(JsonObjectConst& cfg) final; - - /** - * Request new data. - * - * @return If successful it will return true otherwise false. - */ - bool startHttpRequest(void); - - /** - * Register callback function on response reception. - */ - void initHttpClient(void); - - /** - * Handle a web response from the server. - * - * @param[in] jsonDoc Web response as JSON document - */ - void handleWebResponse(DynamicJsonDocument& jsonDoc); - - /** - * Clear the task proxy queue. - */ - void clearQueue(); -}; - -/****************************************************************************** - * Functions - *****************************************************************************/ - -#endif /* SHELLYPLUGSPLUGIN_H */ - -/** @} */ \ No newline at end of file +/* MIT License + * + * Copyright (c) 2019 - 2023 Andreas Merkle + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/******************************************************************************* + DESCRIPTION +*******************************************************************************/ +/** + * @brief Grab information via REST API plugin + * @author Andreas Merkle + * + * @addtogroup plugin + * + * @{ + */ + +#ifndef GRAB_VIA_REST_PLUGIN_H +#define GRAB_VIA_REST_PLUGIN_H + +/****************************************************************************** + * Compile Switches + *****************************************************************************/ + +/****************************************************************************** + * Includes + *****************************************************************************/ +#include +#include "Plugin.hpp" + +#include +#include +#include +#include +#include + +/****************************************************************************** + * Macros + *****************************************************************************/ + +/****************************************************************************** + * Types and Classes + *****************************************************************************/ + +/** + * Grab information from a MQTT broker and display it. + */ +class GrabViaMqttPlugin : public Plugin, private PluginConfigFsHandler +{ +public: + + /** + * Constructs the plugin. + * + * @param[in] name Plugin name + * @param[in] uid Unique id + */ + GrabViaMqttPlugin(const String& name, uint16_t uid) : + Plugin(name, uid), + PluginConfigFsHandler(uid, FILESYSTEM), + m_fontType(Fonts::FONT_TYPE_DEFAULT), + m_layoutRight(), + m_layoutLeft(), + m_layoutTextOnly(), + m_iconWidget(), + m_textWidgetRight("\\calign?"), + m_textWidgetTextOnly("\\calign?"), + m_path(), + m_filter(1024U), + m_iconPath(), + m_format("%s"), + m_multiplier(1.0f), + m_offset(0.0f), + m_mutex(), + m_cfgReloadTimer(), + m_storeConfigReq(false), + m_reloadConfigReq(false), + m_hasTopicChanged(false) + { + (void)m_mutex.create(); + } + + /** + * Destroys the plugin. + */ + ~GrabViaMqttPlugin() + { + m_mutex.destroy(); + } + + /** + * Plugin creation method, used to register on the plugin manager. + * + * @param[in] name Plugin name + * @param[in] uid Unique id + * + * @return If successful, it will return the pointer to the plugin instance, otherwise nullptr. + */ + static IPluginMaintenance* create(const String& name, uint16_t uid) + { + return new GrabViaMqttPlugin(name, uid); + } + + /** + * Get font type. + * + * @return The font type the plugin uses. + */ + Fonts::FontType getFontType() const final + { + return m_fontType; + } + + /** + * Set font type. + * The plugin may skip the font type in case it gets conflicts with the layout. + * + * A font type change will only be considered if it is set before the start() + * method is called! + * + * @param[in] fontType The font type which the plugin shall use. + */ + void setFontType(Fonts::FontType fontType) final + { + m_fontType = fontType; + return; + } + + /** + * Get plugin topics, which can be get/set via different communication + * interfaces like REST, websocket, MQTT, etc. + * + * Example: + * { + * "topics": [ + * "/text" + * ] + * } + * + * By default a topic is readable and writeable. + * This can be set explicit with the "access" key with the following possible + * values: + * - Only readable: "r" + * - Only writeable: "w" + * - Readable and writeable: "rw" + * + * Example: + * { + * "topics": [{ + * "name": "/text", + * "access": "r" + * }] + * } + * + * @param[out] topics Topis in JSON format + */ + void getTopics(JsonArray& topics) const final; + + /** + * Get a topic data. + * Note, currently only JSON format is supported. + * + * @param[in] topic The topic which data shall be retrieved. + * @param[out] value The topic value in JSON format. + * + * @return If successful it will return true otherwise false. + */ + bool getTopic(const String& topic, JsonObject& value) const final; + + /** + * Set a topic data. + * Note, currently only JSON format is supported. + * + * @param[in] topic The topic which data shall be retrieved. + * @param[in] value The topic value in JSON format. + * + * @return If successful it will return true otherwise false. + */ + bool setTopic(const String& topic, const JsonObject& value) final; + + /** + * Is the topic content changed since last time? + * Every readable volatile topic shall support this. Otherwise the topic + * handlers might not be able to provide updated information. + * + * @param[in] topic The topic which to check. + * + * @return If the topic content changed since last time, it will return true otherwise false. + */ + bool hasTopicChanged(const String& topic) final; + + /** + * Start the plugin. This is called only once during plugin lifetime. + * It can be used as deferred initialization (after the constructor) + * and provides the canvas size. + * + * If your display layout depends on canvas or font size, calculate it + * here. + * + * Overwrite it if your plugin needs to know that it was installed. + * + * @param[in] width Display width in pixel + * @param[in] height Display height in pixel + */ + void start(uint16_t width, uint16_t height) final; + + /** + * Stop the plugin. This is called only once during plugin lifetime. + * It can be used as a first clean-up, before the plugin will be destroyed. + * + * Overwrite it if your plugin needs to know that it will be uninstalled. + */ + void stop() final; + + /** + * Process the plugin. + * Overwrite it if your plugin has cyclic stuff to do without being in a + * active slot. + * + * @param[in] isConnected The network connection status. If network + * connection is established, it will be true otherwise false. + */ + void process(bool isConnected) final; + + /** + * Update the display. + * The scheduler will call this method periodically. + * + * @param[in] gfx Display graphics interface + */ + void update(YAGfx& gfx) final; + +private: + + /** + * Icon width in pixels. + */ + static const uint16_t ICON_WIDTH = 8U; + + /** + * Icon height in pixels. + */ + static const uint16_t ICON_HEIGHT = 8U; + + /** + * Plugin topic, used to read/write the configuration. + */ + static const char* TOPIC_CONFIG; + + /** + * The configuration in the persistent memory shall be cyclic loaded. + * This mechanism ensure that manual changes in the file are considered. + * This is the reload period in ms. + */ + static const uint32_t CFG_RELOAD_PERIOD = SIMPLE_TIMER_SECONDS(30U); + + Fonts::FontType m_fontType; /**< Font type which shall be used if there is no conflict with the layout. */ + WidgetGroup m_layoutRight; /**< Canvas used for the text widget in a layout with icon on the left side. */ + WidgetGroup m_layoutLeft; /**< Canvas used for the bitmap widget in a layout with text on the right side. */ + WidgetGroup m_layoutTextOnly; /**< Canvas used in case only text is shown. */ + BitmapWidget m_iconWidget; /**< Bitmap widget, used to show the icon. */ + TextWidget m_textWidgetRight; /**< Text widget, used in layout with icon. */ + TextWidget m_textWidgetTextOnly; /**< Text widget, used in layout without icon. */ + String m_path; /**< MQTT topic path */ + DynamicJsonDocument m_filter; /**< Filter used for the response in JSON format. */ + String m_iconPath; /**< Icon filename with path. */ + String m_format; /**< Format used to embed the retrieved filtered value. */ + float m_multiplier; /**< If grabbed value is a number, it will be multiplied with the multiplier. */ + float m_offset; /**< If grabbed value is a number, the offset will be added after the multiplication with the multiplier. */ + mutable MutexRecursive m_mutex; /**< Mutex to protect against concurrent access. */ + SimpleTimer m_cfgReloadTimer; /**< Timer is used to cyclic reload the configuration from persistent memory. */ + bool m_storeConfigReq; /**< Is requested to store the configuration in persistent memory? */ + bool m_reloadConfigReq; /**< Is requested to reload the configuration from persistent memory? */ + bool m_hasTopicChanged; /**< Has the topic content changed? */ + + /** + * Request to store configuration to persistent memory. + */ + void requestStoreToPersistentMemory(); + + /** + * Get configuration in JSON. + * + * @param[out] cfg Configuration + */ + void getConfiguration(JsonObject& cfg) const final; + + /** + * Set configuration in JSON. + * + * @param[in] cfg Configuration + * + * @return If successful set, it will return true otherwise false. + */ + bool setConfiguration(JsonObjectConst& cfg) final; + + /** + * Request new data. + * + * @return If successful it will return true otherwise false. + */ + bool startHttpRequest(void); + + /** + * Get value from JSON source by the filter. + * + * @param[in] src Source in JSON format + * @param[in] filter Filter in JSON format + * @param[out] value Value in JSON format + */ + void getJsonValueByFilter(JsonObjectConst src, JsonObjectConst filter, JsonVariantConst& value); + + /** + * Clear the task proxy queue. + */ + void clearQueue(); + + /** + * Subscribe MQTT topic to be informed about value changes. + */ + void subscribe(); + + /** + * Unsubscribe MQTT topic to stop on change notifications. + */ + void unsubscribe(); + + /** + * The MQTT callback is registered by subscription and will be called on change by + * the MQTT service. + * + * @param[in] topic Topic + * @param[in] payload Topic payload + * @param[in] size Topic payload size in byte + */ + void mqttTopicCallback(const String& topic, const uint8_t* payload, size_t size); +}; + +/****************************************************************************** + * Functions + *****************************************************************************/ + +#endif /* GRAB_VIA_REST_PLUGIN_H */ + +/** @} */ diff --git a/lib/GithubPlugin/web/GithubPlugin.html b/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html similarity index 67% rename from lib/GithubPlugin/web/GithubPlugin.html rename to lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html index 98c0de08..bea11250 100644 --- a/lib/GithubPlugin/web/GithubPlugin.html +++ b/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html @@ -32,20 +32,24 @@
-

GithubPlugin

-

Screenshot

-

The plugin shows the stargazers count of a github repository.

+

GrabViaMqttPlugin

+

Screenshot

+

The plugin can grab information in JSON format via MQTT and shows it on the display.

REST API

-

Get github user and repository name

-
GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/github
-

Set github user and/or repository name

-
POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/github?user=<USER>&repository=<REPOSITORY>
-
POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/github?user=<USER>&repository=<REPOSITORY>
+

Get configuration

+
GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/config
+

Set configuration

+
POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/config?path=<PATH>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
+
POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/config?path=<PATH>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
  • PLUGIN-UID: The plugin unique id.
  • PLUGIN-ALIAS: The plugin alias name.
  • -
  • USER: The name of the user who owns the repository.
  • -
  • REPOSITORY: The name of the repository.
  • +
  • PATH: MQTT topic path.
  • +
  • FILTER: Filter to identify the value in the JSON response.
  • +
  • ICON-PATH: Filename of the icon including the path.
  • +
  • FORMAT: Format specifier, e.g. "%s" for strings or "%f" for numbers.
  • +
  • MULTIPLIER: Number which is multiplied with a number value. Not used for string values.
  • +
  • OFFSET: Number which is added to a number value. Not used for string values.

Configuration

User

@@ -56,15 +60,31 @@

User

- - + +
- - -
+ + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
- +
@@ -90,7 +110,7 @@

User

- - - - - - - - - - - + + + + + + + + + + + + PIXELIX + + + +
+ + +
+ + +
+
+

GrabViaRestPlugin

+

Screenshot

+

The plugin can grab information in JSON format via REST API and shows it on the display.

+

REST API

+

Get configuration

+
GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/config
+

Set configuration

+
POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/config?method=<METHOD>&url=<URL>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
+
POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/config?method=<METHOD>&url=<URL>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
+
    +
  • PLUGIN-UID: The plugin unique id.
  • +
  • PLUGIN-ALIAS: The plugin alias name.
  • +
  • METHOD: HTTP method, supported are "GET" and "POST".
  • +
  • URL: REST API URL.
  • +
  • FILTER: Filter to identify the value in the JSON response.
  • +
  • ICON-PATH: Filename of the icon including the path.
  • +
  • FORMAT: Format specifier, e.g. "%s" for strings or "%f" for numbers.
  • +
  • MULTIPLIER: Number which is multiplied with a number value. Not used for string values.
  • +
  • OFFSET: Number which is added to a number value. Not used for string values.
  • +
+

Configuration

+

User

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ + +
+
+
+ (C) 2019 - 2023 Andreas Merkle (web@blue-andi.de)
+ MIT License +
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.jpg b/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6fea5c23aae83db825840644973b98663363425c GIT binary patch literal 15282 zcmeHt2UJtr*6xl0f`9@F(u;u7dq;|hbO9-Xf)J5jLZ}jI1O)^^5Cl{ZPysgG2Zyyu*lkLudKD^x8|Djn{z|?N*M!~ zjPwlk00;yETmydq$~e=6F3Q6d08C5(F#rH)0V>ED;Ly(+Kx-7|0Z@T;2>8Gv`^mv4 zzy$t&THpp?U>gH?uL%}_djb4Idr2RWg}^Taej)JR5dlYpi@%q*3jk23 zKz}j`7LYOIrBMA@uS~uFvksM!1Ax@jzv_Rn3fcz;$o*m!{~h(`m(0Hq_=UhP1b!j# z3xU57kdu*+-u7&b-bnX6)Z1>^mmh%RaI4$mXVW|lamB{NCt%X1UjN5eF6mk z7~s51fRn$6Z=eU#hyP$e$Lq+TKn;PQAP;9XS4UUH>(0)KlGh#Ooh4;uogF0|o#kaE zo#o}7l@(=Gxco7yzOnD=i@X`=b9-bAk~4 zIqAQIACwX7qo(8U;uz?19xR#%+bSueBq^t8^^dKVl95$Xm;R@I>e2^b{FjCPXIK7j zp{1$r?4;&&uy&;X@6%hl`2FAQwzr4o9~Am`0m1vL~b4{{IKpuSxzE$iLwF1=rs~;BR&QrCq<^`dbM6t;E&j{^GeVJ|MXb z23ag+1keFs)YLT8R4^K98d?~PmX4VoG07LyYe6Bj!e1OlU_r8`W=LQl^k#?8(x_O~y}dw`i1u!p!pAp*c5 zW(brSLTLr~z;}<-;M>ZB7nXlqkV8-^Y8n{rVLE!SLlqNn2m*y3qJmOWQ-QB4Aratn zfQp&=*hyJ!n&akDkv4~KrYCkV6Z@$si;rN(j3z^hdKHk7m$mh zWjS~MX+_gvL3s;2>vjJgx)VYQGKz0nlqKNx%L#`cKQ1!XUvZ6C(L@@ z-5%B>${DVqG*sGE)f4?8k1!;4d_!hz)tM4|Y3DDt#zKg#c+){EA?_3H<_>&tZDMLf zAkotL`229_dGEp>9g$1#y;b?y^~|dE2OGol7%vh%#`fu3c{$(K4lG6X$k|tV{s<^! z5#V;bd^8`YT?{V0p@E#nZR^ngw$6Q;&;7xQITa?kq5Zd)sEyL@+iMH)c!1TNT0KYHP@(7ahqjrC{W&+$3DrbifkOFiQ ztsR&tK%N=-LMH|Ifa~7es2Nt*p#Wza*A^1!I8^ zUZMaK7%rk|cdObZa`KUagE6FB)1A3^Ql=!FgDgi297(#wOXcBmW;?_YkgI=jlgaNr_bzH>zn*=B_W2B z_FJ+it9(%HTWEq!w7PJ}1dlZZ(0|<9?e;LE�GYrAUTTM(x#V?@jgU1d+T$2@8>Y zB&~r+!Nv7?PM#0hY)r#deTtK>pI-cwf9Peqs^7?99Rj)KNw{|_&y%xZrr>EqW3Jb! z{t}jTHWyuqbGMi{-cJf$gr%4}j8FiE;z{%_9T80d4hd6$NyX_#S$x5EwEC5h@nvhf zkn*LikYw-QdY;Uc%Up)`#Q9W0;&oXrLUdV2!qpC7Fd_Gln`--(DZrb0jPmkpHJ^3m zNuPe?3im8e5r{*Bahui~3Xo9GJbtE|X`yCUeJ~?Ty|wzWb;@dx_RXHCfw;)@=}HKu z0Nq}KZbYX%OA;q**zK>AVpt%HT8xVOyq)iDgBn)KoRD%Mb=7@s)GHE&R_ANa{bmIIYsZ?`?591gcC9s2l0F5vR)X1Q z8K{G+6Jx(vY}xPbhJ-xHwH*958nqtR9N}f$S1%fH^l9;xVIy35lkgMsJ6~_92)}@> zm1kWdoQ^wP@T@X)FsGAeQ9ix)98*M&V_G*IpG` z++>a274>9e`f=2uvjz^AKDlpZ7Z3?+W3efX>_M-YOQ4rn`N&Bb^*N1k^mdCZPS%6( z+AY*m{lFtsOZ)pd!jxfM%yv#a&fJeJ0MTN)2)iAAq`x#UIdPYDt0V;JL_ zEH;MtNwQIHjMkE(8wGgY+XxE7$VKFugwxIQ(I|-X*tFJaBz=RsuS>2VC+?m9+J%KlF zoB|My&^1+0v@WcZJEkM5=TELH?F@~)O`H@|*<_vLB$ zCuah2dO_vcU5l6;J(*W@c}j*tV-N?fJu!5LWj}U_UYeX7-Iz*DrT{{3KA^X#>%m=Q zJPRX9psCPbra|FIoZ6rLB?4EflY(_*ZJ#tubBH=Zp|0|!di0tfO02huNy0G$9|PN)qj zv}SL1hfX}7kW6}zdx8Rdy|E5HS7d<wIv*BhU{q| z=R!qR?e*gD)h$gw&QvD1)DB|Yw;v<=j74P(z8WI%&jZf`!KtWj=>4uM_^j5nc~HMM zvYhec7pUUd>nPr@g061r$VYaGlPPMEitmKurB>8~3k~^V6(b1qHFu{uB$Z|4M#5XR ziSLIY9k3a8Sa!IcAag>7K!LK>jkVszu3_FvQQdQL)5|@n#7>?5llapOLOEgsv*-JK zIG&}7pM4EE9{$1`7~2@THK%L1rQt@7{ccAlU7-Mkgyc5cnWxH8igQ!=a?BykEZ;JT z)M3^{_NsIR;@zCJ&d4~434b!xfPaGAPsk3rk?XlGRc zf^|)iame@@nQh>yXtBIV2!iopWT&rEqG?Eza5e+9%lBw*{Z_6Ua)knjra%9T2wq>g zy2lIp3mu;_3J@s)9?1_*L?fIvlG;6qEkRL5P%WJLx&eYNZw*JMe9jbRYN$Dbp#VOf zd>i7PquX136d)OwGsnjPQUD^-q;ExHQz@gjSA-{3v_ocUys=NMni`rPWMJt8Y8 z41E#lt6E+gR1xSWQy}X9LFK!>6zGM3mDU2^Yz)M_5&IjgId@@!amJ(|7)w0$6<+7>Au0pri#RtLXS6k86Nq+%%A>}lTh!*ZF5Q&2t&4@<9U^d@dV?RVI}ND zSWJv-4r;o(vdT~(sOq7!s{rGt-(pTgI~ z?x$DnE?@OL_F`CR#7@CWaOQ`0)BSW*SBT^L6%^)`*0>1h_~yQGl!k-Z0JSz*D8&Z+N1zoj*u{xZVDE|<-u-2{!6b%>$>M_`0Anq}RB@AFvv z-H12#{fcP$<*>*Rp4y$uYXLPK@oKZ#vw3Z*-Fe|ha8+E=w>ViFYHn{PofF#A8_DwU z9#sC^8W$zB@Xd|8XeyW+mrL%qE%*UCP}-Ui1xX!)H8QSd`L$h5R<7`w@JH}M)h@L} zw`>4K&0>D`X9?a#woBis)l2s%oF|`LLr9Dp@Fy=>FIO<1E9yv0y9pDTc}ph(oySIT zt|mkZEi?-CS7A?hp1|GsGW1^Pb#*0NR{5$711}Bt32!FI&b@#(HM4uRN1wako$TMf zBe+%+ozHdqjtF3Vn6&Q4KGlAxhNGK7m8YF`mIAz9DGn~A0L_+L8uS+jNZqe}_E<6& zr=7amR@JN)bAGC+W)z{*p&uE^;8ne?d3;cTn7g!B?&I>ty^oDK=X}7MfJEOHXt!RhxZXL+sOK0NK9DHY<1tt$>5~5t_&D=G+{X0a!Uf)w z>t)g>QNp0Oiw!(RuRbO+rWo<|l&fB8%SQcsMMeBLHA5#y`~)(w88i#RQ> zeyJNfZ$dn?s*AMOih%H!Fld!)&rdx0kVII^W03A0saKS6P{2v1nw%cUV1HU6$}Uq( zS66oTS|EqeZW>&R+*{427To$oQ~a~-`lTDaPL7Hb=gOaVJw)(YxxbxMPh1O`PtH?^ zPH0###{S?;kfvVD6y;6xEK#t$d^?5KipCyy@>OW!&4{y@TngY}xA#PU!ZcTMCM~2u zZCS+C;_8 zq#_9N3;Y-KaqOHc=qKrsWMv0*;rzC3V+p7jgb-YCnh=xzTdD0EbkBuu${9>$+vK5J zG{a3Pz*9F0AiRtvP6Pfu3v9Lx11kkZ#+k!Kp3iw3m~ zeL=MKS#@WM`ExXys?~T({Wqbyr$Gn*R(R?WWzcGf*oUtTwJ*}%Z6R8FH?1Lx~NL9QE%^HT`aEZK1M4h2|Z z4WaC$|dVn^K}$#5iRXPH)O>HX&*+@UX5l9y-Q|VWuO3G>cGt& zV}O%P{D(7N+cu1vnaPJ9z1nR$in(7l2D%S2L3%&Nh5{HY7ZKt}Xp|SZ8T2gq!HP-a za2U~bV!idyWJ~RZXFP@$GF!6MANC~cuDPH%TvaxOwx54+V8nlJn2q~@jSGBudpJ!f zO2Wy0amy~Ysd-KArQZT%*>*U$>N`mTTesk}8nBGpR2HA~+->>iwK)?ln{#;I;(hLF zwB0H+mdIc_^Kr}X%^CLoR7396FkGbly@Ke(Y1dVx6xqAfnimzvT7}u=HUooE3Lp>L zuR|}}ZZd*gtUW1rKPHd@>`TN}AV{L)GlY!iPA$dL=@S3 zJ*LSlzIwV$$V+niL4aCOEXaJRCWW%Y9s_H+L+fPYnmFt-Xr&AbFx4epB>gcySQ7?i zs-iCZv@Mgf^8Rndh0RoVx3~(2-%AVn7dNfsZI$05v$jWT&6ylT_jKUyPq;(J=)}R; z6d=NywsS$F5EL@5aWpKxGV98z411*$A?(fhZ;rhaN=r2f2dS;mT&?vjg0NZDmyJ#h zrSBULr`$`I2U%R@FVHjPF^oZS9nbP=>jv^ds-xE@%wo%sC+~%YRC!&VPOxG~6@8>_ z4U48#;m3jJ2VbrZW-!hnTzF3`>1&LUCavA4vzf{lfe)P_SvzzyJNzXJ-^v`7%vtAZb%yZ*!E zG+FD&~Po;E9i8Vt)oKl{e!Hc1r|uGBA8^%-j1Uw+&=RWW1jlj;*I9WcLm$S>7Y z<1=3+2JDnaQtDf&)PNfI`S4laQ_AT2q+;*5q8a?95u;?ukH(|d7oi@?DhUPns0~Bi3m6L%!v?QfQWhJjTm}? zh_NB;OcEVV8llfPX>datb-tkwTS|>;#zq%tE0Oo z%;FK=->-+mj6^{12 zJVqg+9|bsN)||V=`38*7=HX~u63HKL6BC$xi*tD^eY}jfZAJRcPVk9AW_hjl#8c6nB(c}x)CTO*q@f5lCD(sX74t-L2opH$}e(U5{JtNoXwgwA=ftQOstwB>o8+V>3#%6v*&6$y_TQf< zyzG3WXQuOwd?&DLrGajSaq4I5+i<(0;s)NVNj?8A$LIHLeDZtc| zb_&1@7nyU?c)Z{DeLvYuuqWxVo@p;h7rz`A|FoZ^%i36xUu=B;}5*$ z(xXcZ_$aJCraMsrKQ1{NNA4+^mom+7mV12lZdGHaU&rUBk8R`kRQYEUk2g1}_DEyY zbt0|}$4r-XqJh6L0neC$I z4ZPjL2}@&zU6X$IJDKM4$3C<3Kz28{9}kFiRYr_X?}@)&lYdaWryBNscX#JW=8(D_ z1<-B)T?*A21z5Wjo<4a}j=1W;CY5?YQ9MepErvDgvv*2n>+K1rk-OR+zl0{MV5wEgyAx|Unee~`pWb^7%1!3HTUO2D{FJGH{u{N@y zpPul5+eRGwTgS5m@r9!R<~$Kiz_g-|DjeQOI@)QR^TVsK*0nxC&-cS618QgUME>PP zhhSYG6pTcc#V2|6wlCD?ntU197%8m1Ep#0C%`uf*$z=+Wp3FYQ__P6}n>Jt;mHHqF z2AKszkbw!Si!jVwZY3r$kOykkE3xtY$nwVC<>l)h*B;i1iCnxKf3^#za_DXD9X=*S z_?=x6XX87h8+EHviD!vxVv&`+jF7x+*VXNfa@z!;?wRMCIW=-_$xuGh@5Oihpktp7 z>p5S0FGI!c91t!3Rcrhu2?^#!FyKiUQwMEaXuT1bW!e|sN+;Y)ayeC2p7pfWFvu_P zLdpfx`NEKgej;NEw;8>I7SChB6}sY<6>szf%KCYEX}lDPM#Q+dKZ@t|33zD3y{&<2 zPcc8si!CGfHp=2EkLVcG_XR4{^h-Kdr9X?lwoqPRXyGELqVCMOMkkWT_;0|f-^!R2 zDM(Dt)6VzGu)TT$|6Q`TD%(OlgYFYVQjo#18)>v&D*|BPJz5@c_Bl10-25x^wOk|f zTNT5ty-l(=tS;tN9E-|6>PsC$4Q7Z5k>JYW;B^BylP{k?43wTIFFagL9be$WAM&ot z)qDMG;OyzLJtJcKmDl$M7frtO2-eor(x{w<1rcmzQn{_z5$vRq%Cr?f?|La-rxQ1EoI4YS%O z&{B)F2g-`cm{)90JEFKiDj%n{#Oehy`HV(q&<>@wj&3L}juK@vNuDZ-qfE9V?MTD~ z!iW9(+57E0q=cfpYd@)Dxl7BY6?e$s;wA-*J?hH`K}c;A5zG(> zp+U=0&9`gL6HWpLlWh4W9bZ+UaHnXEen(lZ_>{J42k_?Fjc*YinYGn5#a9{%pHJ{O ztPEd3#)jbny}1*MRn%{y>4tL*Em+5Go`lt7W3V3|U4J$u4A<^TWy literal 0 HcmV?d00001 diff --git a/lib/ShellyPlugSPlugin/src/ShellyPlugSPlugin.cpp b/lib/ShellyPlugSPlugin/src/ShellyPlugSPlugin.cpp deleted file mode 100644 index 1a7859ca..00000000 --- a/lib/ShellyPlugSPlugin/src/ShellyPlugSPlugin.cpp +++ /dev/null @@ -1,508 +0,0 @@ -/* MIT License - * - * Copyright (c) 2019 - 2023 Andreas Merkle - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -/******************************************************************************* - DESCRIPTION -*******************************************************************************/ -/** - * @brief Shelly PlugS plugin. - * @author Yann Le Glaz - */ - -/****************************************************************************** - * Includes - *****************************************************************************/ -#include "ShellyPlugSPlugin.h" -#include "AsyncHttpClient.h" -#include "ClockDrv.h" -#include "time.h" - -#include -#include - -/****************************************************************************** - * Compiler Switches - *****************************************************************************/ - -/****************************************************************************** - * Macros - *****************************************************************************/ - -/****************************************************************************** - * Types and classes - *****************************************************************************/ - -/****************************************************************************** - * Prototypes - *****************************************************************************/ - -/****************************************************************************** - * Local Variables - *****************************************************************************/ - -/* Initialize image path. */ -const char* ShellyPlugSPlugin::IMAGE_PATH = "/plugins/ShellyPlugSPlugin/plug.bmp"; - -/* Initialize plugin topic. */ -const char* ShellyPlugSPlugin::TOPIC_CONFIG = "/ipAddress"; - -/****************************************************************************** - * Public Methods - *****************************************************************************/ - -void ShellyPlugSPlugin::getTopics(JsonArray& topics) const -{ - (void)topics.add(TOPIC_CONFIG); -} - -bool ShellyPlugSPlugin::getTopic(const String& topic, JsonObject& value) const -{ - bool isSuccessful = false; - - if (0U != topic.equals(TOPIC_CONFIG)) - { - getConfiguration(value); - isSuccessful = true; - } - - return isSuccessful; -} - -bool ShellyPlugSPlugin::setTopic(const String& topic, const JsonObject& value) -{ - bool isSuccessful = false; - - if (0U != topic.equals(TOPIC_CONFIG)) - { - const size_t JSON_DOC_SIZE = 512U; - DynamicJsonDocument jsonDoc(JSON_DOC_SIZE); - JsonObject jsonCfg = jsonDoc.to(); - JsonVariantConst jsonIpAddress = value["ipAddress"]; - - /* The received configuration may not contain all single key/value pair. - * Therefore read first the complete internal configuration and - * overwrite them with the received ones. - */ - getConfiguration(jsonCfg); - - /* Note: - * Check only for the key/value pair availability. - * The type check will follow in the setConfiguration(). - */ - - if (false == jsonIpAddress.isNull()) - { - jsonCfg["ipAddress"] = jsonIpAddress.as(); - isSuccessful = true; - } - - if (true == isSuccessful) - { - JsonObjectConst jsonCfgConst = jsonCfg; - - isSuccessful = setConfiguration(jsonCfgConst); - - if (true == isSuccessful) - { - requestStoreToPersistentMemory(); - } - } - } - - return isSuccessful; -} - -bool ShellyPlugSPlugin::hasTopicChanged(const String& topic) -{ - MutexGuard guard(m_mutex); - bool hasTopicChanged = m_hasTopicChanged; - - /* Only a single topic, therefore its not necessary to check. */ - PLUGIN_NOT_USED(topic); - - m_hasTopicChanged = false; - - return hasTopicChanged; -} - -void ShellyPlugSPlugin::start(uint16_t width, uint16_t height) -{ - MutexGuard guard(m_mutex); - - m_iconCanvas.setPosAndSize(0, 0, ICON_WIDTH, ICON_HEIGHT); - (void)m_iconCanvas.addWidget(m_bitmapWidget); - - (void)m_bitmapWidget.load(FILESYSTEM, IMAGE_PATH); - - /* The text canvas is left aligned to the icon canvas and it spans over - * the whole display height. - */ - m_textCanvas.setPosAndSize(ICON_WIDTH, 0, width - ICON_WIDTH, height); - (void)m_textCanvas.addWidget(m_textWidget); - - /* Choose font. */ - m_textWidget.setFont(Fonts::getFontByType(m_fontType)); - - /* The text widget inside the text canvas is left aligned on x-axis and - * aligned to the center of y-axis. - */ - if (height > m_textWidget.getFont().getHeight()) - { - uint16_t diffY = height - m_textWidget.getFont().getHeight(); - uint16_t offsY = diffY / 2U; - - m_textWidget.move(0, offsY); - } - - /* Try to load configuration. If there is no configuration available, a default configuration - * will be created. - */ - if (false == loadConfiguration()) - { - if (false == saveConfiguration()) - { - LOG_WARNING("Failed to create initial configuration file %s.", getFullPathToConfiguration().c_str()); - } - } - else - { - /* Remember current timestamp to detect updates of the configuration in the - * filesystem without using the plugin API. - */ - updateTimestampLastUpdate(); - } - - m_cfgReloadTimer.start(CFG_RELOAD_PERIOD); - - initHttpClient(); -} - -void ShellyPlugSPlugin::stop() -{ - String configurationFilename = getFullPathToConfiguration(); - MutexGuard guard(m_mutex); - - m_cfgReloadTimer.stop(); - m_requestTimer.stop(); - - if (false != FILESYSTEM.remove(configurationFilename)) - { - LOG_INFO("File %s removed", configurationFilename.c_str()); - } -} - -void ShellyPlugSPlugin::process(bool isConnected) -{ - Msg msg; - MutexGuard guard(m_mutex); - - /* Configuration in persistent memory updated? */ - if ((true == m_cfgReloadTimer.isTimerRunning()) && - (true == m_cfgReloadTimer.isTimeout())) - { - if (true == isConfigurationUpdated()) - { - m_reloadConfigReq = true; - } - - m_cfgReloadTimer.restart(); - } - - if (true == m_storeConfigReq) - { - if (false == saveConfiguration()) - { - LOG_WARNING("Failed to save configuration: %s", getFullPathToConfiguration().c_str()); - } - - m_storeConfigReq = false; - } - else if (true == m_reloadConfigReq) - { - LOG_INFO("Reload configuration: %s", getFullPathToConfiguration().c_str()); - - if (true == loadConfiguration()) - { - updateTimestampLastUpdate(); - } - - m_reloadConfigReq = false; - } - else - { - ; - } - - /* Only if a network connection is established the required information - * shall be periodically requested via REST API. - */ - if (false == m_requestTimer.isTimerRunning()) - { - if (true == isConnected) - { - if (false == startHttpRequest()) - { - m_requestTimer.start(UPDATE_PERIOD_SHORT); - } - else - { - m_requestTimer.start(UPDATE_PERIOD); - } - } - } - else - { - /* If the connection is lost, stop periodically requesting information - * via REST API. - */ - if (false == isConnected) - { - m_requestTimer.stop(); - } - /* Network connection is available and next request may be necessary for - * information update. - */ - else if (true == m_requestTimer.isTimeout()) - { - if (false == startHttpRequest()) - { - m_requestTimer.start(UPDATE_PERIOD_SHORT); - } - else - { - m_requestTimer.start(UPDATE_PERIOD); - } - } - } - - if (true == m_taskProxy.receive(msg)) - { - switch(msg.type) - { - case MSG_TYPE_INVALID: - /* Should never happen. */ - break; - - case MSG_TYPE_RSP: - if (nullptr != msg.rsp) - { - handleWebResponse(*msg.rsp); - delete msg.rsp; - msg.rsp = nullptr; - } - break; - - default: - /* Should never happen. */ - break; - } - } -} - -void ShellyPlugSPlugin::update(YAGfx& gfx) -{ - MutexGuard guard(m_mutex); - - gfx.fillScreen(ColorDef::BLACK); - m_iconCanvas.update(gfx); - m_textCanvas.update(gfx); -} - -/****************************************************************************** - * Protected Methods - *****************************************************************************/ - -/****************************************************************************** - * Private Methods - *****************************************************************************/ - -void ShellyPlugSPlugin::requestStoreToPersistentMemory() -{ - MutexGuard guard(m_mutex); - - m_storeConfigReq = true; -} - -void ShellyPlugSPlugin::getConfiguration(JsonObject& jsonCfg) const -{ - MutexGuard guard(m_mutex); - - jsonCfg["ipAddress"] = m_ipAddress; -} - -bool ShellyPlugSPlugin::setConfiguration(JsonObjectConst& jsonCfg) -{ - bool status = false; - JsonVariantConst jsonIpAddress = jsonCfg["ipAddress"]; - - if (false == jsonIpAddress.is()) - { - LOG_WARNING("ipAddress not found or invalid type."); - } - else - { - MutexGuard guard(m_mutex); - - m_ipAddress = jsonIpAddress.as(); - - /* Force update on display */ - m_requestTimer.start(UPDATE_PERIOD_SHORT); - - m_hasTopicChanged = true; - - status = true; - } - - return status; -} - -bool ShellyPlugSPlugin::startHttpRequest() -{ - bool status = false; - - if (false == m_ipAddress.isEmpty()) - { - String url = String("http://") + m_ipAddress + "/meter/0/"; - - if (true == m_client.begin(url)) - { - if (false == m_client.GET()) - { - LOG_WARNING("GET %s failed.", url.c_str()); - } - else - { - status = true; - } - } - } - - return status; -} - -void ShellyPlugSPlugin::initHttpClient() -{ - /* Note: All registered callbacks are running in a different task context! - * Therefore it is not allowed to access a member here directly. - * The processing must be deferred via task proxy. - */ - m_client.regOnResponse( - [this](const HttpResponse& rsp) - { - const size_t JSON_DOC_SIZE = 512U; - DynamicJsonDocument* jsonDoc = new(std::nothrow) DynamicJsonDocument(JSON_DOC_SIZE); - - if (nullptr != jsonDoc) - { - size_t payloadSize = 0U; - const char* payload = reinterpret_cast(rsp.getPayload(payloadSize)); - const size_t FILTER_SIZE = 128U; - StaticJsonDocument filter; - DeserializationError error; - - filter["power"] = true; - - if (true == filter.overflowed()) - { - LOG_ERROR("Less memory for filter available."); - } - - error = deserializeJson(*jsonDoc, payload, payloadSize, DeserializationOption::Filter(filter)); - - if (DeserializationError::Ok != error.code()) - { - LOG_WARNING("JSON parse error: %s", error.c_str()); - } - else - { - Msg msg; - - msg.type = MSG_TYPE_RSP; - msg.rsp = jsonDoc; - - if (false == this->m_taskProxy.send(msg)) - { - delete jsonDoc; - jsonDoc = nullptr; - } - } - } - } - ); -} - -void ShellyPlugSPlugin::handleWebResponse(DynamicJsonDocument& jsonDoc) -{ - JsonVariantConst jsonPower = jsonDoc["power"]; - - if (false == jsonPower.is()) - { - LOG_WARNING("JSON power type mismatch or missing."); - } - else - { - float powerRaw = jsonPower.as(); - String power; - const char* reducePrecision; - char powerReducedPrecison[6] = { 0 }; - - if (powerRaw < 99.99F) - { - reducePrecision = (powerRaw > 9.9F) ? "%.1f" : "%.2f"; - } - else - { - reducePrecision = "%.0f"; - } - - (void)snprintf(powerReducedPrecison, sizeof(powerReducedPrecison), reducePrecision, powerRaw); - - power = "\\calign"; - power += powerReducedPrecison; - power += " W"; - - m_textWidget.setFormatStr(power); - } -} - -void ShellyPlugSPlugin::clearQueue() -{ - Msg msg; - - while(true == m_taskProxy.receive(msg)) - { - if (MSG_TYPE_RSP == msg.type) - { - delete msg.rsp; - msg.rsp = nullptr; - } - } -} - -/****************************************************************************** - * External Functions - *****************************************************************************/ - -/****************************************************************************** - * Local Functions - *****************************************************************************/ diff --git a/lib/ShellyPlugSPlugin/web/ShellyPlugSPlugin.jpg b/lib/ShellyPlugSPlugin/web/ShellyPlugSPlugin.jpg deleted file mode 100644 index a8f8433c20631b8d67508dca2918d3b66dfa0359..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9258 zcmb_?c|4Ts-~VmP79vY1QS%+bPB%w5VFCORm}BuOde$X3W? zPeNHUgE4~$lNrmnneBH^=X}4v<@x>bJkRU7XRg<4mit;h*Jpcw-q+3l#vcbZ9dUGa z1cZcyfHUw5;FAG+Kv+mf@c!o#5fc$y8$?A##Kbpth{}s8?AUW~!)A}O;+m0) zdv9eFNN61@Z&W(=m7#5V?oynjl(NbeRW+TRx_bHsX66=_R{N~&e{*njI_!Mp_=%IA zUZ=c$0?r2p1z!jWy?o_rR5a$=_1kyi6B3h>Q!?-WnU$UM;9>65!e>R#i(kAfc~enY zRbBJ;U2RiyOKV&E$Bs|Eef)5mWu>z6cK`zCn66Z0UkK>GzHLu8yDgOaeCM` zJ|KZL-~)|&v8r=CJ;;keY00JY0ah{T>Nc-t3$L7Lb#vd3eKBtwyU89W9i5wAh}<-s z0-=40tm_Q94`}EZo^cz4Xyk3=4e&v)1#w;9bKr2ihN(5n!8@e zlPI8qcS$(TW?kx|F%@Qeb1dQzNO!C7o^23+-(N;;dxb&7FlPs6)?!=EDK_0JjugXv z;scD!vUexA2%cgwN7rsA15B##p;OAivzVZ12euuuqQ1wpn^?DG8E8RYWYI7F9I?2G z_4?|dD8zQ=0~@|$JCPf)mb@0M{y=>QWBLPj_ka;6GCD-*d(9J{P!;(@)oM4AvYiWq zBveP^Zr}7v-|LECMB?Ww813VU^G?)o-$qb2Mtq!mY@%H`J9F$puGn*YaaHQnozw+)WH3e!wDnhuT%bj2ddF((lhvzq2>wySy$B z^(=XD;)z6nwo*WSf>beQYcO>ondAn~6=EAg;{!s_Z5Q-bpIx5ox_6A1Z`{e7SGy(K z+xgPZaJ&2XlT-!oqYTAY0M-bW5%Bm|L!Aa2Rl)IW6l2wmblV?7^8v{rjv-~E!PMfZX|d5frG@w^ zqyirZ2AjlK=o)T%uP2v8h?S9X~X?CIeHv4(LF z65fa#VABv3&-6OT2NIHa@E`Gkv^mO_xt6;#t7MG~R(f@|v+!9JCU~tV zDlzmXR^J2l9L4Y^K$gdO+f>*lGe`v_H4j&e>-BD!S?0@o9y|(KsWe(A0JpK0dTRMHsGdtQYI*;|yVw!#<>Y~-+(%YXO zkmD67K}Ty77U~Q3pN{d|8d@IsPsjU{Yt=b9f{q=l0rPoL7gd)`--b5=oUUJUrz{Wy zQE>=HAF4{c8BE{Xo#UTbyi6t6r)v?f8Ci$@1b@UG>Bt-3P=) z9Iz{3%O*asav8st4zhosQot&&MmZ=5MY>IMYKFrEod|g~==XEXxrHorMJ#bX`V#rt znCJB3>e`KJba-9)+X-!vTYs+t-UY!<{N0476Ju;%V2CZs)<^FLTq_B!EozC%j*7Zh zxvFycaHCd=Rljm{(2hedv@-!!f+rtH#PGf#mhG{7K&t9XkeQ1d&qpqTd_V-8e=>tu zzf?n*EqVVt(Ij2d_*hi=V*SAurMPnjHyGV zAv8I;kuc@l`1gcnlsu}avia2eQTgn#PlxXo*!bBiq`yuTG1OZl@?_;}2lzlZdQ*&A zFIE*&r`05__$gET>uM{|iR)8oO-)@z#kmVcpGjnZvfUS1hp4`IuI9ZG|I zVEze2<^#t{eQ@ev2Or>=@_{$8v<2=y%(*&bIrU@F=am}THM!JRH$slH!j47|o`RF0 zIaZTYSpGs71dqe?RC5Px{8;fMbg;+G(3kIJlpx*aV2)PkF}& zWSQJ2$e9GrIoPL7kMV&CbFMbnXiuE{V7Ct(wj1ID%j8J^)|1@3vcVd`+|!>+J=l%5Ev&~bs8-U+|9SwIm@iOTlu5vmBXH>mJToY6D@DEuoa8Ps?pH<s^Kx(wnIbyR>r;UIw$@Lb_c_d<3-4t>02>@ zuz^iO+`{1*q#Ctu0J{~F(yeOJAd6{e8qjdvOI7TQw0m%`d5gAkrq_vs*+C~oaUT(o zsR{4ct%)ACPHiIZab*g37T*05@RJD~isjfk4LkWjpX&gC(dGR1gDuMkzDDqYP8$>- zm{coZi-L_Ou(BKvCWS6Hfsk}%nzs2XF@sxf+~_hn$yjLWQJhn*c;>Ijpg+5nPd<9j zL9;7-?}JGaB*l)hHU)K7xO>6Ihv4E}KA-^(AL~ah_dRnLe+Z+FVM7sjjul)M5cg@sOYvX^@(i|&R#FftX@y6-)xJyD(rCRPJ(-D#!X%Vay;vofWt&y2RZ>^8BumW*NxW_Kxi;#m3qK!g_0vc1 zcrvuQ%ry$xDW)Z&Ht zi^0U&E4bq|QS!bYaAv)mBu>#BqpePQo)cmoStUR^C|Fx8G#oICsCcf8&lmBPMm`|7 z9lF5>4lT43ID5v-`M}gkP!qv8hF9=~I(brinCw#(IQN9UlMs$(_%WcNJiqCOg6mOg zzYE%0`^!fmp~5VsFH_zpZ;7x&2(%oY)Jr~~e}MZ8IsX!q#|MrMoe&zYgm+i4!~bVH z%~(jPl9Ry1wR`rW31;uTT;8Vt>|X2*ilg;j_-1>lUGv_@*$=w|x5o*Io6k}h3^><{ z@b2ccnQ(TknDF-d+c=yE*5mBzE>gQZ80j`}ntK-B67Y0bgEjYljCpCOhAmXeEqrx4 z)G`_!Ho=Yfs{3$7Jv;OGR?8!Kqn$Q?ovy1eRru^Yca^(=gYC`V>;}0u7@F23Tz#X9 zPV$*M?I<>wk>%`4d5|&iI4AuFBYAJ!X8!VI}X=|!g$B(`VxwR5y`bGUNEEkwo?l*k9}hjoAY8*&e@aR_??KvD{nG_r*piQ}In4X2d$ZkEei0rY!4WWs8rHz^C49#6kpb~gr5)J{WB|jUKJ&(7rm=dezT3oiEAbtMRx0$0c6ey zXtvX2EVLj~26G2+$%V=_moZD;bvF*Nyk5{%9$%ta>UGEWZy44pi>ebDi7^_&YU-xj zX{&?M7-MIA%O81J_ggLZygmk=5BXpqU;KFPFRh3jN52P>4ke&fU^^Ws;01otWuMY*c)&6qlm+(o9P4=}n|Nal8)^ciplb4@bT< zv)D^Zt%DZX6j>93f}1S#>I7r->(&0{UlA&4Cx$Wq`^70qsa6-wUqD<;pL>%dJx+mi zk#NxL@rQ=Y$Fdj`EnfH9W0|`M-)k$D2^Ts5vrC0qsjm$6xAXMjWcJ*8 z2xo>A%(@f0MkPu>g+Ay+l@4=4QoVEJ6H+^xTU?Z#5ggaHl6!!*TWgQrjt2{3hid4- z9L3^xurhF(vl&0JY(3pXjLXH|HEG7l?<}|)z&w7uzP7r?I`Bj0;U}QnZ*M-neET|l z*V!bXR^%<<&IgiU{B@SHu8Ca3%)xl?@d@M^9HbN7NQob_;TCYVQT4DpF_ujYn;{%J zxaf!fHmI2PSYuE$rzwA1-Mu%yp?i|ghm-{cXy@U@jz_gPpb|iuC2qYM$@xg+&JjwG z<7ptn9hZt{xXX!hE<(+WrSa)z8$XpcynJ(40L>)1X3)|nwzVeF%y4H~OqvumFfEg` zPxMZ~=hu0=Z;gmFg5&cG!zN8cA>#NWp7fyEelJ?!%4ko^4RuRg3re12Pjhikdmfs8 zggk3m(UcjeD5)fJ)7xGwN_=gaFcZL&?Phwlxn73T`S@Gx4oHrQk6(tv?K#FMCb~U` zw9=o07|wliiX^#wL+8{dRmpotT9iEgO4vp0*$;qz6oxDMo7{(ny|CF{=D-=jd?D?R z(OIIbt<5#FDC8Cn>TD=(=fjA3&6?ZC51&m{IMl9Su)V3IUDf_@3>LR5ZyAnsasgf*yBir!7Jw- z=pN#*7f((SM!A999G*JI68;p;gAsasEH&J;8AU=e^0|31;}TGrLhfE9R^U4+Q$L5_ zSO09ANWI@oWMJG_;`D)orH!qr_ldt@x-kkdwuI_(Q-td7Szd04S8sk2SzD6XG zqb{o3^*rt(N(il*(EVVjAuL#o+B3=R`Lz%@ppM;R>$&KptXY)^bZG33w-y zATqVRkuJx=_Hp)?Vrpo}+Z4Bk2I%IZPiZS5X?gD3L{8AdcL5pnL5mFg2QBeUhrI%@ zAHb#-?hE7$jw1%c_r_P?lIa1&$3)lxnxp4=c^uC~=bI!~oEFz@u+WqpH082w$i+91rKi2z+@|@q+<4e;f#(c=9w;dht*m}I) z!>s((PiZX&)iXkRcjy}W2Vh8k0P&?A2IFiU$ZMhp_Pn;-ws$3*-^=T&AzW-w1{4j0 zr5MXlk1Jh9jbX9YED8VL>qNA6ToQhH?!^-Xj1%W`U?4<+agf-Yn#QYta8O!aJIy4C z_csnQchU)8Q6GkJP>N{Etq;2j;_8IK#^-hhP)7kAFZOXl zv&cSJn?Audb&*kP+=Y!Mf$(c!r_;m~=7aqyr}H%M3-=Ti%*OtsiAmVzr|`cbHwpYNyyJv+np76d!CW7I=62-BBzQD z!j2?E{snpju=@VrpodD{_pH3(Z_qQkr8QFI84P-K&L>)B<@@FiIEw%$(*Z z6i&3=XDrb)N~zm|eFm>c#5imm+gAJbZI$n}2;=Yp@8URR1@WqT_Y_l=){?b1%0WA; zx?cwW(yoX_VsP=ST0a~utk+{pS0;u@BO@|LN7}bmJUdqDQ9+l{Y~A7REHN3r9WexQ zY(VM)cOy!J$bg_BK9F6-NfO{;OEetX1$bDa+x1YGt0Ao7*VN4Q^&9Ns!z(s-jvKZ7 zTyot3z=c!rsjq2$y`|xCu`~#+MH~%;o(CmtvOIMbmJRFtJRU%EuSa z9pgALH_s|pTjH*Th5vEY=C*d^QRZ*GZG^YG(i8)4iay<331Er@0QLXV5XA_Fn;R}) zjNvs8Qe{lNnZl5+o*FjA zT!qszO^diE*I_9vfykuyY-Gphs3v;$TeM*E7+({oTRSc*(&gM_U8F; zJ8xqqlpYKhEBfY47KHNIM` zcxpLR99IzyXYg@eA0J5fl4CpZfsT6260da>uEL$4mGu$}LKqFEa|2o{NgS90%0a&_ z0sDHi)>viI#xN*)ryFq{AA#(2<2`-^Hc?1U)G_=7E_w55%xdEw_OUdp;OcAUb%DM~ zZ5hRmev;rr`@%9u$H}w(YD^F^SZD*qISw#X@kPi%70o^?lm1$9(KUTlR>nn#TdR6= z3{nrfdplkaOe~FQg>6j&hItLlXf8}}{7`z2X$8$Aqqossx~MlV7AqH8-@7zm8Pe(N zNA6wd=lwZOl=#D-K|J}af6U0A&!6;$L6OduMcj+;g;Vqh?QS#M|Dm8nR-^@v_rzA5 zZ3TP2qiskToH%Ok#E3r~jj?JdPjqK0m!peFU4OvE|D05Z&C|+wt&F(aneyqoWs$#1 zf5tJwa8El3#~U0;Baz7-e_l}`=iNI_u%o%)gw}k`c`5G88^i^k|-lG^N-PRoV0DXtx*A~ipNbM6jS~jr&2bs0A z1|Z8{D}`fJ0mmMyV)T5tJ}}gBV{QhnWaTkW+g)mNM=QIGlmpYRdF1Y8ek{1zsMowt z98r@sPK2>P=aC5=u1o`uwDSRXg0#E%)BUpNPK>Q1k)dt z(ZA@F>E<4Fj8l8A5uy36x@wi2?40s?(Q8ZTm$T_#D(~5pd{j96?d3k9{t3}w8V=f7 zld?>N)1{*wFqiwisCb5CoW?{n*N_qlV*kiyRa-DaqZYJ#KTht7d}?TFx=B&k8SoMS zU{$!*vfRf9E)jU&!xm7`zCND&Rc0M;9JLPiT@Zm8I_fMQMo!$Vi62lLD>axu(Dqt{ zy+BN#>7O$}{FNXs+xJ&;gB=F3T2Knxt$$a90&Q@8%Uw>0m3ulcs`|^k^qL8KrTe8X2Z~H;$L>{EO37KDGq$hN+WYVQmhtpwzMA~ zO1*?thV>r9@n;q%T%iZJGH|3TqdtR%ozBFnUH7|Eev?hBdcJY*v#pyXdbWGL#&#kU z$vnvd8d?#?ql&gR1m949y{hv1ibU5im(LFBqdS7nT6qY3AF2Ow>v@qc4TfN&ft?0M zrHNHDnA?*&nH?M8()Z0Z`r;?8+I}PYGcDzb?~;Z}`$dHAIUf9hq{3$;8~MP@z6kjnSIEVZfU16@pXWtX#J}rr7TYE*pSj^M zJOmRV7(pB!L0B+CF2I2atj_T{R3f;=){DwkHP)&4Qg!#eo%rM1N0_ZPZAD3d@(#zt zK=5D1G5SzD%`MJOGtZOJP{hr7aoX4XStd7Mzvj2kWnKz~a`h>fHeluk-~0M-@AI^= z{rUPs4LdN%rW}=gRc2H<_#$Mn=Y@Bpep*=(HRHM1sZ9sB?X}WA^C%Ao>GrhFp*DNZ zEMlA@iekg|Pr~^o)H{ZGi1DnnuA5|0$=s+QN%>%9HPb|UMJ;J-P>a}{Ug8mM#(rLf z(%N`bIbHM57w5JYOJ3;D3?Y~;2O<@)BYZ%~){jc&>bf(XNM_t)Bo~(l#ES6ET`3NF zb6H99S=#3}IRyxgwR+ZbxQfJW;J6)*4}3IVtDxZpOIi?vf;|S!O(eK^uD#)b^>umd zV;!6qg@=BUJ;fP_a-BEWV#^qyB6DRn=$eX#BgvFhRa&1IF*A%U( zqAO2N{rjf&tuetOp|U{tQD^3-WjtwNLir(cm6gbpyL$y=Qd&9cJ3sb&bW*bQTGZFy z$Z-`WW2qSlw(@;N*Q#PxuCExL$O`Sf^k-gpH935ijP@iC)Vy2pC_I{G^|erGORILK zr?ZiQ^My+A$)&~|ouD5#GrmqMh1z!qdB$DBqto^u#@ z4g0y`*BxxXqR*}yzoOH-{45>k>oT$uE=gFKGhG#hRtW;GI}-ex>OLT#x<4>L&Q=$8 z07ksU&S2mQJ{57k-!{BtXH}^Sd`|v6S(hBq`)RN-u-W_GgX%ZcxKJ_FXhhrhAau%w zCvwlhixg;=6MY*TNyH)4*>U1siaba)W(+1PFt;Y{#E>u+~cWdRJ zB%OLm$=oZ1F#U)8L&q3E__7!UsDMgT!<=KYO#-JiDawCM1J9E$5;*_sp@cWf?ar??ld2@ ziVt3*PgduwdP!=#*6Ke)J(RUPf99r>hyx(VC-T0Iokm8;z`T%0bSDqKJcv^%?FCSt}e#@I$0N5 zC&IN03|sH(GEiJ1_#?I}8oM3-`jCO+-c_6CWrZj~oelszW Date: Sun, 25 Jun 2023 21:53:19 +0200 Subject: [PATCH 010/105] Example configurations for GrabViaMqttPlugin and GrabViaRestPlugin. --- .../mqtt/grodansparadisOutdoorTemperature.json | 12 ++++++++++++ doc/grabConfigs/rest/githubStargazers.json | 11 +++++++++++ doc/grabConfigs/rest/shellyPlugSPower.json | 11 +++++++++++ 3 files changed, 34 insertions(+) create mode 100644 doc/grabConfigs/mqtt/grodansparadisOutdoorTemperature.json create mode 100644 doc/grabConfigs/rest/githubStargazers.json create mode 100644 doc/grabConfigs/rest/shellyPlugSPower.json diff --git a/doc/grabConfigs/mqtt/grodansparadisOutdoorTemperature.json b/doc/grabConfigs/mqtt/grodansparadisOutdoorTemperature.json new file mode 100644 index 00000000..a06ff2d3 --- /dev/null +++ b/doc/grabConfigs/mqtt/grodansparadisOutdoorTemperature.json @@ -0,0 +1,12 @@ +{ + "path": "vscp/FF:FF:FF:FF:FF:FF:FF:FF:61:00:08:01:92:AF:A8:10/10/6/16/0", + "filter": { + "measurement": { + "value": true + } + }, + "iconPath": "/configuration/bumblebee.bmp", + "format": "\\calign%0.1f\\x8EC", + "multiplier": 1, + "offset": 0 +} \ No newline at end of file diff --git a/doc/grabConfigs/rest/githubStargazers.json b/doc/grabConfigs/rest/githubStargazers.json new file mode 100644 index 00000000..85a5beae --- /dev/null +++ b/doc/grabConfigs/rest/githubStargazers.json @@ -0,0 +1,11 @@ +{ + "method": "GET", + "url": "https://api.github.com/repos/BlueAndi/esp-rgb-led-matrix", + "filter": { + "stargazers_count": true + }, + "iconPath": "/configuration/github.bmp", + "format": "\\calign%s", + "multiplier": 1, + "offset": 0 + } \ No newline at end of file diff --git a/doc/grabConfigs/rest/shellyPlugSPower.json b/doc/grabConfigs/rest/shellyPlugSPower.json new file mode 100644 index 00000000..af3eae9e --- /dev/null +++ b/doc/grabConfigs/rest/shellyPlugSPower.json @@ -0,0 +1,11 @@ +{ + "method": "GET", + "url": "http://192.168.1.123/meter/0/", + "filter": { + "power": true + }, + "iconPath": "/configuration/plug.bmp", + "format": "\\calign%0.1f W", + "multiplier": 1, + "offset": 0 + } \ No newline at end of file From 93f5758aee8aa67ff6edc380c46a6d1dba055904 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 25 Jun 2023 22:03:04 +0200 Subject: [PATCH 011/105] Unnecessary header include removed. --- lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp b/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp index 46416d32..4776f7e2 100644 --- a/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp +++ b/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp @@ -36,7 +36,6 @@ #include #include -#include /****************************************************************************** * Compiler Switches From ae819df9ef7263de345db6319a1f717a939d1966 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 25 Jun 2023 22:11:00 +0200 Subject: [PATCH 012/105] Floating point literal has suffix 'f', which is not uppercase. --- lib/Sensors/src/SensorLdr.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/Sensors/src/SensorLdr.cpp b/lib/Sensors/src/SensorLdr.cpp index 49b284a8..8a4c441c 100644 --- a/lib/Sensors/src/SensorLdr.cpp +++ b/lib/Sensors/src/SensorLdr.cpp @@ -110,12 +110,12 @@ typedef struct */ static const Ldr LDR_CONSTANTS[] = { - { "GL5516", 562500000.0f, -2.0f }, - { "GL5528", 91233029.9336f, -1.6667f }, - { "GL5537-1", 213746993.3346f, -1.6667f }, - { "GL5537-2", 37529382.2835f, -1.4286f }, - { "GL5539", 12411565.9487f, -1.25f }, - { "GL5549", 5639135.2390f, -1.1111f } + { "GL5516", 562500000.0F, -2.0F }, + { "GL5528", 91233029.9336F, -1.6667F }, + { "GL5537-1", 213746993.3346F, -1.6667F }, + { "GL5537-2", 37529382.2835F, -1.4286F }, + { "GL5539", 12411565.9487F, -1.25F }, + { "GL5549", 5639135.2390F, -1.1111F } }; /****************************************************************************** From db867a3e2de43080bdbaf756ab75a76f2cc1ff01 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 25 Jun 2023 22:11:24 +0200 Subject: [PATCH 013/105] Redundant return statement at the end of a function with a void return type. --- src/Hal/ButtonDrv.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Hal/ButtonDrv.cpp b/src/Hal/ButtonDrv.cpp index 8e608d02..d79402c9 100644 --- a/src/Hal/ButtonDrv.cpp +++ b/src/Hal/ButtonDrv.cpp @@ -281,8 +281,6 @@ void ButtonDrv::setState(ButtonId buttonId, ButtonState state) (void)xSemaphoreGive(m_xSemaphore); } } - - return; } void ButtonDrv::buttonTask(void *parameters) From 50945c0fb643a84f76d167df728c37ad1aa234e8 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Wed, 5 Jul 2023 21:47:40 +0200 Subject: [PATCH 014/105] Ulanzi RTC support implemented. #135 --- config/board.ini | 9 + config/configSmallUlanzi.ini | 2 +- config/display.ini | 4 +- lib/CountdownPlugin/library.json | 2 + lib/CountdownPlugin/src/CountdownPlugin.cpp | 2 +- lib/DateTimePlugin/library.json | 2 + lib/DateTimePlugin/src/DateTimePlugin.cpp | 6 +- lib/IRtc/IRtc.h | 111 ++++++++++ lib/IRtc/library.json | 15 ++ lib/Rtc1307/library.json | 25 +++ lib/Rtc1307/src/Rtc1307Drv.cpp | 136 +++++++++++++ lib/Rtc1307/src/Rtc1307Drv.h | 124 ++++++++++++ lib/SunrisePlugin/library.json | 2 + src/Hal/ClockDrv.cpp | 212 ++++++++++++++------ src/Hal/ClockDrv.h | 83 ++++++-- src/Hal/RtcNoneDrv.cpp | 75 +++++++ src/Hal/RtcNoneDrv.h | 126 ++++++++++++ src/StateMachine/ConnectedState.cpp | 4 - src/StateMachine/InitState.cpp | 4 + src/StateMachine/InitState.h | 13 ++ 20 files changed, 877 insertions(+), 80 deletions(-) create mode 100644 lib/IRtc/IRtc.h create mode 100644 lib/IRtc/library.json create mode 100644 lib/Rtc1307/library.json create mode 100644 lib/Rtc1307/src/Rtc1307Drv.cpp create mode 100644 lib/Rtc1307/src/Rtc1307Drv.h create mode 100644 src/Hal/RtcNoneDrv.cpp create mode 100644 src/Hal/RtcNoneDrv.h diff --git a/config/board.ini b/config/board.ini index c07f47c8..c0686a52 100644 --- a/config/board.ini +++ b/config/board.ini @@ -30,6 +30,7 @@ build_flags = -D CONFIG_BUTTON_CTRL=1 -D CONFIG_LED_TOPO=ColumnMajorAlternatingLayout -D CONFIG_SUPPLY_CURRENT=3500U + -D CONFIG_RTC=0 lib_deps = ${mcu:esp32.lib_deps_builtin} ${mcu:esp32.lib_deps_external} @@ -77,6 +78,7 @@ build_flags = -D CONFIG_BUTTON_CTRL=1 -D CONFIG_LED_TOPO=ColumnMajorAlternatingLayout -D CONFIG_SUPPLY_CURRENT=3500U + -D CONFIG_RTC=0 lib_deps = ${mcu:esp32.lib_deps_builtin} ${mcu:esp32.lib_deps_external} @@ -124,6 +126,7 @@ build_flags = -D CONFIG_BUTTON_CTRL=1 -D CONFIG_LED_TOPO=ColumnMajorAlternatingLayout -D CONFIG_SUPPLY_CURRENT=3500U + -D CONFIG_RTC=0 lib_deps = ${mcu:esp32.lib_deps_builtin} ${mcu:esp32.lib_deps_external} @@ -171,6 +174,7 @@ build_flags = -D CONFIG_BUTTON_CTRL=1 -D CONFIG_LED_TOPO=ColumnMajorAlternatingLayout -D CONFIG_SUPPLY_CURRENT=3500U + -D CONFIG_RTC=0 lib_deps = ${mcu:esp32.lib_deps_builtin} ${mcu:esp32.lib_deps_external} @@ -218,6 +222,7 @@ build_flags = -D CONFIG_SENSOR_LDR=SensorLdr::LDR_TYPE_GL5528 -D CONFIG_SENSOR_LDR_SERIES_RESISTANCE=1000.0F -D CONFIG_BUTTON_CTRL=1 + -D CONFIG_RTC=0 lib_deps = ${mcu:esp32.lib_deps_builtin} ${mcu:esp32.lib_deps_external} @@ -266,6 +271,7 @@ build_flags = -D CONFIG_SENSOR_LDR=SensorLdr::LDR_TYPE_GL5528 -D CONFIG_SENSOR_LDR_SERIES_RESISTANCE=1000.0F -D CONFIG_BUTTON_CTRL=2 + -D CONFIG_RTC=0 lib_deps = ${mcu:esp32.lib_deps_builtin} ${mcu:esp32.lib_deps_external} @@ -311,6 +317,7 @@ build_flags = -D CONFIG_BUTTON_CTRL=1 -D CONFIG_LED_TOPO=ColumnMajorAlternatingLayout -D CONFIG_SUPPLY_CURRENT=3500U + -D CONFIG_RTC=0 lib_deps = ${mcu:esp32.lib_deps_builtin} ${mcu:esp32.lib_deps_external} @@ -357,6 +364,7 @@ build_flags = -D CONFIG_SENSOR_LDR_SERIES_RESISTANCE=10000.0f -D CONFIG_BUTTON_CTRL=3 -D CONFIG_SUPPLY_CURRENT=800U + -D CONFIG_RTC=1 lib_deps = ${mcu:esp32.lib_deps_builtin} ${mcu:esp32.lib_deps_external} @@ -406,6 +414,7 @@ build_flags = -D CONFIG_BUTTON_CTRL=1 -D CONFIG_LED_TOPO=ColumnMajorAlternatingLayout -D CONFIG_SUPPLY_CURRENT=3500U + -D CONFIG_RTC=0 lib_deps = ${mcu:esp32.lib_deps_builtin} ${mcu:esp32.lib_deps_external} diff --git a/config/configSmallUlanzi.ini b/config/configSmallUlanzi.ini index ebe78388..3cdc251b 100644 --- a/config/configSmallUlanzi.ini +++ b/config/configSmallUlanzi.ini @@ -16,7 +16,7 @@ lib_deps = # ********** Plugins ********** BatteryPlugin @ ~0.1.0 ;BTCQuotePlugin @ ~0.1.0 - CountdownPlugin @ ~0.1.0 + ;CountdownPlugin @ ~0.1.0 DateTimePlugin @ ~0.1.0 ;DDPPlugin @ ~0.1.0 FirePlugin @ ~0.1.0 diff --git a/config/display.ini b/config/display.ini index 386c35df..5e36a715 100644 --- a/config/display.ini +++ b/config/display.ini @@ -114,7 +114,6 @@ lib_ignore_builtin = HalLedMatrix lib_ignore_external = - ; ******************************************************************************** ; M5Stack ; https://shop.m5stack.com/collections/m5-controllers/CORE @@ -150,7 +149,6 @@ lib_deps_builtin = HalTftDisplay lib_deps_external = bodmer/TFT_eSPI @ ~2.5.30 - lib_ignore_builtin = HalLedMatrix -lib_ignore_external = \ No newline at end of file +lib_ignore_external = diff --git a/lib/CountdownPlugin/library.json b/lib/CountdownPlugin/library.json index bbae6984..8c2e84f8 100644 --- a/lib/CountdownPlugin/library.json +++ b/lib/CountdownPlugin/library.json @@ -13,6 +13,8 @@ "name": "LittleFS" }, { "name": "Plugin" + }, { + "name": "IRtc" }], "frameworks": "*", "platforms": "*" diff --git a/lib/CountdownPlugin/src/CountdownPlugin.cpp b/lib/CountdownPlugin/src/CountdownPlugin.cpp index 09179b00..0347c031 100644 --- a/lib/CountdownPlugin/src/CountdownPlugin.cpp +++ b/lib/CountdownPlugin/src/CountdownPlugin.cpp @@ -369,7 +369,7 @@ void CountdownPlugin::calculateDifferenceInDays() { tm currentTime; - if (false != ClockDrv::getInstance().getTime(¤tTime)) + if (false != ClockDrv::getInstance().getTime(currentTime)) { uint32_t currentDateInDays = 0U; uint32_t targetDateInDays = 0U; diff --git a/lib/DateTimePlugin/library.json b/lib/DateTimePlugin/library.json index 65c13094..05375296 100644 --- a/lib/DateTimePlugin/library.json +++ b/lib/DateTimePlugin/library.json @@ -13,6 +13,8 @@ "name": "LittleFS" }, { "name": "Plugin" + }, { + "name": "IRtc" }], "frameworks": "*", "platforms": "*" diff --git a/lib/DateTimePlugin/src/DateTimePlugin.cpp b/lib/DateTimePlugin/src/DateTimePlugin.cpp index d2e04a0d..a7ce6e01 100644 --- a/lib/DateTimePlugin/src/DateTimePlugin.cpp +++ b/lib/DateTimePlugin/src/DateTimePlugin.cpp @@ -460,11 +460,11 @@ void DateTimePlugin::updateDateTime(bool force) /* If not other timezone is given, the local time shall be used. */ if (true == m_timeZone.isEmpty()) { - isClockAvailable = clockDrv.getTime(&timeInfo); + isClockAvailable = clockDrv.getTime(timeInfo); } else { - isClockAvailable = clockDrv.getTzTime(m_timeZone.c_str(), &timeInfo); + isClockAvailable = clockDrv.getTzTime(m_timeZone.c_str(), timeInfo); } if (true == isClockAvailable) @@ -680,7 +680,7 @@ bool DateTimePlugin::getTimeAsString(String& time, const String& format, const t { timeStructPtr = &timeStruct; - if (false == ClockDrv::getInstance().getTime(&timeStruct)) + if (false == ClockDrv::getInstance().getTime(timeStruct)) { timeStructPtr = nullptr; } diff --git a/lib/IRtc/IRtc.h b/lib/IRtc/IRtc.h new file mode 100644 index 00000000..48f2a660 --- /dev/null +++ b/lib/IRtc/IRtc.h @@ -0,0 +1,111 @@ +/* MIT License + * + * Copyright (c) 2019 - 2023 Andreas Merkle + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/******************************************************************************* + DESCRIPTION +*******************************************************************************/ +/** + * @brief RTC interface + * @author Andreas Merkle + * + * @addtogroup Hal + * + * @{ + */ + +#ifndef IRTC_H +#define IRTC_H + +/****************************************************************************** + * Compile Switches + *****************************************************************************/ + +/****************************************************************************** + * Includes + *****************************************************************************/ +#include "time.h" + +/****************************************************************************** + * Macros + *****************************************************************************/ + +/****************************************************************************** + * Types and Classes + *****************************************************************************/ + +/** + * Abstract RTC interface. + */ +class IRtc +{ +public: + + /** + * Destroys the inteface. + */ + virtual ~IRtc() + { + } + + /** + * Checks for the RTC and if available, it will be initialized + * and started. + * + * @return If no RTC is available, it will return false. + */ + virtual bool begin() = 0; + + /** + * Get the time from the RTC. + * + * @param[out] timeInfo Time destination + * + * @return If time info is updated, it will return true otherwise false. + */ + virtual bool getTime(struct tm& timeInfo) = 0; + + /** + * Set the RTC by time. + * + * @param[in] timeInfo Time source + */ + virtual void setTime(const struct tm& timeInfo) = 0; + +protected: + + /** + * Creates the interface. + */ + IRtc() + { + } + +}; + +/****************************************************************************** + * Functions + *****************************************************************************/ + +#endif /* IRTC_H */ + +/** @} */ \ No newline at end of file diff --git a/lib/IRtc/library.json b/lib/IRtc/library.json new file mode 100644 index 00000000..876a5137 --- /dev/null +++ b/lib/IRtc/library.json @@ -0,0 +1,15 @@ +{ + "name": "IRtc", + "version": "0.1.0", + "description": "....", + "authors": [{ + "name": "Andreas Merkle", + "email": "web@blue-andi.de", + "url": "https://github.com/BlueAndi", + "maintainer": true + }], + "license": "MIT", + "dependencies": [], + "frameworks": "*", + "platforms": "*" +} diff --git a/lib/Rtc1307/library.json b/lib/Rtc1307/library.json new file mode 100644 index 00000000..75f57e05 --- /dev/null +++ b/lib/Rtc1307/library.json @@ -0,0 +1,25 @@ +{ + "name": "Rtc1307", + "version": "0.1.0", + "description": "....", + "authors": [{ + "name": "Andreas Merkle", + "email": "web@blue-andi.de", + "url": "https://github.com/BlueAndi", + "maintainer": true + }], + "license": "MIT", + "dependencies": [{ + "name": "IRtc" + }, { + "owner": "Adafruit", + "name": "RTClib", + "version": "~2.1.1" + }, { + "owner": "Adafruit", + "name": "Adafruit BusIO", + "version": "~1.14.1" + }], + "frameworks": "*", + "platforms": "*" +} diff --git a/lib/Rtc1307/src/Rtc1307Drv.cpp b/lib/Rtc1307/src/Rtc1307Drv.cpp new file mode 100644 index 00000000..56ce2175 --- /dev/null +++ b/lib/Rtc1307/src/Rtc1307Drv.cpp @@ -0,0 +1,136 @@ +/* MIT License + * + * Copyright (c) 2019 - 2023 Andreas Merkle + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/******************************************************************************* + DESCRIPTION +*******************************************************************************/ +/** + * @brief RTC 1307 driver + * @author Andreas Merkle + */ + +/****************************************************************************** + * Includes + *****************************************************************************/ +#include "Rtc1307Drv.h" + +/****************************************************************************** + * Compiler Switches + *****************************************************************************/ + +/****************************************************************************** + * Macros + *****************************************************************************/ + +/****************************************************************************** + * Types and classes + *****************************************************************************/ + +/****************************************************************************** + * Prototypes + *****************************************************************************/ + +/****************************************************************************** + * Local Variables + *****************************************************************************/ + +/****************************************************************************** + * Public Methods + *****************************************************************************/ + +bool Rtc1307Drv::begin() +{ + bool isSuccess = false; + + if (false == m_isInitialized) + { + isSuccess = m_rtc.begin(); + + if (true == isSuccess) + { + /* If RTC doesn't run, the RTC will be set. */ + if (false == m_rtc.isrunning()) + { + /* Set explicit to date & time this version of Pixelix was compiled. */ + m_rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); + } + } + + m_isInitialized = isSuccess; + } + + return isSuccess; +} + +bool Rtc1307Drv::getTime(struct tm& timeInfo) +{ + bool isSuccess = false; + + if (true == m_isInitialized) + { + DateTime dateTime = m_rtc.now(); + + memset(&timeInfo, 0, sizeof(struct tm)); + + timeInfo.tm_sec = dateTime.second(); + timeInfo.tm_min = dateTime.minute(); + timeInfo.tm_hour = dateTime.hour(); + timeInfo.tm_mday = dateTime.day(); + timeInfo.tm_mon = dateTime.month() - 1; + timeInfo.tm_year = dateTime.year() - 1900; + } + + return isSuccess; +} + +void Rtc1307Drv::setTime(const struct tm& timeInfo) +{ + if (true == m_isInitialized) + { + DateTime time( + timeInfo.tm_year + 1900, + timeInfo.tm_mon + 1, + timeInfo.tm_mday, + timeInfo.tm_hour, + timeInfo.tm_min, + timeInfo.tm_sec + ); + m_rtc.adjust(time); + } +} + +/****************************************************************************** + * Protected Methods + *****************************************************************************/ + +/****************************************************************************** + * Private Methods + *****************************************************************************/ + +/****************************************************************************** + * External Functions + *****************************************************************************/ + +/****************************************************************************** + * Local Functions + *****************************************************************************/ diff --git a/lib/Rtc1307/src/Rtc1307Drv.h b/lib/Rtc1307/src/Rtc1307Drv.h new file mode 100644 index 00000000..96ce07b6 --- /dev/null +++ b/lib/Rtc1307/src/Rtc1307Drv.h @@ -0,0 +1,124 @@ +/* MIT License + * + * Copyright (c) 2019 - 2023 Andreas Merkle + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/******************************************************************************* + DESCRIPTION +*******************************************************************************/ +/** + * @brief RTC 1307 driver + * @author Andreas Merkle + * + * @addtogroup Hal + * + * @{ + */ + +#ifndef RTC1307_DRV_H +#define RTC1307_DRV_H + +/****************************************************************************** + * Compile Switches + *****************************************************************************/ + +/****************************************************************************** + * Includes + *****************************************************************************/ +#include "Arduino.h" +#include + +#include +#include + +/****************************************************************************** + * Macros + *****************************************************************************/ + +/****************************************************************************** + * Types and Classes + *****************************************************************************/ + +/** + * The RTC 1307 driver provides the abstract RTC interface and realizes + * its functionality. + */ +class Rtc1307Drv : public IRtc +{ +public: + + /** + * Constructs the driver. + */ + Rtc1307Drv() : + IRtc(), + m_isInitialized(false), + m_rtc() + { + } + + /** + * Destroys the driver. + */ + ~Rtc1307Drv() + { + } + + /** + * Checks for the RTC and if available, it will be initialized + * and started. + * + * @return If no RTC is available, it will return false. + */ + bool begin() final; + + /** + * Get the time from the RTC. + * + * @param[out] timeInfo Time destination + * + * @return If time info is updated, it will return true otherwise false. + */ + bool getTime(struct tm& timeInfo) final; + + /** + * Set the RTC by time. + * + * @param[in] timeInfo Time source + */ + void setTime(const struct tm& timeInfo) final; + +private: + + bool m_isInitialized; /**< Already initialized or not. */ + RTC_DS1307 m_rtc; /**< Specific RTC driver. */ + + Rtc1307Drv(const Rtc1307Drv& drv); + Rtc1307Drv& operator=(const Rtc1307Drv& drv); +}; + +/****************************************************************************** + * Functions + *****************************************************************************/ + +#endif /* RTC1307_DRV_H */ + +/** @} */ \ No newline at end of file diff --git a/lib/SunrisePlugin/library.json b/lib/SunrisePlugin/library.json index fc667eab..e2efd8ea 100644 --- a/lib/SunrisePlugin/library.json +++ b/lib/SunrisePlugin/library.json @@ -13,6 +13,8 @@ "name": "LittleFS" }, { "name": "Plugin" + }, { + "name": "IRtc" }], "frameworks": "*", "platforms": "*" diff --git a/src/Hal/ClockDrv.cpp b/src/Hal/ClockDrv.cpp index 4c96a20e..cfb869b4 100644 --- a/src/Hal/ClockDrv.cpp +++ b/src/Hal/ClockDrv.cpp @@ -38,6 +38,7 @@ #include #include #include +#include /****************************************************************************** * Compiler Switches @@ -55,6 +56,8 @@ * Prototypes *****************************************************************************/ +extern void sntpCallback(struct timeval *tv); + /****************************************************************************** * Local Variables *****************************************************************************/ @@ -66,86 +69,88 @@ const char* ClockDrv::TZ_UTC = "UTC+0"; * Public Methods *****************************************************************************/ -void ClockDrv::init() +void ClockDrv::init(IRtc* rtc) { - if (false == m_isClockDrvInitialized) + String ntpServerAddress; + struct tm timeInfo = { 0 }; + SettingsService& settings = SettingsService::getInstance(); + char tzBuffer[TZ_MIN_SIZE]; + + /* Get the GMT offset, daylight saving enabled/disabled and NTP server address from persistent memory. */ + if (false == settings.open(true)) { - String ntpServerAddress; - struct tm timeInfo = { 0 }; - SettingsService& settings = SettingsService::getInstance(); - char tzBuffer[TZ_MIN_SIZE]; + LOG_WARNING("Use default values for NTP request."); - /* Get the GMT offset, daylight saving enabled/disabled and NTP server address from persistent memory. */ - if (false == settings.open(true)) - { - LOG_WARNING("Use default values for NTP request."); + m_timeZone = settings.getTimezone().getDefault(); + ntpServerAddress = settings.getNTPServerAddress().getDefault(); + } + else + { + m_timeZone = settings.getTimezone().getValue(); + ntpServerAddress = settings.getNTPServerAddress().getValue(); + settings.close(); + } - m_timeZone = settings.getTimezone().getDefault(); - ntpServerAddress = settings.getNTPServerAddress().getDefault(); - } - else - { - m_timeZone = settings.getTimezone().getValue(); - ntpServerAddress = settings.getNTPServerAddress().getValue(); - settings.close(); - } + /* Initialize RTC use its time. */ + m_rtc = rtc; - /* Workaround part 1 to avoid memory leaks by calling setenv() of the newlib. - * https://github.com/espressif/esp-idf/issues/3046 - */ - strcpy(tzBuffer, TZ_UTC); - fillUpWithSpaces(tzBuffer, TZ_MIN_SIZE); - - /* Configure NTP: - * This will periodically synchronize the time. The time synchronization - * period is determined by CONFIG_LWIP_SNTP_UPDATE_DELAY (default value is one hour). - * To modify the variable, set CONFIG_LWIP_SNTP_UPDATE_DELAY in project configuration. - * https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/system_time.html - * https://github.com/espressif/esp-idf/issues/4386 - */ - configTzTime(tzBuffer, ntpServerAddress.c_str()); - - /* Workaround part 2 to avoid memory leaks by calling setenv() of the newlib. - * https://github.com/espressif/esp-idf/issues/3046 - */ - m_internalTimeZoneBuffer = getenv("TZ"); - - /* Wait for synchronization (default 5s) */ - if (false == getLocalTime(&timeInfo)) + if (nullptr != m_rtc) + { + /* Check whether RTC is available and initialize it. */ + if (false == m_rtc->begin()) { - LOG_ERROR("Failed to synchronize time."); + LOG_INFO("No RTC is available."); } else { - LOG_INFO("UTC: %d-%d-%d %d:%d", - (timeInfo.tm_year + 1900), - (timeInfo.tm_mon + 1), - timeInfo.tm_mday, - timeInfo.tm_hour, - timeInfo.tm_min); + LOG_INFO("RTC is available."); + syncTimeByRtc(); + sntp_set_time_sync_notification_cb(sntpCallback); } - - m_isClockDrvInitialized = true; } + + /* Workaround part 1 to avoid memory leaks by calling setenv() of the newlib. + * https://github.com/espressif/esp-idf/issues/3046 + */ + strcpy(tzBuffer, TZ_UTC); + fillUpWithSpaces(tzBuffer, TZ_MIN_SIZE); + + /* Configure NTP: + * This will periodically synchronize the time. The time synchronization + * period is determined by CONFIG_LWIP_SNTP_UPDATE_DELAY (default value is one hour). + * To modify the variable, set CONFIG_LWIP_SNTP_UPDATE_DELAY in project configuration. + * https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/system_time.html + * https://github.com/espressif/esp-idf/issues/4386 + */ + configTzTime(tzBuffer, ntpServerAddress.c_str()); + + /* Workaround part 2 to avoid memory leaks by calling setenv() of the newlib. + * https://github.com/espressif/esp-idf/issues/3046 + */ + m_internalTimeZoneBuffer = getenv("TZ"); } -bool ClockDrv::getTime(tm* timeInfo) +bool ClockDrv::getTime(struct tm& timeInfo) { return getTzTime(m_timeZone.c_str(), timeInfo); } -bool ClockDrv::getUtcTime(tm* timeInfo) +bool ClockDrv::getUtcTime(struct tm& timeInfo) { const uint32_t WAIT_TIME_MS = 0U; - return getLocalTime(timeInfo, WAIT_TIME_MS); + syncTimeByRtc(); + + return getLocalTime(&timeInfo, WAIT_TIME_MS); } -bool ClockDrv::getTzTime(const char* tz, tm* timeInfo) +bool ClockDrv::getTzTime(const char* tz, struct tm& timeInfo) { const uint32_t WAIT_TIME_MS = 0U; bool result = false; + syncTimeByRtc(); + if (nullptr != tz) { /* Configure timezone */ @@ -162,7 +167,7 @@ bool ClockDrv::getTzTime(const char* tz, tm* timeInfo) } } - result = getLocalTime(timeInfo, WAIT_TIME_MS); + result = getLocalTime(&timeInfo, WAIT_TIME_MS); if (nullptr != tz) { @@ -205,11 +210,106 @@ void ClockDrv::fillUpWithSpaces(char* str, size_t size) str[size - 1U] = '\0'; } +void ClockDrv::setTimeByRtc() +{ + if (nullptr != m_rtc) + { + struct tm timeInfo; + + if (false == m_rtc->getTime(timeInfo)) + { + time_t timeSinceEpoch = mktime(&timeInfo); + struct timeval tv = { timeSinceEpoch, 0 }; + + settimeofday(&tv, nullptr); + } + } +} + +void ClockDrv::setRtcByTime() +{ + if (nullptr != m_rtc) + { + struct tm timeInfo; + + if (true == getUtcTime(timeInfo)) + { + m_rtc->setTime(timeInfo); + } + } +} + +void ClockDrv::syncTimeByRtc() +{ + bool sync = false; + + if (false == m_syncTimeByRtcTimer.isTimerRunning()) + { + m_syncTimeByRtcTimer.start(SYNC_TIME_BY_RTC_PERIOD); + sync = true; + } + else if (true == m_syncTimeByRtcTimer.isTimeout()) + { + sync = true; + } + + if (true == sync) + { + LOG_INFO("Sync time by RTC."); + + setTimeByRtc(); + m_syncTimeByRtcTimer.restart(); + } +} + +void ClockDrv::syncRtcByTime() +{ + bool sync = false; + + if (false == m_syncRtcByNtpTimer.isTimerRunning()) + { + m_syncRtcByNtpTimer.start(SYNC_RTC_BY_TIME_PERIOD); + sync = true; + } + else if (true == m_syncRtcByNtpTimer.isTimeout()) + { + sync = true; + } + + if (true == sync) + { + LOG_INFO("Sync RTC by time."); + + setRtcByTime(); + m_syncRtcByNtpTimer.restart(); + } +} + /****************************************************************************** * External Functions *****************************************************************************/ +/** + * This function is called by the SNTP for every received time information + * from the NTP. + * + * @param[in] tv Time information + */ +extern void sntpCallback(struct timeval *tv) +{ + ClockDrv& clockDrv = ClockDrv::getInstance(); + + (void)tv; + + /* As long as updates from NTP are received, no synchronization from the RTC + * to the local timer shall be done. + */ + clockDrv.m_syncTimeByRtcTimer.restart(); + + /* Synchronize RTC by time. */ + clockDrv.syncRtcByTime(); +} + /****************************************************************************** * Local Functions *****************************************************************************/ - diff --git a/src/Hal/ClockDrv.h b/src/Hal/ClockDrv.h index 17d0dbb1..469fd322 100644 --- a/src/Hal/ClockDrv.h +++ b/src/Hal/ClockDrv.h @@ -44,7 +44,9 @@ * Includes *****************************************************************************/ #include "Arduino.h" -#include "time.h" +#include + +#include /****************************************************************************** * Macros @@ -75,9 +77,11 @@ class ClockDrv /** * Initialize the ClockDrv. + * If no RTC is available, use nullptr for the rtc parameter. * + * @param[in] rtc Real time clock driver */ - void init(); + void init(IRtc* rtc); /** * Get the local time by considering device timezone. @@ -86,7 +90,7 @@ class ClockDrv * * @return If time is not synchronized, it will return false otherwise true. */ - bool getTime(tm* timeInfo); + bool getTime(struct tm& timeInfo); /** * Get the current time in UTC. @@ -95,7 +99,7 @@ class ClockDrv * * @return If time is not synchronized, it will return false otherwise true. */ - bool getUtcTime(tm* timeInfo); + bool getUtcTime(struct tm& timeInfo); /** * Get the local time by considering the timezone. @@ -105,26 +109,47 @@ class ClockDrv * * @return If time is not synchronized, it will return false otherwise true. */ - bool getTzTime(const char* tz, tm* timeInfo); + bool getTzTime(const char* tz, struct tm& timeInfo); private: /** * The minimum timezone string size (incl. string termination). */ - static const size_t TZ_MIN_SIZE = 60U; + static const size_t TZ_MIN_SIZE = 60U; - /** Use UTC timezone by default. */ - static const char* TZ_UTC; + /** + * Use UTC timezone by default. + */ + static const char* TZ_UTC; + + /** + * Period for time synchronization by RTC in ms. + */ + static const int32_t SYNC_TIME_BY_RTC_PERIOD = SIMPLE_TIMER_HOURS(1U); + + /** + * Period for RTC synchronization by time in ms. + */ + static const uint32_t SYNC_RTC_BY_TIME_PERIOD = SIMPLE_TIMER_DAYS(2U); /** Flag indicating a initialized clock driver. */ - bool m_isClockDrvInitialized; + bool m_isClockDrvInitialized; /** Device timezone */ - String m_timeZone; + String m_timeZone; /** newlib's internal timezone buffer. */ - char* m_internalTimeZoneBuffer; + char* m_internalTimeZoneBuffer; + + /** Real time clock */ + IRtc* m_rtc; + + /** Timer used to synchronize the time by the RTC. */ + SimpleTimer m_syncTimeByRtcTimer; + + /** Timer used to synchronize the RTC by the time. */ + SimpleTimer m_syncRtcByNtpTimer; /** * Construct ClockDrv. @@ -132,7 +157,10 @@ class ClockDrv ClockDrv() : m_isClockDrvInitialized(false), m_timeZone(TZ_UTC), - m_internalTimeZoneBuffer(nullptr) + m_internalTimeZoneBuffer(nullptr), + m_rtc(nullptr), + m_syncTimeByRtcTimer(), + m_syncRtcByNtpTimer() { } @@ -154,6 +182,37 @@ class ClockDrv * @param[in] size String buffer size in byte (incl. termination) */ void fillUpWithSpaces(char* str, size_t size); + + /** + * Update the time by the RTC. + * If no RTC is available, nothing will happen. + */ + void setTimeByRtc(); + + /** + * Update the RTC by the time. + * If no RTC is available, nothing will happen. + */ + void setRtcByTime(); + + /** + * Synchronize periodically the time by the RTC. + * If the synchronization time period is expired, it will synchronizse + * otherwise not. + * If no RTC is available, nothing will happen. + */ + void syncTimeByRtc(); + + /** + * Synchronize periodically the RTC by the time. + * If the synchronization time period is expired, it will synchronizse + * otherwise not. + * If no RTC is available, nothing will happen. + */ + void syncRtcByTime(); + + /* Allow the SNTP callback to synchronize the RTC by the NTP. */ + friend void sntpCallback(struct timeval *tv); }; /****************************************************************************** diff --git a/src/Hal/RtcNoneDrv.cpp b/src/Hal/RtcNoneDrv.cpp new file mode 100644 index 00000000..69829786 --- /dev/null +++ b/src/Hal/RtcNoneDrv.cpp @@ -0,0 +1,75 @@ +/* MIT License + * + * Copyright (c) 2019 - 2023 Andreas Merkle + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/******************************************************************************* + DESCRIPTION +*******************************************************************************/ +/** + * @brief RTC driver without functionality + * @author Andreas Merkle + */ + +/****************************************************************************** + * Includes + *****************************************************************************/ +#include "RtcNoneDrv.h" + +/****************************************************************************** + * Compiler Switches + *****************************************************************************/ + +/****************************************************************************** + * Macros + *****************************************************************************/ + +/****************************************************************************** + * Types and classes + *****************************************************************************/ + +/****************************************************************************** + * Prototypes + *****************************************************************************/ + +/****************************************************************************** + * Local Variables + *****************************************************************************/ + +/****************************************************************************** + * Public Methods + *****************************************************************************/ + +/****************************************************************************** + * Protected Methods + *****************************************************************************/ + +/****************************************************************************** + * Private Methods + *****************************************************************************/ + +/****************************************************************************** + * External Functions + *****************************************************************************/ + +/****************************************************************************** + * Local Functions + *****************************************************************************/ diff --git a/src/Hal/RtcNoneDrv.h b/src/Hal/RtcNoneDrv.h new file mode 100644 index 00000000..f1599dee --- /dev/null +++ b/src/Hal/RtcNoneDrv.h @@ -0,0 +1,126 @@ +/* MIT License + * + * Copyright (c) 2019 - 2023 Andreas Merkle + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/******************************************************************************* + DESCRIPTION +*******************************************************************************/ +/** + * @brief RTC driver without functionality + * @author Andreas Merkle + * + * @addtogroup Hal + * + * @{ + */ + +#ifndef RTCNONE_DRV_H +#define RTCNONE_DRV_H + +/****************************************************************************** + * Compile Switches + *****************************************************************************/ + +/****************************************************************************** + * Includes + *****************************************************************************/ +#include "Arduino.h" +#include + +/****************************************************************************** + * Macros + *****************************************************************************/ + +/****************************************************************************** + * Types and Classes + *****************************************************************************/ + +/** + * The RTC driver provides the abstract RTC interface, but has no functionality. + */ +class RtcNoneDrv : public IRtc +{ +public: + + /** + * Constructs the driver. + */ + RtcNoneDrv() : + IRtc() + { + } + + /** + * Destroys the driver. + */ + ~RtcNoneDrv() + { + } + + /** + * Checks for the RTC and if available, it will be initialized + * and started. + * + * @return If no RTC is available, it will return false. + */ + bool begin() final + { + return false; + } + + /** + * Get the time from the RTC. + * + * @param[out] timeInfo Time destination + * + * @return If time info is updated, it will return true otherwise false. + */ + bool getTime(struct tm& timeInfo) final + { + (void)timeInfo; + + return false; + } + + /** + * Set the RTC by time. + * + * @param[in] timeInfo Time source + */ + void setTime(const struct tm& timeInfo) final + { + (void)timeInfo; + } + +private: + + RtcNoneDrv(const RtcNoneDrv& drv); + RtcNoneDrv& operator=(const RtcNoneDrv& drv); +}; + +/****************************************************************************** + * Functions + *****************************************************************************/ + +#endif /* RTCNONE_DRV_H */ + +/** @} */ \ No newline at end of file diff --git a/src/StateMachine/ConnectedState.cpp b/src/StateMachine/ConnectedState.cpp index ffc8b4fb..dbd38965 100644 --- a/src/StateMachine/ConnectedState.cpp +++ b/src/StateMachine/ConnectedState.cpp @@ -36,7 +36,6 @@ #include "SysMsg.h" #include "UpdateMgr.h" #include "MyWebServer.h" -#include "ClockDrv.h" #include "DisplayMgr.h" #include "Services.h" @@ -120,9 +119,6 @@ void ConnectedState::entry(StateMachine& sm) const uint32_t DURATION_NON_SCROLLING = 4000U; /* ms */ const uint32_t SCROLLING_REPEAT_NUM = 2U; - /* Start the ClockDriver */ - ClockDrv::getInstance().init(); - /* Notify about successful network connection. */ DisplayMgr::getInstance().setNetworkStatus(true); diff --git a/src/StateMachine/InitState.cpp b/src/StateMachine/InitState.cpp index a820af84..4fda0605 100644 --- a/src/StateMachine/InitState.cpp +++ b/src/StateMachine/InitState.cpp @@ -42,6 +42,7 @@ #include #include "ButtonDrv.h" +#include "ClockDrv.h" #include "DisplayMgr.h" #include "SysMsg.h" #include "Version.h" @@ -174,6 +175,9 @@ void InitState::entry(StateMachine& sm) } else { + /* Initialize clock driver */ + ClockDrv::getInstance().init(&m_rtcDrv); + /* Initialize sensors */ SensorDataProvider::getInstance().begin(); diff --git a/src/StateMachine/InitState.h b/src/StateMachine/InitState.h index 8f8ec0f3..afd3e833 100644 --- a/src/StateMachine/InitState.h +++ b/src/StateMachine/InitState.h @@ -48,6 +48,13 @@ #include #include +#if CONFIG_RTC == 1 +#include "Rtc1307Drv.h" +#else /* CONFIG_RTC == 1 */ +#include "RtcNoneDrv.h" +#endif /* CONFIG_RTC == 1 */ + + /****************************************************************************** * Macros *****************************************************************************/ @@ -110,6 +117,12 @@ class InitState : public AbstractState bool m_isApModeRequested; /**< Is wifi AP mode requested? */ SimpleTimer m_timer; /**< Timer used to stay for a min. time in this state. */ +#if CONFIG_RTC == 1 + Rtc1307Drv m_rtcDrv; /**< RTC driver */ +#else /* CONFIG_RTC == 1 */ + RtcNoneDrv m_rtcDrv; /**< RTC driver without functionality. */ +#endif /* CONFIG_RTC == 1 */ + /** * Constructs the state. */ From b2f16e0075fe46182c479d57da5220b51b5b0ce5 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Thu, 6 Jul 2023 18:48:08 +0200 Subject: [PATCH 015/105] Start with 25 % brightness to get in the DateTimePlugin all of the days shown, not only the actual one. --- lib/SettingsService/src/SettingsService.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/SettingsService/src/SettingsService.cpp b/lib/SettingsService/src/SettingsService.cpp index 73c07dfc..20d44f68 100644 --- a/lib/SettingsService/src/SettingsService.cpp +++ b/lib/SettingsService/src/SettingsService.cpp @@ -194,7 +194,7 @@ static const char* DEFAULT_WEB_LOGIN_PASSWORD = "skywalker"; static const char* DEFAULT_HOSTNAME = "pixelix"; /** Brightness default value in % */ -static const uint8_t DEFAULT_BRIGHTNESS = 20U; /* If powered via USB, keep this at 20% to avoid damage. */ +static const uint8_t DEFAULT_BRIGHTNESS = 25U; /* If powered via USB, keep this at 25% to avoid damage. */ /** Automatic brightness control default value */ static const bool DEFAULT_AUTO_BRIGHTNESS_CTRL = false; From 8b1a4b06c1da8534d3513fddb76ad6d0911da2ad Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sat, 8 Jul 2023 22:28:44 +0200 Subject: [PATCH 016/105] Keep naming consistent. --- lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp | 6 +++--- lib/IconTextPlugin/src/IconTextPlugin.cpp | 6 +++--- lib/JustTextPlugin/src/JustTextPlugin.cpp | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp b/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp index b197a02a..dc6ae5c0 100644 --- a/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp +++ b/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp @@ -170,11 +170,11 @@ bool IconTextLampPlugin::setTopic(const String& topic, const JsonObject& value) if (0U != topic.equals(TOPIC_TEXT)) { String text; - JsonVariantConst jsonShow = value["text"]; + JsonVariantConst jsonText = value["text"]; - if (false == jsonShow.isNull()) + if (false == jsonText.isNull()) { - text = jsonShow.as(); + text = jsonText.as(); isSuccessful = true; } diff --git a/lib/IconTextPlugin/src/IconTextPlugin.cpp b/lib/IconTextPlugin/src/IconTextPlugin.cpp index c0a77af3..a47eb6a4 100644 --- a/lib/IconTextPlugin/src/IconTextPlugin.cpp +++ b/lib/IconTextPlugin/src/IconTextPlugin.cpp @@ -128,11 +128,11 @@ bool IconTextPlugin::setTopic(const String& topic, const JsonObject& value) if (0U != topic.equals(TOPIC_TEXT)) { String text; - JsonVariantConst jsonShow = value["text"]; + JsonVariantConst jsonText = value["text"]; - if (false == jsonShow.isNull()) + if (false == jsonText.isNull()) { - text = jsonShow.as(); + text = jsonText.as(); isSuccessful = true; } diff --git a/lib/JustTextPlugin/src/JustTextPlugin.cpp b/lib/JustTextPlugin/src/JustTextPlugin.cpp index c777fe56..55b836e4 100644 --- a/lib/JustTextPlugin/src/JustTextPlugin.cpp +++ b/lib/JustTextPlugin/src/JustTextPlugin.cpp @@ -115,11 +115,11 @@ bool JustTextPlugin::setTopic(const String& topic, const JsonObject& value) if (0U != topic.equals(TOPIC_TEXT)) { String text; - JsonVariantConst jsonShow = value["text"]; + JsonVariantConst jsonText = value["text"]; - if (false == jsonShow.isNull()) + if (false == jsonText.isNull()) { - text = jsonShow.as(); + text = jsonText.as(); isSuccessful = true; } From e1677047fde1d7f7e4f281d817d8035ba83e5ddf Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Tue, 11 Jul 2023 23:36:03 +0200 Subject: [PATCH 017/105] Sourcecode in libs moved to a separate src folder with a library.json for consistency. --- lib/IRtc/{ => src}/IRtc.h | 0 lib/ITopicHandler/{ => src}/ITopicHandler.h | 0 lib/Os/library.json | 15 +++++++++++++++ lib/Os/{ => src}/CriticalSection.hpp | 0 lib/Os/{ => src}/Mutex.hpp | 0 lib/Os/{ => src}/Queue.hpp | 0 lib/Plugin/{ => src}/IPluginMaintenance.hpp | 0 lib/Plugin/{ => src}/ISlotPlugin.hpp | 0 lib/Plugin/{ => src}/Plugin.hpp | 0 lib/StateMachine/library.json | 15 +++++++++++++++ lib/StateMachine/{ => src}/StateMachine.hpp | 0 lib/Utilities/library.json | 15 +++++++++++++++ lib/Utilities/{ => src}/FileSystem.h | 0 lib/Utilities/{ => src}/JsonFile.cpp | 0 lib/Utilities/{ => src}/JsonFile.h | 0 lib/Utilities/{ => src}/LogSinkPrinter.cpp | 0 lib/Utilities/{ => src}/LogSinkPrinter.h | 0 lib/Utilities/{ => src}/Logging.cpp | 0 lib/Utilities/{ => src}/Logging.h | 0 lib/Utilities/{ => src}/SimpleTimer.hpp | 0 lib/Utilities/{ => src}/StatisticValue.hpp | 0 lib/Utilities/{ => src}/TaskProxy.hpp | 0 lib/Utilities/{ => src}/Util.cpp | 0 lib/Utilities/{ => src}/Util.h | 0 24 files changed, 45 insertions(+) rename lib/IRtc/{ => src}/IRtc.h (100%) rename lib/ITopicHandler/{ => src}/ITopicHandler.h (100%) create mode 100644 lib/Os/library.json rename lib/Os/{ => src}/CriticalSection.hpp (100%) rename lib/Os/{ => src}/Mutex.hpp (100%) rename lib/Os/{ => src}/Queue.hpp (100%) rename lib/Plugin/{ => src}/IPluginMaintenance.hpp (100%) rename lib/Plugin/{ => src}/ISlotPlugin.hpp (100%) rename lib/Plugin/{ => src}/Plugin.hpp (100%) create mode 100644 lib/StateMachine/library.json rename lib/StateMachine/{ => src}/StateMachine.hpp (100%) create mode 100644 lib/Utilities/library.json rename lib/Utilities/{ => src}/FileSystem.h (100%) rename lib/Utilities/{ => src}/JsonFile.cpp (100%) rename lib/Utilities/{ => src}/JsonFile.h (100%) rename lib/Utilities/{ => src}/LogSinkPrinter.cpp (100%) rename lib/Utilities/{ => src}/LogSinkPrinter.h (100%) rename lib/Utilities/{ => src}/Logging.cpp (100%) rename lib/Utilities/{ => src}/Logging.h (100%) rename lib/Utilities/{ => src}/SimpleTimer.hpp (100%) rename lib/Utilities/{ => src}/StatisticValue.hpp (100%) rename lib/Utilities/{ => src}/TaskProxy.hpp (100%) rename lib/Utilities/{ => src}/Util.cpp (100%) rename lib/Utilities/{ => src}/Util.h (100%) diff --git a/lib/IRtc/IRtc.h b/lib/IRtc/src/IRtc.h similarity index 100% rename from lib/IRtc/IRtc.h rename to lib/IRtc/src/IRtc.h diff --git a/lib/ITopicHandler/ITopicHandler.h b/lib/ITopicHandler/src/ITopicHandler.h similarity index 100% rename from lib/ITopicHandler/ITopicHandler.h rename to lib/ITopicHandler/src/ITopicHandler.h diff --git a/lib/Os/library.json b/lib/Os/library.json new file mode 100644 index 00000000..29d77108 --- /dev/null +++ b/lib/Os/library.json @@ -0,0 +1,15 @@ +{ + "name": "Os", + "version": "0.1.0", + "description": "....", + "authors": [{ + "name": "Andreas Merkle", + "email": "web@blue-andi.de", + "url": "https://github.com/BlueAndi", + "maintainer": true + }], + "license": "MIT", + "dependencies": [], + "frameworks": "*", + "platforms": "*" +} diff --git a/lib/Os/CriticalSection.hpp b/lib/Os/src/CriticalSection.hpp similarity index 100% rename from lib/Os/CriticalSection.hpp rename to lib/Os/src/CriticalSection.hpp diff --git a/lib/Os/Mutex.hpp b/lib/Os/src/Mutex.hpp similarity index 100% rename from lib/Os/Mutex.hpp rename to lib/Os/src/Mutex.hpp diff --git a/lib/Os/Queue.hpp b/lib/Os/src/Queue.hpp similarity index 100% rename from lib/Os/Queue.hpp rename to lib/Os/src/Queue.hpp diff --git a/lib/Plugin/IPluginMaintenance.hpp b/lib/Plugin/src/IPluginMaintenance.hpp similarity index 100% rename from lib/Plugin/IPluginMaintenance.hpp rename to lib/Plugin/src/IPluginMaintenance.hpp diff --git a/lib/Plugin/ISlotPlugin.hpp b/lib/Plugin/src/ISlotPlugin.hpp similarity index 100% rename from lib/Plugin/ISlotPlugin.hpp rename to lib/Plugin/src/ISlotPlugin.hpp diff --git a/lib/Plugin/Plugin.hpp b/lib/Plugin/src/Plugin.hpp similarity index 100% rename from lib/Plugin/Plugin.hpp rename to lib/Plugin/src/Plugin.hpp diff --git a/lib/StateMachine/library.json b/lib/StateMachine/library.json new file mode 100644 index 00000000..7d463a60 --- /dev/null +++ b/lib/StateMachine/library.json @@ -0,0 +1,15 @@ +{ + "name": "StateMachine", + "version": "0.1.0", + "description": "....", + "authors": [{ + "name": "Andreas Merkle", + "email": "web@blue-andi.de", + "url": "https://github.com/BlueAndi", + "maintainer": true + }], + "license": "MIT", + "dependencies": [], + "frameworks": "*", + "platforms": "*" +} diff --git a/lib/StateMachine/StateMachine.hpp b/lib/StateMachine/src/StateMachine.hpp similarity index 100% rename from lib/StateMachine/StateMachine.hpp rename to lib/StateMachine/src/StateMachine.hpp diff --git a/lib/Utilities/library.json b/lib/Utilities/library.json new file mode 100644 index 00000000..5f7571c9 --- /dev/null +++ b/lib/Utilities/library.json @@ -0,0 +1,15 @@ +{ + "name": "Utilities", + "version": "0.1.0", + "description": "...", + "authors": [{ + "name": "Andreas Merkle", + "email": "web@blue-andi.de", + "url": "https://github.com/BlueAndi", + "maintainer": true + }], + "license": "MIT", + "dependencies": [], + "frameworks": "*", + "platforms": "*" +} diff --git a/lib/Utilities/FileSystem.h b/lib/Utilities/src/FileSystem.h similarity index 100% rename from lib/Utilities/FileSystem.h rename to lib/Utilities/src/FileSystem.h diff --git a/lib/Utilities/JsonFile.cpp b/lib/Utilities/src/JsonFile.cpp similarity index 100% rename from lib/Utilities/JsonFile.cpp rename to lib/Utilities/src/JsonFile.cpp diff --git a/lib/Utilities/JsonFile.h b/lib/Utilities/src/JsonFile.h similarity index 100% rename from lib/Utilities/JsonFile.h rename to lib/Utilities/src/JsonFile.h diff --git a/lib/Utilities/LogSinkPrinter.cpp b/lib/Utilities/src/LogSinkPrinter.cpp similarity index 100% rename from lib/Utilities/LogSinkPrinter.cpp rename to lib/Utilities/src/LogSinkPrinter.cpp diff --git a/lib/Utilities/LogSinkPrinter.h b/lib/Utilities/src/LogSinkPrinter.h similarity index 100% rename from lib/Utilities/LogSinkPrinter.h rename to lib/Utilities/src/LogSinkPrinter.h diff --git a/lib/Utilities/Logging.cpp b/lib/Utilities/src/Logging.cpp similarity index 100% rename from lib/Utilities/Logging.cpp rename to lib/Utilities/src/Logging.cpp diff --git a/lib/Utilities/Logging.h b/lib/Utilities/src/Logging.h similarity index 100% rename from lib/Utilities/Logging.h rename to lib/Utilities/src/Logging.h diff --git a/lib/Utilities/SimpleTimer.hpp b/lib/Utilities/src/SimpleTimer.hpp similarity index 100% rename from lib/Utilities/SimpleTimer.hpp rename to lib/Utilities/src/SimpleTimer.hpp diff --git a/lib/Utilities/StatisticValue.hpp b/lib/Utilities/src/StatisticValue.hpp similarity index 100% rename from lib/Utilities/StatisticValue.hpp rename to lib/Utilities/src/StatisticValue.hpp diff --git a/lib/Utilities/TaskProxy.hpp b/lib/Utilities/src/TaskProxy.hpp similarity index 100% rename from lib/Utilities/TaskProxy.hpp rename to lib/Utilities/src/TaskProxy.hpp diff --git a/lib/Utilities/Util.cpp b/lib/Utilities/src/Util.cpp similarity index 100% rename from lib/Utilities/Util.cpp rename to lib/Utilities/src/Util.cpp diff --git a/lib/Utilities/Util.h b/lib/Utilities/src/Util.h similarity index 100% rename from lib/Utilities/Util.h rename to lib/Utilities/src/Util.h From e0762a3af10f4e4a8f0d447536315c17864e891d Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Thu, 13 Jul 2023 21:18:55 +0200 Subject: [PATCH 018/105] Small refactoring by moving sourcecode to methods to improve readability and maintainability. --- src/Hal/SensorDataProvider.cpp | 126 ++++++++++++++++++--------------- src/Hal/SensorDataProvider.h | 10 +++ 2 files changed, 80 insertions(+), 56 deletions(-) diff --git a/src/Hal/SensorDataProvider.cpp b/src/Hal/SensorDataProvider.cpp index d3f5362c..261f53e3 100644 --- a/src/Hal/SensorDataProvider.cpp +++ b/src/Hal/SensorDataProvider.cpp @@ -68,70 +68,16 @@ const char* SensorDataProvider::SENSOR_CALIB_FILE_NAME = "/configuration/sensors void SensorDataProvider::begin() { - uint8_t index = 0U; - uint8_t cnt = m_impl->getNumSensors(); - /* Initialize all sensor drivers. */ m_impl->begin(); /* Load calibration values. If they are not available, save it with the sensor defaults. */ if (false == load()) { - uint8_t defaultValues = 0U; - const SensorChannelDefaultValue* sensorChannelDefaultValueList = Sensors::getSensorChannelDefaultValues(defaultValues); - - /* Use the default values. */ - if (nullptr != sensorChannelDefaultValueList) - { - for(index = 0U; index < defaultValues; ++index) - { - const SensorChannelDefaultValue* value = &sensorChannelDefaultValueList[index]; - - if (nullptr != value) - { - ISensor* sensor = getSensor(value->sensorId); - - if (nullptr == sensor) - { - LOG_ERROR("Sensor %u doesn't exists.", value->sensorId); - } - else - { - ISensorChannel* channel = sensor->getChannel(value->channelId); - - if (nullptr == channel) - { - LOG_ERROR("Sensor %u has no channel %u.", value->sensorId, value->channelId); - } - else - { - DynamicJsonDocument jsonDoc(256U); - - if (DeserializationError::Ok == deserializeJson(jsonDoc, value->jsonStrValue)) - { - channelOffsetFromJson(*channel, jsonDoc["offset"]); - } - } - } - } - } - } - - (void)save(); + createCalibrationFile(); } - /* For debug purposes, show the sensor driver states. */ - for(index = 0U; index < cnt; ++index) - { - ISensor* sensor = m_impl->getSensor(index); - - if (nullptr != sensor) - { - bool isAvailable = sensor->isAvailable(); - - LOG_INFO("Sensor %s: %s", sensor->getName(), (false == isAvailable) ? "-" : "available" ); - } - } + logSensorAvailability(); } uint8_t SensorDataProvider::getNumSensors() const @@ -317,6 +263,27 @@ SensorDataProvider::SensorDataProvider() : { } +void SensorDataProvider::logSensorAvailability() +{ + uint8_t index = 0U; + uint8_t cnt = m_impl->getNumSensors(); + + /* For user information, show the sensor driver states. */ + while(cnt > index) + { + ISensor* sensor = m_impl->getSensor(index); + + if (nullptr != sensor) + { + bool isAvailable = sensor->isAvailable(); + + LOG_INFO("Sensor %s: %s", sensor->getName(), (false == isAvailable) ? "-" : "available" ); + } + + ++index; + } +} + void SensorDataProvider::channelOffsetToJson(JsonArray& jsonOffset, const ISensorChannel& channel) const { switch(channel.getDataType()) @@ -433,6 +400,53 @@ void SensorDataProvider::channelOffsetFromJson(ISensorChannel& channel, JsonVari } } +void SensorDataProvider::createCalibrationFile() +{ + uint8_t defaultValues = 0U; + const SensorChannelDefaultValue* sensorChannelDefaultValueList = Sensors::getSensorChannelDefaultValues(defaultValues); + + /* Use the default values. */ + if (nullptr != sensorChannelDefaultValueList) + { + uint8_t index = 0U; + + for(index = 0U; index < defaultValues; ++index) + { + const SensorChannelDefaultValue* value = &sensorChannelDefaultValueList[index]; + + if (nullptr != value) + { + ISensor* sensor = getSensor(value->sensorId); + + if (nullptr == sensor) + { + LOG_ERROR("Sensor %u doesn't exists.", value->sensorId); + } + else + { + ISensorChannel* channel = sensor->getChannel(value->channelId); + + if (nullptr == channel) + { + LOG_ERROR("Sensor %u has no channel %u.", value->sensorId, value->channelId); + } + else + { + DynamicJsonDocument jsonDoc(256U); + + if (DeserializationError::Ok == deserializeJson(jsonDoc, value->jsonStrValue)) + { + channelOffsetFromJson(*channel, jsonDoc["offset"]); + } + } + } + } + } + } + + (void)save(); +} + /****************************************************************************** * External Functions *****************************************************************************/ diff --git a/src/Hal/SensorDataProvider.h b/src/Hal/SensorDataProvider.h index d387115f..0cdffb29 100644 --- a/src/Hal/SensorDataProvider.h +++ b/src/Hal/SensorDataProvider.h @@ -167,6 +167,11 @@ class SensorDataProvider SensorDataProvider(const SensorDataProvider& instance); SensorDataProvider& operator=(const SensorDataProvider& instance); + /** + * Log the sensor availability to the logging system as user information. + */ + void logSensorAvailability(); + /** * Add the channel offset value to the JSON array. * @@ -182,6 +187,11 @@ class SensorDataProvider * @param[in] jsonOffset JSON offset value */ void channelOffsetFromJson(ISensorChannel& channel, JsonVariantConst jsonOffset) const; + + /** + * Create file with the default calibration values. + */ + void createCalibrationFile(); }; /****************************************************************************** From d02f4732a3f04be2213966291e33f5abce7f6406 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Thu, 13 Jul 2023 21:43:20 +0200 Subject: [PATCH 019/105] Typo fixed. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3602048a..9ed06094 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ The PIXELIX firmware is for ESP32 boards that controls a RGB LED matrix. It can * Supports REST API and MQTT for remote control and integration with other systems, like [Home Assistant](https://www.home-assistant.io/). * Can be extended with custom effects and animations. See list of [plugins](./doc/PLUGINS.md). -Please note, that not every feature might be available for all kind of development boards. E.g. for MQTT support you need 8 a development board with 8 MB flash or more. See the `config.ini` configuration files in [./config](./config) folder. +Please note, that not every feature might be available for all kind of development boards. E.g. for MQTT support you need a development board with 8 MB flash or more. See the `config.ini` configuration files in [./config](./config) folder. | Some impressions | | | - | - | From 7e3455abdffaf9020cf19dea07dd75b4b48fb541 Mon Sep 17 00:00:00 2001 From: Haju Schulz Date: Sun, 30 Jul 2023 13:35:56 +0200 Subject: [PATCH 020/105] s3: Switch to official eSPI_TFT library S3 support meanwhile part of bodmer/TFT_eSPI. Remove temporary patched versiom which had compile time warnings. --- config/display.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/display.ini b/config/display.ini index 5e36a715..c41378a7 100644 --- a/config/display.ini +++ b/config/display.ini @@ -109,7 +109,7 @@ build_flags = lib_deps_builtin = HalTftDisplay lib_deps_external = - https://github.com/nhjschulz/TFT_eSPI + bodmer/TFT_eSPI @ ~2.5.31 lib_ignore_builtin = HalLedMatrix lib_ignore_external = From 0fd73be76c8528cfcdcdc62d88089123fb6323c7 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Mon, 31 Jul 2023 19:16:12 +0200 Subject: [PATCH 021/105] Multi-line comment fixed. --- lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp b/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp index 4776f7e2..d6fbb55c 100644 --- a/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp +++ b/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp @@ -341,15 +341,15 @@ void GrabViaRestPlugin::process(bool isConnected) else { /* If the connection is lost, stop periodically requesting information - * via REST API. - */ + * via REST API. + */ if (false == isConnected) { m_requestTimer.stop(); } /* Network connection is available and next request may be necessary for - * information update. - */ + * information update. + */ else if (true == m_requestTimer.isTimeout()) { if (false == startHttpRequest()) From 2cfbab595734f074f32d568e8f42316fa704c311 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Wed, 2 Aug 2023 20:43:19 +0200 Subject: [PATCH 022/105] Bugfix: There were two problems. The first was that configTzTime() doesn't copy the NTP server address. It will access the given address during runtime, therefore the string with the NTP server address needs to be "static". The second problem was that the CountdownPlugin didn't update periodically in the process() routine. #142 --- lib/CountdownPlugin/src/CountdownPlugin.cpp | 12 +- lib/CountdownPlugin/src/CountdownPlugin.h | 11 +- src/Hal/ClockDrv.cpp | 131 +++++++++++--------- src/Hal/ClockDrv.h | 9 ++ src/StateMachine/InitState.h | 3 +- 5 files changed, 97 insertions(+), 69 deletions(-) diff --git a/lib/CountdownPlugin/src/CountdownPlugin.cpp b/lib/CountdownPlugin/src/CountdownPlugin.cpp index 0347c031..a93db2f2 100644 --- a/lib/CountdownPlugin/src/CountdownPlugin.cpp +++ b/lib/CountdownPlugin/src/CountdownPlugin.cpp @@ -221,7 +221,7 @@ void CountdownPlugin::start(uint16_t width, uint16_t height) m_cfgReloadTimer.start(CFG_RELOAD_PERIOD); - calculateDifferenceInDays(); + calculateRemainingDays(); } void CountdownPlugin::stop() @@ -279,6 +279,8 @@ void CountdownPlugin::process(bool isConnected) { ; } + + calculateRemainingDays(); } void CountdownPlugin::update(YAGfx& gfx) @@ -355,8 +357,6 @@ bool CountdownPlugin::setConfiguration(JsonObjectConst& jsonCfg) m_targetDateInformation.plural = jsonDescPlural.as(); m_targetDateInformation.singular = jsonDescSingular.as(); - calculateDifferenceInDays(); - m_hasTopicChanged = true; status = true; @@ -365,11 +365,11 @@ bool CountdownPlugin::setConfiguration(JsonObjectConst& jsonCfg) return status; } -void CountdownPlugin::calculateDifferenceInDays() +void CountdownPlugin::calculateRemainingDays() { tm currentTime; - if (false != ClockDrv::getInstance().getTime(currentTime)) + if (true == ClockDrv::getInstance().getTime(currentTime)) { uint32_t currentDateInDays = 0U; uint32_t targetDateInDays = 0U; @@ -387,7 +387,7 @@ void CountdownPlugin::calculateDifferenceInDays() numberOfDays = targetDateInDays - currentDateInDays; - if( numberOfDays > 0) + if (0 < numberOfDays) { char remaining[10] = ""; diff --git a/lib/CountdownPlugin/src/CountdownPlugin.h b/lib/CountdownPlugin/src/CountdownPlugin.h index e7eabb74..5c2508cb 100644 --- a/lib/CountdownPlugin/src/CountdownPlugin.h +++ b/lib/CountdownPlugin/src/CountdownPlugin.h @@ -195,9 +195,9 @@ class CountdownPlugin : public Plugin, private PluginConfigFsHandler m_hasTopicChanged(false) { /* Example data, used to generate the very first configuration file. */ - m_targetDate.day = 29; - m_targetDate.month = 8; - m_targetDate.year = 2019; + m_targetDate.day = 1U; + m_targetDate.month = 8U; + m_targetDate.year = 2023U; m_targetDateInformation.plural = "DAYS"; m_targetDateInformation.singular = "DAY"; @@ -432,9 +432,10 @@ class CountdownPlugin : public Plugin, private PluginConfigFsHandler bool setConfiguration(JsonObjectConst& cfg) final; /** - * Calculates the difference between m_targetTime and m_currentTime in days. + * Calculates the remaining days between m_targetTime and m_currentTime in days and + * update m_remainingDays. */ - void calculateDifferenceInDays(void); + void calculateRemainingDays(void); /** * Counts the number of leap years. diff --git a/src/Hal/ClockDrv.cpp b/src/Hal/ClockDrv.cpp index cfb869b4..d0379b46 100644 --- a/src/Hal/ClockDrv.cpp +++ b/src/Hal/ClockDrv.cpp @@ -71,44 +71,49 @@ const char* ClockDrv::TZ_UTC = "UTC+0"; void ClockDrv::init(IRtc* rtc) { - String ntpServerAddress; - struct tm timeInfo = { 0 }; SettingsService& settings = SettingsService::getInstance(); char tzBuffer[TZ_MIN_SIZE]; - /* Get the GMT offset, daylight saving enabled/disabled and NTP server address from persistent memory. */ - if (false == settings.open(true)) - { - LOG_WARNING("Use default values for NTP request."); + /* Handle RTC */ + m_rtc = rtc; - m_timeZone = settings.getTimezone().getDefault(); - ntpServerAddress = settings.getNTPServerAddress().getDefault(); - } - else + if (nullptr == m_rtc) { - m_timeZone = settings.getTimezone().getValue(); - ntpServerAddress = settings.getNTPServerAddress().getValue(); - settings.close(); + LOG_INFO("No RTC is available."); } - - /* Initialize RTC use its time. */ - m_rtc = rtc; - - if (nullptr != m_rtc) + else { /* Check whether RTC is available and initialize it. */ if (false == m_rtc->begin()) { LOG_INFO("No RTC is available."); + m_rtc = nullptr; } else { LOG_INFO("RTC is available."); syncTimeByRtc(); - sntp_set_time_sync_notification_cb(sntpCallback); } } + /* Get the GMT offset, daylight saving enabled/disabled and NTP server address from persistent memory. */ + if (false == settings.open(true)) + { + LOG_WARNING("Use default values for NTP request."); + + m_timeZone = settings.getTimezone().getDefault(); + m_ntpServerAddress = settings.getNTPServerAddress().getDefault(); + } + else + { + m_timeZone = settings.getTimezone().getValue(); + m_ntpServerAddress = settings.getNTPServerAddress().getValue(); + settings.close(); + } + + sntp_set_time_sync_notification_cb(sntpCallback); + sntp_set_sync_interval(SYNC_TIME_BY_NTP_PERIOD); + /* Workaround part 1 to avoid memory leaks by calling setenv() of the newlib. * https://github.com/espressif/esp-idf/issues/3046 */ @@ -121,8 +126,11 @@ void ClockDrv::init(IRtc* rtc) * To modify the variable, set CONFIG_LWIP_SNTP_UPDATE_DELAY in project configuration. * https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/system_time.html * https://github.com/espressif/esp-idf/issues/4386 + * + * Important: The NTP server address is not copied by configTzTime(). It will access the + * string periodically, therefore its important to keep it as member variable! */ - configTzTime(tzBuffer, ntpServerAddress.c_str()); + configTzTime(tzBuffer, m_ntpServerAddress.c_str()); /* Workaround part 2 to avoid memory leaks by calling setenv() of the newlib. * https://github.com/espressif/esp-idf/issues/3046 @@ -241,47 +249,53 @@ void ClockDrv::setRtcByTime() void ClockDrv::syncTimeByRtc() { - bool sync = false; - - if (false == m_syncTimeByRtcTimer.isTimerRunning()) - { - m_syncTimeByRtcTimer.start(SYNC_TIME_BY_RTC_PERIOD); - sync = true; - } - else if (true == m_syncTimeByRtcTimer.isTimeout()) + if (nullptr != m_rtc) { - sync = true; - } + bool sync = false; - if (true == sync) - { - LOG_INFO("Sync time by RTC."); + if (false == m_syncTimeByRtcTimer.isTimerRunning()) + { + m_syncTimeByRtcTimer.start(SYNC_TIME_BY_RTC_PERIOD); + sync = true; + } + else if (true == m_syncTimeByRtcTimer.isTimeout()) + { + sync = true; + } + + if (true == sync) + { + LOG_INFO("Sync time by RTC."); - setTimeByRtc(); - m_syncTimeByRtcTimer.restart(); + setTimeByRtc(); + m_syncTimeByRtcTimer.restart(); + } } } void ClockDrv::syncRtcByTime() { - bool sync = false; - - if (false == m_syncRtcByNtpTimer.isTimerRunning()) - { - m_syncRtcByNtpTimer.start(SYNC_RTC_BY_TIME_PERIOD); - sync = true; - } - else if (true == m_syncRtcByNtpTimer.isTimeout()) + if (nullptr != m_rtc) { - sync = true; - } + bool sync = false; - if (true == sync) - { - LOG_INFO("Sync RTC by time."); + if (false == m_syncRtcByNtpTimer.isTimerRunning()) + { + m_syncRtcByNtpTimer.start(SYNC_RTC_BY_TIME_PERIOD); + sync = true; + } + else if (true == m_syncRtcByNtpTimer.isTimeout()) + { + sync = true; + } - setRtcByTime(); - m_syncRtcByNtpTimer.restart(); + if (true == sync) + { + LOG_INFO("Sync RTC by time."); + + setRtcByTime(); + m_syncRtcByNtpTimer.restart(); + } } } @@ -301,13 +315,16 @@ extern void sntpCallback(struct timeval *tv) (void)tv; - /* As long as updates from NTP are received, no synchronization from the RTC - * to the local timer shall be done. - */ - clockDrv.m_syncTimeByRtcTimer.restart(); - - /* Synchronize RTC by time. */ - clockDrv.syncRtcByTime(); + if (nullptr != clockDrv.m_rtc) + { + /* As long as updates from NTP are received, no synchronization from the RTC + * to the local timer shall be done. + */ + clockDrv.m_syncTimeByRtcTimer.restart(); + + /* Synchronize RTC by time. */ + clockDrv.syncRtcByTime(); + } } /****************************************************************************** diff --git a/src/Hal/ClockDrv.h b/src/Hal/ClockDrv.h index 469fd322..23a49289 100644 --- a/src/Hal/ClockDrv.h +++ b/src/Hal/ClockDrv.h @@ -123,6 +123,11 @@ class ClockDrv */ static const char* TZ_UTC; + /** + * Period for time synchronization by NTP in ms. + */ + static const uint32_t SYNC_TIME_BY_NTP_PERIOD = SIMPLE_TIMER_HOURS(12U); + /** * Period for time synchronization by RTC in ms. */ @@ -142,6 +147,9 @@ class ClockDrv /** newlib's internal timezone buffer. */ char* m_internalTimeZoneBuffer; + /** NTP server address, used by sntp. Don't remove it! */ + String m_ntpServerAddress; + /** Real time clock */ IRtc* m_rtc; @@ -158,6 +166,7 @@ class ClockDrv m_isClockDrvInitialized(false), m_timeZone(TZ_UTC), m_internalTimeZoneBuffer(nullptr), + m_ntpServerAddress(), m_rtc(nullptr), m_syncTimeByRtcTimer(), m_syncRtcByNtpTimer() diff --git a/src/StateMachine/InitState.h b/src/StateMachine/InitState.h index afd3e833..cf603cc6 100644 --- a/src/StateMachine/InitState.h +++ b/src/StateMachine/InitState.h @@ -129,7 +129,8 @@ class InitState : public AbstractState InitState() : m_isQuiet(false), m_isApModeRequested(false), - m_timer() + m_timer(), + m_rtcDrv() { } From 8c06094f7c8a87e514b294cd8ab65ecf4fa557d6 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Wed, 2 Aug 2023 20:46:25 +0200 Subject: [PATCH 023/105] Library bodmer/TFT_eSPI updated to ~2.5.31 --- config/display.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/display.ini b/config/display.ini index c41378a7..e7294a25 100644 --- a/config/display.ini +++ b/config/display.ini @@ -62,7 +62,7 @@ build_flags = lib_deps_builtin = HalTftDisplay lib_deps_external = - bodmer/TFT_eSPI @ ~2.5.30 + bodmer/TFT_eSPI @ ~2.5.31 lib_ignore_builtin = HalLedMatrix lib_ignore_external = @@ -148,7 +148,7 @@ build_flags = lib_deps_builtin = HalTftDisplay lib_deps_external = - bodmer/TFT_eSPI @ ~2.5.30 + bodmer/TFT_eSPI @ ~2.5.31 lib_ignore_builtin = HalLedMatrix lib_ignore_external = From 064179ef9c260e0d2d0e133848fcb6a476bdc23c Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 10 Sep 2023 10:08:24 +0200 Subject: [PATCH 024/105] Missing word added in README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ed06094..01b6baa5 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ After configuration, restart again and voila, PIXELIX will be available in your For changing whats displayed, go to its web interface. Use the same credentials than for the captive portal in variant 1. In the "Display" page you can change it according to your needs. # User Interface -* Pixelix can be controlled with buttons. Most of the development are supported with just one user button. +* Pixelix can be controlled with buttons. Most of the development boards are supported with just one user button. * One button control: * 1 short pulse: Activates the next slot. * 2 short pulses: Activates the previous slot. From bd1d6b881b5ec946ec3d027ef38a08299908540f Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 10 Sep 2023 10:09:34 +0200 Subject: [PATCH 025/105] Platform espressif32 upgraded to 6.4.0 Library ArduinoJson upgraded to 6.21.3 --- config/mcu.ini | 4 ++-- platformio.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/mcu.ini b/config/mcu.ini index 95afebc2..bab3b3ea 100644 --- a/config/mcu.ini +++ b/config/mcu.ini @@ -2,7 +2,7 @@ ; MCU ESP32 configuration ; ******************************************************************************** [mcu:esp32] -platform = espressif32 @ ~6.3.0 +platform = espressif32 @ ~6.4.0 framework = arduino build_flags = -I./src/Common @@ -21,7 +21,7 @@ build_flags = -D PIO_ENV="$PIOENV" -Wl,-Map,firmware.map lib_deps_external = - bblanchon/ArduinoJson @ ~6.21.2 + bblanchon/ArduinoJson @ ~6.21.3 bblanchon/StreamUtils @ ~1.7.3 muwerk/mufonts @ ~0.2.0 https://github.com/BlueAndi/AsyncTCPSock diff --git a/platformio.ini b/platformio.ini index acc15e3f..ccfadeb3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -184,7 +184,7 @@ build_flags = -D NATIVE lib_compat_mode = off ; The muwerk/mufonts require Arduino framework. lib_deps = - bblanchon/ArduinoJson @ ~6.21.2 + bblanchon/ArduinoJson @ ~6.21.3 muwerk/mufonts @ ~0.2.0 lib_ignore = Sensors From c6104bf339cc2f182bb93984a8b8f36fa1c2c40d Mon Sep 17 00:00:00 2001 From: Nikey Date: Thu, 14 Sep 2023 17:58:36 +0200 Subject: [PATCH 026/105] Fix mqtt login with a password Fix the false reading from the config --- lib/MqttService/src/MqttService.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/MqttService/src/MqttService.cpp b/lib/MqttService/src/MqttService.cpp index ad41b48f..1c7b7b1a 100644 --- a/lib/MqttService/src/MqttService.cpp +++ b/lib/MqttService/src/MqttService.cpp @@ -438,7 +438,7 @@ void MqttService::parseMqttBrokerUrl(const String& mqttBrokerUrl) /* Password not empty? */ if (idx > (dividerIdx + 1)) { - m_password = m_url.substring(dividerIdx + 1, idx - dividerIdx - 1); + m_password = m_url.substring(dividerIdx + 1, idx); } } From cbaf66e215430aa4bd8ca146e6c16ab8fa01ddc3 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Thu, 14 Sep 2023 21:45:57 +0200 Subject: [PATCH 027/105] URLs to the configuration examples and the filter description added. #140 --- doc/PLUGINS.md | 2 ++ lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html | 3 ++- lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index 6222a816..3e107d48 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -47,10 +47,12 @@ The generic plugins allow the user to control the different UI elements describe ## GrabViaMqttPlugin The plugin can grab information in JSON format via MQTT and shows it on the display. Each part can be set separately via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/GrabViaMqttPlugin). +[Configuration examples](./grabConfigs/mqtt/) may help to configure. ## GrabViaRestPlugin The plugin can grab information in JSON format via REST API and shows it on the display. Each part can be set separately via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/GrabViaRestPlugin). +[Configuration examples](./grabConfigs/rest/) may help to configure. ## IconTextPlugin The IconTextPlugin shows an icon on left side, text on right side. If no text is set, the plugin will be skipped in the slot.\ diff --git a/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html b/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html index bea11250..5d69d81c 100644 --- a/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html +++ b/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html @@ -45,12 +45,13 @@

Set configuration

  • PLUGIN-UID: The plugin unique id.
  • PLUGIN-ALIAS: The plugin alias name.
  • PATH: MQTT topic path.
  • -
  • FILTER: Filter to identify the value in the JSON response.
  • +
  • FILTER: Filter to identify the value in the JSON response. See the details here: ArduinoJson 6.15: Filtering done right
  • ICON-PATH: Filename of the icon including the path.
  • FORMAT: Format specifier, e.g. "%s" for strings or "%f" for numbers.
  • MULTIPLIER: Number which is multiplied with a number value. Not used for string values.
  • OFFSET: Number which is added to a number value. Not used for string values.
  • +

    Configuration examples may help to configure.

    Configuration

    User

    diff --git a/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html b/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html index d016a549..a5931644 100644 --- a/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html +++ b/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html @@ -46,12 +46,13 @@

    Set configuration

  • PLUGIN-ALIAS: The plugin alias name.
  • METHOD: HTTP method, supported are "GET" and "POST".
  • URL: REST API URL.
  • -
  • FILTER: Filter to identify the value in the JSON response.
  • +
  • FILTER: Filter to identify the value in the JSON response. See the details here: ArduinoJson 6.15: Filtering done right
  • ICON-PATH: Filename of the icon including the path.
  • FORMAT: Format specifier, e.g. "%s" for strings or "%f" for numbers.
  • MULTIPLIER: Number which is multiplied with a number value. Not used for string values.
  • OFFSET: Number which is added to a number value. Not used for string values.
  • +

    Configuration examples may help to configure.

    Configuration

    User

    From 86cfb1d58569f9bb707d37cc1f1bb097205428bf Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Thu, 14 Sep 2023 22:20:19 +0200 Subject: [PATCH 028/105] For REST API compliance, the path "/config" changed to "/grabConfig". This change is related to GrabViaMqttPlugin and GrabViaRestPlugin. --- doc/PLUGINS.md | 3 +-- lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp | 2 +- lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html | 10 +++++----- lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp | 2 +- lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html | 10 +++++----- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index 3e107d48..23d2c8e2 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -42,11 +42,10 @@ Each plugin is identified by its unique UID. * [Contribution](#contribution) # Generic plugins -The generic plugins allow the user to control the different UI elements described in the plugin name via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.3.0). +The generic plugins allow the user to control the different UI elements described in the plugin name via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0). ## GrabViaMqttPlugin The plugin can grab information in JSON format via MQTT and shows it on the display. -Each part can be set separately via the [REST API](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0#/GrabViaMqttPlugin). [Configuration examples](./grabConfigs/mqtt/) may help to configure. ## GrabViaRestPlugin diff --git a/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp b/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp index 19d08395..96de92f1 100644 --- a/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp +++ b/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp @@ -59,7 +59,7 @@ *****************************************************************************/ /* Initialize plugin topic. */ -const char* GrabViaMqttPlugin::TOPIC_CONFIG = "/config"; +const char* GrabViaMqttPlugin::TOPIC_CONFIG = "/grabConfig"; /****************************************************************************** * Public Methods diff --git a/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html b/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html index 5d69d81c..39f8a67c 100644 --- a/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html +++ b/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html @@ -37,10 +37,10 @@

    GrabViaMqttPlugin

    The plugin can grab information in JSON format via MQTT and shows it on the display.

    REST API

    Get configuration

    -
    GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/config
    +
    GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/grabConfig

    Set configuration

    -
    POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/config?path=<PATH>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
    -
    POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/config?path=<PATH>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
    +
    POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/grabConfig?path=<PATH>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
    +
    POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/grabConfig?path=<PATH>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
    • PLUGIN-UID: The plugin unique id.
    • PLUGIN-ALIAS: The plugin alias name.
    • @@ -164,7 +164,7 @@

      User

      disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/config", + url: "/rest/api/v1/display/uid/" + pluginUid + "/grabConfig", isJsonResponse: true }).then(function(rsp) { $("#path").val(rsp.data.path); @@ -185,7 +185,7 @@

      User

      return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/config", + url: "/rest/api/v1/display/uid/" + pluginUid + "/grabConfig", isJsonResponse: true, parameter: { path: $("#path").val(), diff --git a/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp b/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp index d6fbb55c..6091b13d 100644 --- a/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp +++ b/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp @@ -58,7 +58,7 @@ *****************************************************************************/ /* Initialize plugin topic. */ -const char* GrabViaRestPlugin::TOPIC_CONFIG = "/config"; +const char* GrabViaRestPlugin::TOPIC_CONFIG = "/grabConfig"; /****************************************************************************** * Public Methods diff --git a/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html b/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html index a5931644..0257709f 100644 --- a/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html +++ b/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html @@ -37,10 +37,10 @@

      GrabViaRestPlugin

      The plugin can grab information in JSON format via REST API and shows it on the display.

      REST API

      Get configuration

      -
      GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/config
      +
      GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/grabConfig

      Set configuration

      -
      POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/config?method=<METHOD>&url=<URL>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
      -
      POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/config?method=<METHOD>&url=<URL>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
      +
      POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/grabConfig?method=<METHOD>&url=<URL>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
      +
      POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/grabConfig?method=<METHOD>&url=<URL>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
      • PLUGIN-UID: The plugin unique id.
      • PLUGIN-ALIAS: The plugin alias name.
      • @@ -169,7 +169,7 @@

        User

        disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/config", + url: "/rest/api/v1/display/uid/" + pluginUid + "/grabConfig", isJsonResponse: true }).then(function(rsp) { $("#method").val(rsp.data.method); @@ -191,7 +191,7 @@

        User

        return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/config", + url: "/rest/api/v1/display/uid/" + pluginUid + "/grabConfig", isJsonResponse: true, parameter: { method: $("#method").val(), From b5d29207f46ea09e7d332a5b0328f78a578fee06 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Thu, 14 Sep 2023 22:28:40 +0200 Subject: [PATCH 029/105] Readme for the grab configuration examples added. --- doc/grabConfigs/README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 doc/grabConfigs/README.md diff --git a/doc/grabConfigs/README.md b/doc/grabConfigs/README.md new file mode 100644 index 00000000..0ed1d05e --- /dev/null +++ b/doc/grabConfigs/README.md @@ -0,0 +1,34 @@ +# PIXELIX +![PIXELIX](../images/LogoBlack.png) + +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](http://choosealicense.com/licenses/mit/) + +# Configurations for the GrabViaMqttPlugin and GrabViaRestPlugin + +* [General](#general) +* [GrabViaMqttPlugin examples](#grabviamqttplugin-examples) +* [GrabViaRestPlugin examples](#grabviarestplugin-examples) +* [Issues, Ideas And Bugs](#issues-ideas-and-bugs) +* [License](#license) +* [Contribution](#contribution) + +# General +Please share your configuration for others by pull request or you can open an issue with your example. It will be added here to the list. + +# GrabViaMqttPlugin examples +* [Grodans Paradis AB outdoor temperature](./mqtt/grodansparadisOutdoorTemperature.json) on Ake's house south side. + +# GrabViaRestPlugin examples +* [Github repository stargazers](./rest/githubStargazers.json) +* [Shelly Plug S - power consumption](./rest/shellyPlugSPower.json) + +# Issues, Ideas And Bugs +If you have further ideas or you found some bugs, great! Create a [issue](https://github.com/BlueAndi/esp-rgb-led-matrix/issues) or if you are able and willing to fix it by yourself, clone the repository and create a pull request. + +# License +The whole source code is published under the [MIT license](http://choosealicense.com/licenses/mit/). +Consider the different licenses of the used third party libraries too! + +# Contribution +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, shall be licensed as above, without any +additional terms or conditions. From 854e3d74593b3b1c9a52a35d6f7984c5d33c9117 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Thu, 14 Sep 2023 22:31:15 +0200 Subject: [PATCH 030/105] Readme for the grab configuration examples added. --- doc/README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/doc/README.md b/doc/README.md index bc4f7020..681c4a3d 100644 --- a/doc/README.md +++ b/doc/README.md @@ -5,12 +5,12 @@ # Documentation -- [Requirements](#requirements) -- [Hardware](#hardware) -- [Software](#software) -- [Issues, Ideas And Bugs](#issues-ideas-and-bugs) -- [License](#license) -- [Contribution](#contribution) +* [Requirements](#requirements) +* [Hardware](#hardware) +* [Software](#software) +* [Issues, Ideas And Bugs](#issues-ideas-and-bugs) +* [License](#license) +* [Contribution](#contribution) # Requirements @@ -30,12 +30,13 @@ * [Software update](./config/SW-UPDATE.md) * [Plugins](PLUGINS.md) * [Plugin Development](PLUGIN-DEV.md) -* [REST API description](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.3.0) +* [REST API description](https://app.swaggerhub.com/apis/BlueAndi/Pixelix/1.4.0) * [MQTT](MQTT.md) * [Home Assistant REST Wrapper](HOMEASSISTANT.md) * [Websocket API description](WEBSOCKET.md) * [Sprite sheet](SPRITESHEET.md) * [Alternative icons](ICONS.md) +* [Configuration examples for GrabViaMqttPlugin and GrabViaRestPlugin.](./grabConfigs/README.md) # Issues, Ideas And Bugs If you have further ideas or you found some bugs, great! Create a [issue](https://github.com/BlueAndi/esp-rgb-led-matrix/issues) or if you are able and willing to fix it by yourself, clone the repository and create a pull request. From 5876bccfa206a288a58c91a62a05c84431cb2213 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Mon, 18 Sep 2023 21:00:31 +0200 Subject: [PATCH 031/105] Static check shall fail in case of medium or high priority errors. --- .github/workflows/Development.yml | 2 +- .github/workflows/main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Development.yml b/.github/workflows/Development.yml index 48aeb095..e18cd734 100644 --- a/.github/workflows/Development.yml +++ b/.github/workflows/Development.yml @@ -103,7 +103,7 @@ jobs: pip install --upgrade platformio - name: Perform static checks on ${{ matrix.environment }} - run: platformio check --environment ${{ matrix.environment }} + run: platformio check --environment ${{ matrix.environment }} --fail-on-defect=medium --fail-on-defect=high # Perform tests test: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 395ac5bb..c3580862 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -121,7 +121,7 @@ jobs: pip install --upgrade platformio - name: Perform static checks on ${{ matrix.environment }} - run: platformio check --environment ${{ matrix.environment }} + run: platformio check --environment ${{ matrix.environment }} --fail-on-defect=medium --fail-on-defect=high # Perform tests test: From 1ed609ffa2332cdf817dc0836675f78171af4b2a Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Mon, 16 Oct 2023 18:37:42 +0200 Subject: [PATCH 032/105] Bitmap and sprite sheet path can be set now directly. #133 --- config/configSmall.ini | 2 +- .../src/IconTextLampPlugin.cpp | 205 +++++++++--- .../src/IconTextLampPlugin.h | 52 ++- .../web/IconTextLampPlugin.html | 105 ++++++- lib/IconTextPlugin/src/IconTextPlugin.cpp | 208 +++++++++--- lib/IconTextPlugin/src/IconTextPlugin.h | 52 ++- lib/IconTextPlugin/web/IconTextPlugin.html | 108 +++++-- lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp | 297 ++++++++++++------ lib/ThreeIconPlugin/src/ThreeIconPlugin.h | 71 ++++- lib/ThreeIconPlugin/web/ThreeIconPlugin.html | 134 ++++++-- 10 files changed, 988 insertions(+), 246 deletions(-) diff --git a/config/configSmall.ini b/config/configSmall.ini index 3986fa71..44942e92 100644 --- a/config/configSmall.ini +++ b/config/configSmall.ini @@ -24,7 +24,7 @@ lib_deps = ;GrabViaMqttPlugin @ ~0.1.0 # Requires MqttService GrabViaRestPlugin @ ~0.1.0 ;GruenbeckPlugin @ ~0.1.0 - IconTextLampPlugin @ ~0.1.0 + ;IconTextLampPlugin @ ~0.1.0 IconTextPlugin @ ~0.1.0 JustTextPlugin @ ~0.1.0 MatrixPlugin @ ~0.1.0 diff --git a/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp b/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp index dc6ae5c0..5aa38426 100644 --- a/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp +++ b/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp @@ -71,6 +71,9 @@ const char* IconTextLampPlugin::TOPIC_LAMP = "/lamp"; /* Initialize plugin topic. */ const char* IconTextLampPlugin::TOPIC_ICON = "/bitmap"; +/* Initialize plugin topic. */ +const char* IconTextLampPlugin::TOPIC_SPRITESHEET = "/spritesheet"; + /* Initialize bitmap image filename extension. */ const char* IconTextLampPlugin::FILE_EXT_BITMAP = ".bmp"; @@ -83,10 +86,11 @@ const char* IconTextLampPlugin::FILE_EXT_SPRITE_SHEET = ".sprite"; void IconTextLampPlugin::getTopics(JsonArray& topics) const { - uint8_t lampId = 0U; - JsonObject jsonText = topics.createNestedObject(); - JsonObject jsonIcon = topics.createNestedObject(); - JsonObject jsonLamps = topics.createNestedObject(); + uint8_t lampId = 0U; + JsonObject jsonText = topics.createNestedObject(); + JsonObject jsonIcon = topics.createNestedObject(); + JsonObject jsonSpriteSheet = topics.createNestedObject(); + JsonObject jsonLamps = topics.createNestedObject(); jsonText["name"] = TOPIC_TEXT; @@ -99,6 +103,9 @@ void IconTextLampPlugin::getTopics(JsonArray& topics) const jsonIcon["name"] = TOPIC_ICON; jsonIcon["access"] = "w"; /* Only icon upload is supported. */ + jsonSpriteSheet["name"] = TOPIC_SPRITESHEET; + jsonSpriteSheet["access"] = "w"; /* Only sprite sheet upload is supported. */ + jsonLamps["name"] = TOPIC_LAMPS; jsonLamps["access"] = "r"; /* Only read access allowed. */ @@ -114,9 +121,16 @@ bool IconTextLampPlugin::getTopic(const String& topic, JsonObject& value) const if (0U != topic.equals(TOPIC_TEXT)) { - String formattedText = getText(); + String formattedText = getText(); + String iconFullPath; + String spriteSheetFullPath; + + getIconFilePath(iconFullPath); + getSpriteSheetFilePath(spriteSheetFullPath); - value["text"] = formattedText; + value["text"] = formattedText; + value["iconFullPath"] = iconFullPath; + value["spriteSheetFullPath"] = spriteSheetFullPath; isSuccessful = true; } @@ -170,17 +184,49 @@ bool IconTextLampPlugin::setTopic(const String& topic, const JsonObject& value) if (0U != topic.equals(TOPIC_TEXT)) { String text; - JsonVariantConst jsonText = value["text"]; + JsonVariantConst jsonText = value["text"]; + JsonVariantConst jsonIconFullPath = value["iconFullPath"]; + JsonVariantConst jsonSpriteSheetFullPath = value["spriteSheetFullPath"]; if (false == jsonText.isNull()) { text = jsonText.as(); + + setText(text); + isSuccessful = true; } - if (true == isSuccessful) + if (false == jsonIconFullPath.isNull()) { - setText(text); + String iconFullPath = jsonIconFullPath.as(); + + if (true == iconFullPath.isEmpty()) + { + clearBitmap(); + } + else + { + loadBitmap(iconFullPath); + } + + isSuccessful = true; + } + + if (false == jsonSpriteSheetFullPath.isNull()) + { + String spriteSheetFullPath = jsonSpriteSheetFullPath.as(); + + if (true == spriteSheetFullPath.isEmpty()) + { + clearSpriteSheet(); + } + else + { + loadSpriteSheet(spriteSheetFullPath); + } + + isSuccessful = true; } } else if (0U != topic.startsWith(String(TOPIC_LAMP) + "/")) @@ -217,6 +263,7 @@ bool IconTextLampPlugin::setTopic(const String& topic, const JsonObject& value) { JsonVariantConst jsonFullPath = value["fullPath"]; + /* File upload? */ if (false == jsonFullPath.isNull()) { String fullPath = jsonFullPath.as(); @@ -224,6 +271,23 @@ bool IconTextLampPlugin::setTopic(const String& topic, const JsonObject& value) isSuccessful = loadBitmap(fullPath); } } + else if (0U != topic.equals(TOPIC_SPRITESHEET)) + { + JsonVariantConst jsonFullPath = value["fullPath"]; + + /* File upload? */ + if (false == jsonFullPath.isNull()) + { + String fullPath = jsonFullPath.as(); + + /* Don't use the return value, because there may be no bitmap + * available. + */ + (void)loadSpriteSheet(fullPath); + + isSuccessful = true; + } + } else { ; @@ -287,18 +351,20 @@ bool IconTextLampPlugin::isUploadAccepted(const String& topic, const String& src isAccepted = true; } + } + else if (0U != topic.equals(TOPIC_SPRITESHEET)) + { /* Accept upload of a sprite sheet file. */ - else if (0U != srcFilename.endsWith(FILE_EXT_SPRITE_SHEET)) + if (0U != srcFilename.endsWith(FILE_EXT_SPRITE_SHEET)) { dstFilename = getFileName(FILE_EXT_SPRITE_SHEET); isAccepted = true; } - else - { - /* Not accepted. */ - ; - } + } + else + { + ; } return isAccepted; @@ -317,13 +383,24 @@ void IconTextLampPlugin::start(uint16_t width, uint16_t height) m_iconCanvas.setPosAndSize(0, 0, ICON_WIDTH, ICON_HEIGHT); (void)m_iconCanvas.addWidget(m_bitmapWidget); - /* If there is already an icon in the filesystem, it will be loaded. - * First check whether it is a animated sprite sheet and if not, try - * to load just a bitmap image. + /* If there is an icon in the filesystem with the plugin UID as filename, + * it will be loaded. First check whether it is a animated sprite sheet + * and if not, try to load just a bitmap image. */ + m_iconPath.clear(); + m_spriteSheetPath.clear(); + if (false == m_bitmapWidget.loadSpriteSheet(FILESYSTEM, getFileName(FILE_EXT_SPRITE_SHEET), getFileName(FILE_EXT_BITMAP))) { - (void)m_bitmapWidget.load(FILESYSTEM, getFileName(FILE_EXT_BITMAP)); + if (true == m_bitmapWidget.load(FILESYSTEM, getFileName(FILE_EXT_BITMAP))) + { + m_iconPath = getFileName(FILE_EXT_BITMAP); + } + } + else + { + m_iconPath = getFileName(FILE_EXT_BITMAP); + m_spriteSheetPath = getFileName(FILE_EXT_SPRITE_SHEET); } /* The text canvas is left aligned to the icon canvas and aligned to the @@ -369,11 +446,13 @@ void IconTextLampPlugin::stop() { MutexGuard guard(m_mutex); + /* Remove icon which is specific for the plugin instance. */ if (false != FILESYSTEM.remove(getFileName(FILE_EXT_BITMAP))) { LOG_INFO("File %s removed", getFileName(FILE_EXT_BITMAP).c_str()); } + /* Remove spritesheet which is specific for the plugin instance. */ if (false != FILESYSTEM.remove(getFileName(FILE_EXT_SPRITE_SHEET))) { LOG_INFO("File %s removed", getFileName(FILE_EXT_SPRITE_SHEET).c_str()); @@ -417,36 +496,90 @@ bool IconTextLampPlugin::loadBitmap(const String& filename) bool status = false; MutexGuard guard(m_mutex); - if (0U != filename.endsWith(FILE_EXT_BITMAP)) + if (m_iconPath != filename) { - status = m_bitmapWidget.load(FILESYSTEM, filename); + m_iconPath = filename; - /* Ensure that only the bitmap image file exists in the filesystem, - * otherwise after a restart, the obsolete sprite sheet will - * be loaded. - */ - if (true == status) - { - (void)FILESYSTEM.remove(getFileName(FILE_EXT_SPRITE_SHEET)); - } + m_hasTopicTextChanged = true; + } + + if (false == m_spriteSheetPath.isEmpty()) + { + status = m_bitmapWidget.loadSpriteSheet(FILESYSTEM, m_iconPath, m_spriteSheetPath); } - else if (0U != filename.endsWith(FILE_EXT_SPRITE_SHEET)) + + if (false == status) { - String bmpFilename = filename; + status = m_bitmapWidget.load(FILESYSTEM, m_iconPath); + } + + return status; +} + +bool IconTextLampPlugin::loadSpriteSheet(const String& filename) +{ + bool status = false; + MutexGuard guard(m_mutex); - bmpFilename.replace(FILE_EXT_SPRITE_SHEET, FILE_EXT_BITMAP); + if (m_spriteSheetPath != filename) + { + m_spriteSheetPath = filename; - status = m_bitmapWidget.loadSpriteSheet(FILESYSTEM, filename, bmpFilename); + m_hasTopicTextChanged = true; } - else + + if (false == m_iconPath.isEmpty()) { - /* Not supported. */ - ; + status = m_bitmapWidget.loadSpriteSheet(FILESYSTEM, m_iconPath, m_spriteSheetPath); } return status; } +void IconTextLampPlugin::clearBitmap() +{ + MutexGuard guard(m_mutex); + + if (false == m_iconPath.isEmpty()) + { + m_iconPath.clear(); + m_bitmapWidget.clear(ColorDef::BLACK); + + m_hasTopicTextChanged = true; + } +} + +void IconTextLampPlugin::clearSpriteSheet() +{ + MutexGuard guard(m_mutex); + + if (false == m_spriteSheetPath.isEmpty()) + { + m_spriteSheetPath.clear(); + + m_hasTopicTextChanged = true; + } + + if (false == m_iconPath.isEmpty()) + { + (void)m_bitmapWidget.load(FILESYSTEM, m_iconPath); + } +} + +void IconTextLampPlugin::getIconFilePath(String& fullPath) const +{ + MutexGuard guard(m_mutex); + + fullPath = m_iconPath; +} + +void IconTextLampPlugin::getSpriteSheetFilePath(String& fullPath) const +{ + MutexGuard guard(m_mutex); + + fullPath = m_spriteSheetPath; +} + bool IconTextLampPlugin::getLamp(uint8_t lampId) const { bool lampState = false; diff --git a/lib/IconTextLampPlugin/src/IconTextLampPlugin.h b/lib/IconTextLampPlugin/src/IconTextLampPlugin.h index 7c9ae158..21a40c9e 100644 --- a/lib/IconTextLampPlugin/src/IconTextLampPlugin.h +++ b/lib/IconTextLampPlugin/src/IconTextLampPlugin.h @@ -83,6 +83,8 @@ class IconTextLampPlugin : public Plugin m_lampCanvas(), m_bitmapWidget(), m_textWidget(), + m_iconPath(), + m_spriteSheetPath(), m_lampWidgets(), m_mutex(), m_hasTopicTextChanged(false), @@ -230,18 +232,49 @@ class IconTextLampPlugin : public Plugin void setText(const String& formatText); /** - * Load bitmap image / sprite sheet from filesystem. - * If a bitmap image is loaded, it will remove a corresponding sprite - * sheet file from filesystem. - * If a sprite sheet is loaded, it will load the texture file from - * filesystem. This assumes that the texture file was uploaded before! + * Load bitmap image from filesystem. If a sprite sheet is available, the + * bitmap will be automatically used as texture for animation. * - * @param[in] filename Bitmap image / Sprite sheet filename + * @param[in] filename Bitmap image filename * * @return If successul, it will return true otherwise false. */ bool loadBitmap(const String& filename); + /** + * Load sprite sheet from filesystem. If a bitmap is available, it will + * be automatically used as texture for animation. + * + * @param[in] filename Sprite sheet filename + * + * @return If successul, it will return true otherwise false. + */ + bool loadSpriteSheet(const String& filename); + + /** + * Clear bitmap icon. + */ + void clearBitmap(); + + /** + * Clear sprite sheet. + */ + void clearSpriteSheet(); + + /** + * Get icon file path. + * + * @param[out] Path to icon file. + */ + void getIconFilePath(String& fullPath) const; + + /** + * Get sprite sheet file path. + * + * @param[out] Path to sprite sheet file. + */ + void getSpriteSheetFilePath(String& fullPath) const; + /** * Get lamp state (true = on / false = off). * @@ -281,6 +314,11 @@ class IconTextLampPlugin : public Plugin */ static const char* TOPIC_ICON; + /** + * Plugin topic, used for parameter exchange. + */ + static const char* TOPIC_SPRITESHEET; + /** * Icon width in pixels. */ @@ -311,6 +349,8 @@ class IconTextLampPlugin : public Plugin WidgetGroup m_lampCanvas; /**< Canvas used for the lamp widget. */ BitmapWidget m_bitmapWidget; /**< Bitmap widget, used to show the icon. */ TextWidget m_textWidget; /**< Text widget, used for showing the text. */ + String m_iconPath; /**< Full path to icon. */ + String m_spriteSheetPath; /**< Full path to spritesheet. */ LampWidget m_lampWidgets[MAX_LAMPS]; /**< Lamp widgets, used to signal different things. */ mutable MutexRecursive m_mutex; /**< Mutex to protect against concurrent access. */ bool m_hasTopicTextChanged; /**< Has the topic text content changed? */ diff --git a/lib/IconTextLampPlugin/web/IconTextLampPlugin.html b/lib/IconTextLampPlugin/web/IconTextLampPlugin.html index a7cc80ce..58a95383 100644 --- a/lib/IconTextLampPlugin/web/IconTextLampPlugin.html +++ b/lib/IconTextLampPlugin/web/IconTextLampPlugin.html @@ -57,20 +57,29 @@

        Get text

      • PLUGIN-ALIAS: The plugin alias name.

      Set text

      -
      POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/text?text=<TEXT>
      -
      POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/text?text=<TEXT>
      +
      POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
      +
      POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
      • PLUGIN-UID: The plugin unique id.
      • PLUGIN-ALIAS: The plugin alias name.
      • TEXT: The text to show on the display.
      • +
      • ICON-FULL-PATH: Full path to the icon.
      • +
      • SPRITESHEET-FULL-PATH: Full path to the sprite sheet.
      -

      Set icon

      +

      Upload icon

      POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/bitmap
      POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/bitmap
      • PLUGIN-UID: The plugin unique id.
      • PLUGIN-ALIAS: The plugin alias name.
      +

      Upload sprite sheet

      +
      POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/spritesheet
      +
      POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/spritesheet
      +
        +
      • PLUGIN-UID: The plugin unique id.
      • +
      • PLUGIN-ALIAS: The plugin alias name.
      • +

      Get all lamp states

      GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/lamps
      GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/lamps
      @@ -89,7 +98,7 @@

      Set lamp state

    Configuration

    Icon

    - +
    +

    Sprite sheet

    +
    +
    + + +
    +
    + + +
    + +

    Text

    -
    +
    -
    - + +
    +
    + + +
    +
    + +
    -
    +

    Lamp

    @@ -169,12 +199,14 @@

    Lamp

    function enableUI() { utils.enableForm("myFormIcon", true); + utils.enableForm("myFormSpriteSheet", true); utils.enableForm("myFormText", true); utils.enableForm("myFormLamp", true); } function disableUI() { utils.enableForm("myFormIcon", false); + utils.enableForm("myFormSpriteSheet", false); utils.enableForm("myFormText", false); utils.enableForm("myFormLamp", false); } @@ -222,7 +254,7 @@

    Lamp

    }); }; - function setIcon(pluginUid, file) { + function uploadIcon(pluginUid, file) { disableUI(); return utils.makeRequest({ @@ -236,6 +268,30 @@

    Lamp

    "X-File-Size": file.size } }).then(function(rsp) { + updateForms(); + alert("Ok."); + }).catch(function(rsp) { + alert("Failed."); + }).finally(function() { + enableUI(); + }); + } + + function uploadSpriteSheet(pluginUid, file) { + disableUI(); + + return utils.makeRequest({ + method: "POST", + url: "/rest/api/v1/display/uid/" + pluginUid + "/spritesheet", + isJsonResponse: true, + parameter: { + file: file + }, + headers: { + "X-File-Size": file.size + } + }).then(function(rsp) { + updateForms(); alert("Ok."); }).catch(function(rsp) { alert("Failed."); @@ -244,7 +300,7 @@

    Lamp

    }); } - function getText(pluginUid, justTextId) { + function getText(pluginUid, justTextId, iconFullPathId, spriteSheetFullPathId) { disableUI(); return utils.makeRequest({ method: "GET", @@ -252,8 +308,12 @@

    Lamp

    isJsonResponse: true }).then(function(rsp) { var justTextInput = document.getElementById(justTextId); + var iconFullPathInput = document.getElementById(iconFullPathId); + var spriteSheetFullPathInput = document.getElementById(spriteSheetFullPathId); justTextInput.value = rsp.data.text; + iconFullPathInput.value = rsp.data.iconFullPath; + spriteSheetFullPathInput.value = rsp.data.spriteSheetFullPath; }).catch(function(rsp) { alert("Internal error."); @@ -262,7 +322,7 @@

    Lamp

    }); } - function setText(pluginUid, justText) { + function setText(pluginUid, justText, iconFullPath, spriteSheetFullPath) { disableUI(); return utils.makeRequest({ @@ -270,7 +330,9 @@

    Lamp

    url: "/rest/api/v1/display/uid/" + pluginUid + "/text", isJsonResponse: true, parameter: { - text: justText + text: justText, + iconFullPath: iconFullPath, + spriteSheetFullPath: spriteSheetFullPath } }).then(function(rsp) { alert("Ok."); @@ -322,6 +384,20 @@

    Lamp

    }); } + function updateForms() { + var selectText = document.getElementById("pluginUidText"); + + if (0 <= selectText.selectedIndex) { + + return getText( + selectText.options[selectText.selectedIndex].value, + "justText", + "iconFullPath", + "spriteSheetFullPath" + ); + } + } + $(document).ready(function() { menu.addSubMenu(menu.data, "Plugins", pluginSubMenu); menu.create("menu", menu.data); @@ -339,10 +415,7 @@

    Lamp

    if (0 < cnt) { - return getText( - selectText.options[selectText.selectedIndex].value, - "justText" - ).then(function() { + return updateForms().then(function() { return getLamp( selectLamp.options[selectLamp.selectedIndex].value, selectLampId.options[selectLampId.selectedIndex].value, diff --git a/lib/IconTextPlugin/src/IconTextPlugin.cpp b/lib/IconTextPlugin/src/IconTextPlugin.cpp index a47eb6a4..03fefd67 100644 --- a/lib/IconTextPlugin/src/IconTextPlugin.cpp +++ b/lib/IconTextPlugin/src/IconTextPlugin.cpp @@ -64,6 +64,9 @@ const char* IconTextPlugin::TOPIC_TEXT = "/text"; /* Initialize plugin topic. */ const char* IconTextPlugin::TOPIC_ICON = "/bitmap"; +/* Initialize plugin topic. */ +const char* IconTextPlugin::TOPIC_SPRITESHEET = "/spritesheet"; + /* Initialize bitmap image filename extension. */ const char* IconTextPlugin::FILE_EXT_BITMAP = ".bmp"; @@ -90,8 +93,9 @@ bool IconTextPlugin::isEnabled() const void IconTextPlugin::getTopics(JsonArray& topics) const { - JsonObject jsonText = topics.createNestedObject(); - JsonObject jsonIcon = topics.createNestedObject(); + JsonObject jsonText = topics.createNestedObject(); + JsonObject jsonIcon = topics.createNestedObject(); + JsonObject jsonSpriteSheet = topics.createNestedObject(); jsonText["name"] = TOPIC_TEXT; @@ -101,8 +105,11 @@ void IconTextPlugin::getTopics(JsonArray& topics) const jsonText["ha"]["valueTemplate"] = "{{ value_json.text }}"; jsonText["ha"]["icon"] = "mdi:form-textbox"; - jsonIcon["name"] = TOPIC_ICON; - jsonIcon["access"] = "w"; /* Only icon upload is supported. */ + jsonIcon["name"] = TOPIC_ICON; + jsonIcon["access"] = "w"; /* Only icon upload is supported. */ + + jsonSpriteSheet["name"] = TOPIC_SPRITESHEET; + jsonSpriteSheet["access"] = "w"; /* Only sprite sheet upload is supported. */ } bool IconTextPlugin::getTopic(const String& topic, JsonObject& value) const @@ -111,9 +118,16 @@ bool IconTextPlugin::getTopic(const String& topic, JsonObject& value) const if (0U != topic.equals(TOPIC_TEXT)) { - String formattedText = getText(); + String formattedText = getText(); + String iconFullPath; + String spriteSheetFullPath; - value["text"] = formattedText; + getIconFilePath(iconFullPath); + getSpriteSheetFilePath(spriteSheetFullPath); + + value["text"] = formattedText; + value["iconFullPath"] = iconFullPath; + value["spriteSheetFullPath"] = spriteSheetFullPath; isSuccessful = true; } @@ -127,24 +141,56 @@ bool IconTextPlugin::setTopic(const String& topic, const JsonObject& value) if (0U != topic.equals(TOPIC_TEXT)) { - String text; - JsonVariantConst jsonText = value["text"]; + JsonVariantConst jsonText = value["text"]; + JsonVariantConst jsonIconFullPath = value["iconFullPath"]; + JsonVariantConst jsonSpriteSheetFullPath = value["spriteSheetFullPath"]; if (false == jsonText.isNull()) { - text = jsonText.as(); + String text = jsonText.as(); + + setText(text); + isSuccessful = true; } - if (true == isSuccessful) + if (false == jsonIconFullPath.isNull()) { - setText(text); + String iconFullPath = jsonIconFullPath.as(); + + if (true == iconFullPath.isEmpty()) + { + clearBitmap(); + } + else + { + loadBitmap(iconFullPath); + } + + isSuccessful = true; + } + + if (false == jsonSpriteSheetFullPath.isNull()) + { + String spriteSheetFullPath = jsonSpriteSheetFullPath.as(); + + if (true == spriteSheetFullPath.isEmpty()) + { + clearSpriteSheet(); + } + else + { + loadSpriteSheet(spriteSheetFullPath); + } + + isSuccessful = true; } } else if (0U != topic.equals(TOPIC_ICON)) { JsonVariantConst jsonFullPath = value["fullPath"]; + /* File upload? */ if (false == jsonFullPath.isNull()) { String fullPath = jsonFullPath.as(); @@ -152,6 +198,23 @@ bool IconTextPlugin::setTopic(const String& topic, const JsonObject& value) isSuccessful = loadBitmap(fullPath); } } + else if (0U != topic.equals(TOPIC_SPRITESHEET)) + { + JsonVariantConst jsonFullPath = value["fullPath"]; + + /* File upload? */ + if (false == jsonFullPath.isNull()) + { + String fullPath = jsonFullPath.as(); + + /* Don't use the return value, because there may be no bitmap + * available. + */ + (void)loadSpriteSheet(fullPath); + + isSuccessful = true; + } + } else { ; @@ -189,18 +252,20 @@ bool IconTextPlugin::isUploadAccepted(const String& topic, const String& srcFile isAccepted = true; } + } + else if (0U != topic.equals(TOPIC_SPRITESHEET)) + { /* Accept upload of a sprite sheet file. */ - else if (0U != srcFilename.endsWith(FILE_EXT_SPRITE_SHEET)) + if (0U != srcFilename.endsWith(FILE_EXT_SPRITE_SHEET)) { dstFilename = getFileName(FILE_EXT_SPRITE_SHEET); isAccepted = true; } - else - { - /* Not accepted. */ - ; - } + } + else + { + ; } return isAccepted; @@ -213,13 +278,24 @@ void IconTextPlugin::start(uint16_t width, uint16_t height) m_iconCanvas.setPosAndSize(0, 0, ICON_WIDTH, ICON_HEIGHT); (void)m_iconCanvas.addWidget(m_bitmapWidget); - /* If there is already an icon in the filesystem, it will be loaded. - * First check whether it is a animated sprite sheet and if not, try - * to load just a bitmap image. + /* If there is an icon in the filesystem with the plugin UID as filename, + * it will be loaded. First check whether it is a animated sprite sheet + * and if not, try to load just a bitmap image. */ + m_iconPath.clear(); + m_spriteSheetPath.clear(); + if (false == m_bitmapWidget.loadSpriteSheet(FILESYSTEM, getFileName(FILE_EXT_SPRITE_SHEET), getFileName(FILE_EXT_BITMAP))) { - (void)m_bitmapWidget.load(FILESYSTEM, getFileName(FILE_EXT_BITMAP)); + if (true == m_bitmapWidget.load(FILESYSTEM, getFileName(FILE_EXT_BITMAP))) + { + m_iconPath = getFileName(FILE_EXT_BITMAP); + } + } + else + { + m_iconPath = getFileName(FILE_EXT_BITMAP); + m_spriteSheetPath = getFileName(FILE_EXT_SPRITE_SHEET); } /* The text canvas is left aligned to the icon canvas and it spans over @@ -247,11 +323,13 @@ void IconTextPlugin::stop() { MutexGuard guard(m_mutex); + /* Remove icon which is specific for the plugin instance. */ if (false != FILESYSTEM.remove(getFileName(FILE_EXT_BITMAP))) { LOG_INFO("File %s removed", getFileName(FILE_EXT_BITMAP).c_str()); } + /* Remove spritesheet which is specific for the plugin instance. */ if (false != FILESYSTEM.remove(getFileName(FILE_EXT_SPRITE_SHEET))) { LOG_INFO("File %s removed", getFileName(FILE_EXT_SPRITE_SHEET).c_str()); @@ -294,36 +372,90 @@ bool IconTextPlugin::loadBitmap(const String& filename) bool status = false; MutexGuard guard(m_mutex); - if (0U != filename.endsWith(FILE_EXT_BITMAP)) + if (m_iconPath != filename) { - status = m_bitmapWidget.load(FILESYSTEM, filename); + m_iconPath = filename; - /* Ensure that only the bitmap image file exists in the filesystem, - * otherwise after a restart, the obsolete sprite sheet will - * be loaded. - */ - if (true == status) - { - (void)FILESYSTEM.remove(getFileName(FILE_EXT_SPRITE_SHEET)); - } + m_hasTopicChanged = true; } - else if (0U != filename.endsWith(FILE_EXT_SPRITE_SHEET)) + + if (false == m_spriteSheetPath.isEmpty()) { - String bmpFilename = filename; + status = m_bitmapWidget.loadSpriteSheet(FILESYSTEM, m_iconPath, m_spriteSheetPath); + } + + if (false == status) + { + status = m_bitmapWidget.load(FILESYSTEM, m_iconPath); + } + + return status; +} - bmpFilename.replace(FILE_EXT_SPRITE_SHEET, FILE_EXT_BITMAP); +bool IconTextPlugin::loadSpriteSheet(const String& filename) +{ + bool status = false; + MutexGuard guard(m_mutex); - status = m_bitmapWidget.loadSpriteSheet(FILESYSTEM, filename, bmpFilename); + if (m_spriteSheetPath != filename) + { + m_spriteSheetPath = filename; + + m_hasTopicChanged = true; } - else + + if (false == m_iconPath.isEmpty()) { - /* Not supported. */ - ; + status = m_bitmapWidget.loadSpriteSheet(FILESYSTEM, m_iconPath, m_spriteSheetPath); } return status; } +void IconTextPlugin::clearBitmap() +{ + MutexGuard guard(m_mutex); + + if (false == m_iconPath.isEmpty()) + { + m_iconPath.clear(); + m_bitmapWidget.clear(ColorDef::BLACK); + + m_hasTopicChanged = true; + } +} + +void IconTextPlugin::clearSpriteSheet() +{ + MutexGuard guard(m_mutex); + + if (false == m_spriteSheetPath.isEmpty()) + { + m_spriteSheetPath.clear(); + + m_hasTopicChanged = true; + } + + if (false == m_iconPath.isEmpty()) + { + (void)m_bitmapWidget.load(FILESYSTEM, m_iconPath); + } +} + +void IconTextPlugin::getIconFilePath(String& fullPath) const +{ + MutexGuard guard(m_mutex); + + fullPath = m_iconPath; +} + +void IconTextPlugin::getSpriteSheetFilePath(String& fullPath) const +{ + MutexGuard guard(m_mutex); + + fullPath = m_spriteSheetPath; +} + /****************************************************************************** * Protected Methods *****************************************************************************/ diff --git a/lib/IconTextPlugin/src/IconTextPlugin.h b/lib/IconTextPlugin/src/IconTextPlugin.h index e0221911..69a7d649 100644 --- a/lib/IconTextPlugin/src/IconTextPlugin.h +++ b/lib/IconTextPlugin/src/IconTextPlugin.h @@ -81,6 +81,8 @@ class IconTextPlugin : public Plugin m_iconCanvas(), m_bitmapWidget(), m_textWidget(), + m_iconPath(), + m_spriteSheetPath(), m_isUploadError(false), m_mutex(), m_hasTopicChanged(false) @@ -258,18 +260,49 @@ class IconTextPlugin : public Plugin void setText(const String& formatText); /** - * Load bitmap image / sprite sheet from filesystem. - * If a bitmap image is loaded, it will remove a corresponding sprite - * sheet file from filesystem. - * If a sprite sheet is loaded, it will load the texture file from - * filesystem. This assumes that the texture file was uploaded before! + * Load bitmap image from filesystem. If a sprite sheet is available, the + * bitmap will be automatically used as texture for animation. * - * @param[in] filename Bitmap image / Sprite sheet filename + * @param[in] filename Bitmap image filename * * @return If successul, it will return true otherwise false. */ bool loadBitmap(const String& filename); + /** + * Load sprite sheet from filesystem. If a bitmap is available, it will + * be automatically used as texture for animation. + * + * @param[in] filename Sprite sheet filename + * + * @return If successul, it will return true otherwise false. + */ + bool loadSpriteSheet(const String& filename); + + /** + * Clear bitmap icon. + */ + void clearBitmap(); + + /** + * Clear sprite sheet. + */ + void clearSpriteSheet(); + + /** + * Get icon file path. + * + * @param[out] Path to icon file. + */ + void getIconFilePath(String& fullPath) const; + + /** + * Get sprite sheet file path. + * + * @param[out] Path to sprite sheet file. + */ + void getSpriteSheetFilePath(String& fullPath) const; + private: /** @@ -282,6 +315,11 @@ class IconTextPlugin : public Plugin */ static const char* TOPIC_ICON; + /** + * Plugin topic, used for parameter exchange. + */ + static const char* TOPIC_SPRITESHEET; + /** * Icon width in pixels. */ @@ -307,6 +345,8 @@ class IconTextPlugin : public Plugin WidgetGroup m_iconCanvas; /**< Canvas used for the bitmap widget. */ BitmapWidget m_bitmapWidget; /**< Bitmap widget, used to show the icon. */ TextWidget m_textWidget; /**< Text widget, used for showing the text. */ + String m_iconPath; /**< Full path to icon. */ + String m_spriteSheetPath; /**< Full path to spritesheet. */ bool m_isUploadError; /**< Flag to signal a upload error. */ mutable MutexRecursive m_mutex; /**< Mutex to protect against concurrent access. */ bool m_hasTopicChanged; /**< Has the topic text content changed? */ diff --git a/lib/IconTextPlugin/web/IconTextPlugin.html b/lib/IconTextPlugin/web/IconTextPlugin.html index 36325d1f..a51835eb 100644 --- a/lib/IconTextPlugin/web/IconTextPlugin.html +++ b/lib/IconTextPlugin/web/IconTextPlugin.html @@ -57,23 +57,32 @@

    Get text

  • PLUGIN-ALIAS: The plugin alias name.
  • Set text

    -
    POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/text?text=<TEXT>
    -
    POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/text?text=<TEXT>
    +
    POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
    +
    POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
    • PLUGIN-UID: The plugin unique id.
    • PLUGIN-ALIAS: The plugin alias name.
    • TEXT: The text to show on the display.
    • +
    • ICON-FULL-PATH: Full path to the icon.
    • +
    • SPRITESHEET-FULL-PATH: Full path to the sprite sheet.
    -

    Set icon

    +

    Upload icon

    POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/bitmap
    POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/bitmap
    • PLUGIN-UID: The plugin unique id.
    • PLUGIN-ALIAS: The plugin alias name.
    +

    Upload sprite sheet

    +
    POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/spritesheet
    +
    POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/spritesheet
    +
      +
    • PLUGIN-UID: The plugin unique id.
    • +
    • PLUGIN-ALIAS: The plugin alias name.
    • +

    Configuration

    Icon

    - +
    +

    Sprite sheet

    +
    +
    + + +
    +
    + + +
    + +

    Text

    -
    +
    -
    - + +
    +
    + + +
    +
    + +
    @@ -128,11 +158,13 @@

    Text

    function enableUI() { utils.enableForm("myFormIcon", true); + utils.enableForm("myFormSpriteSheet", true); utils.enableForm("myFormText", true); } function disableUI() { utils.enableForm("myFormIcon", false); + utils.enableForm("myFormSpriteSheet", false); utils.enableForm("myFormText", false); } @@ -179,7 +211,7 @@

    Text

    }); }; - function setIcon(pluginUid, file) { + function uploadIcon(pluginUid, file) { disableUI(); return utils.makeRequest({ @@ -193,6 +225,7 @@

    Text

    "X-File-Size": file.size } }).then(function(rsp) { + updateForms(); alert("Ok."); }).catch(function(rsp) { alert("Failed."); @@ -201,7 +234,30 @@

    Text

    }); } - function getText(pluginUid, justTextId) { + function uploadSpriteSheet(pluginUid, file) { + disableUI(); + + return utils.makeRequest({ + method: "POST", + url: "/rest/api/v1/display/uid/" + pluginUid + "/spritesheet", + isJsonResponse: true, + parameter: { + file: file + }, + headers: { + "X-File-Size": file.size + } + }).then(function(rsp) { + updateForms(); + alert("Ok."); + }).catch(function(rsp) { + alert("Failed."); + }).finally(function() { + enableUI(); + }); + } + + function getText(pluginUid, justTextId, iconFullPathId, spriteSheetFullPathId) { disableUI(); return utils.makeRequest({ method: "GET", @@ -209,8 +265,12 @@

    Text

    isJsonResponse: true }).then(function(rsp) { var justTextInput = document.getElementById(justTextId); + var iconFullPathInput = document.getElementById(iconFullPathId); + var spriteSheetFullPathInput = document.getElementById(spriteSheetFullPathId); justTextInput.value = rsp.data.text; + iconFullPathInput.value = rsp.data.iconFullPath; + spriteSheetFullPathInput.value = rsp.data.spriteSheetFullPath; }).catch(function(rsp) { alert("Internal error."); @@ -219,7 +279,7 @@

    Text

    }); } - function setText(pluginUid, justText) { + function setText(pluginUid, justText, iconFullPath, spriteSheetFullPath) { disableUI(); return utils.makeRequest({ @@ -227,7 +287,9 @@

    Text

    url: "/rest/api/v1/display/uid/" + pluginUid + "/text", isJsonResponse: true, parameter: { - text: justText + text: justText, + iconFullPath: iconFullPath, + spriteSheetFullPath: spriteSheetFullPath } }).then(function(rsp) { alert("Ok."); @@ -238,6 +300,20 @@

    Text

    }); } + function updateForms() { + var selectText = document.getElementById("pluginUidText"); + + if (0 <= selectText.selectedIndex) { + + return getText( + selectText.options[selectText.selectedIndex].value, + "justText", + "iconFullPath", + "spriteSheetFullPath" + ); + } + } + $(document).ready(function() { menu.addSubMenu(menu.data, "Plugins", pluginSubMenu); menu.create("menu", menu.data); @@ -249,15 +325,7 @@

    Text

    /* Load all plugin instances. */ getPluginInstances().then(function(cnt) { - var selectText = document.getElementById("pluginUidText"); - - if (0 < cnt) { - - return getText( - selectText.options[selectText.selectedIndex].value, - "justText" - ); - } + return updateForms(); }); }); diff --git a/lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp b/lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp index 7278339d..dcc33e32 100644 --- a/lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp +++ b/lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp @@ -27,7 +27,7 @@ /** * @brief Three icon plugin * @author Yann Le Glaz - + * */ /****************************************************************************** @@ -60,16 +60,19 @@ *****************************************************************************/ /* Initialize bitmap control topic. */ -const char* ThreeIconPlugin::TOPIC_BITMAP = "/bitmap"; +const char* ThreeIconPlugin::TOPIC_BITMAP = "/bitmap"; + +/* Initialize plugin topic. */ +const char* ThreeIconPlugin::TOPIC_SPRITESHEET = "/spritesheet"; + +/* Initialize animation control topic. */ +const char* ThreeIconPlugin::TOPIC_ANIMATION = "/animation"; /* Initialize bitmap image filename extension. */ -const char* ThreeIconPlugin::FILE_EXT_BITMAP = ".bmp"; +const char* ThreeIconPlugin::FILE_EXT_BITMAP = ".bmp"; /* Initialize sprite sheet parameter filename extension. */ -const char* ThreeIconPlugin::FILE_EXT_SPRITE_SHEET = ".sprite"; - -/* Initialize animation control topic. */ -const char* ThreeIconPlugin::TOPIC_ANIMATION = "/animation"; +const char* ThreeIconPlugin::FILE_EXT_SPRITE_SHEET = ".sprite"; /****************************************************************************** * Public Methods @@ -81,12 +84,17 @@ void ThreeIconPlugin::getTopics(JsonArray& topics) const for(iconId = 0U; iconId < MAX_ICONS; ++iconId) { - JsonObject jsonIcon = topics.createNestedObject(); + JsonObject jsonIcon = topics.createNestedObject(); + JsonObject jsonSpriteSheet = topics.createNestedObject(); + JsonObject jsonAnimation = topics.createNestedObject(); - jsonIcon["name"] = String(TOPIC_BITMAP) + "/" + iconId; - jsonIcon["access"] = "w"; /* Only icon upload is supported. */ + jsonIcon["name"] = String(TOPIC_BITMAP) + "/" + iconId; + jsonIcon["access"] = "w"; /* Only icon upload is supported. */ - (void)topics.add(String(TOPIC_ANIMATION) + "/" + iconId); + jsonSpriteSheet["name"] = String(TOPIC_SPRITESHEET) + "/" + iconId; + jsonSpriteSheet["access"] = "w"; /* Only sprite sheet upload is supported. */ + + jsonAnimation["name"] = String(TOPIC_ANIMATION) + "/" + iconId; } } @@ -94,11 +102,7 @@ bool ThreeIconPlugin::getTopic(const String& topic, JsonObject& value) const { bool isSuccessful = false; - if (0U != topic.startsWith(String(TOPIC_BITMAP) + "/")) - { - isSuccessful = true; - } - else if (0U != topic.startsWith(String(TOPIC_ANIMATION) + "/")) + if (0U != topic.startsWith(String(TOPIC_ANIMATION) + "/")) { uint32_t indexBeginIconId = topic.lastIndexOf("/") + 1U; String iconIdStr = topic.substring(indexBeginIconId); @@ -108,17 +112,21 @@ bool ThreeIconPlugin::getTopic(const String& topic, JsonObject& value) const if ((true == status) && (MAX_ICONS > iconId)) { - value["id"] = iconId; - value["repeat"] = getIsRepeat(iconId); - value["forward"] = getIsForward(iconId); + String iconFullPath; + String spriteSheetFullPath; + + getIconFilePath(iconId, iconFullPath); + getSpriteSheetFilePath(iconId, spriteSheetFullPath); + + value["id"] = iconId; + value["repeat"] = getIsRepeat(iconId); + value["forward"] = getIsForward(iconId); + value["iconFullPath"] = iconFullPath; + value["spriteSheetFullPath"] = spriteSheetFullPath; isSuccessful = true; } } - else - { - ; - } return isSuccessful; } @@ -143,18 +151,34 @@ bool ThreeIconPlugin::setTopic(const String& topic, const JsonObject& value) if (false == jsonIconPath.isNull()) { String iconPath = jsonIconPath.as(); - isSuccessful = loadBitmap(iconPath, iconId); + + isSuccessful = loadBitmap(iconId, iconPath); } - /* Control command */ - else + } + } + else if (0U != topic.startsWith(String(TOPIC_SPRITESHEET) + "/")) + { + uint32_t indexBeginIconId = topic.lastIndexOf("/") + 1U; + String iconIdStr = topic.substring(indexBeginIconId); + uint8_t iconId = MAX_ICONS; + bool status = Util::strToUInt8(iconIdStr, iconId); + + if ((true == status) && + (MAX_ICONS > iconId)) + { + JsonVariantConst jsonSpriteSheetPath = value["fullPath"]; + + /* File upload? */ + if (false == jsonSpriteSheetPath.isNull()) { - JsonVariantConst jsonClear = value["clear"]; + String spriteSheetPath = jsonSpriteSheetPath.as(); - if (jsonClear.as() == "true") - { - clearBitmap(iconId); - isSuccessful = true; - } + /* Don't use the return value, because there may be no bitmap + * available. + */ + (void)loadSpriteSheet(iconId, spriteSheetPath); + + isSuccessful = true; } } } @@ -167,10 +191,12 @@ bool ThreeIconPlugin::setTopic(const String& topic, const JsonObject& value) if ((true == status) && (MAX_ICONS > iconId) && - (false != m_isSpriteSheetAvailable[iconId])) + (false == m_spriteSheetPaths[iconId].isEmpty())) { - JsonVariantConst jsonIsForward = value["forward"]; - JsonVariantConst jsonIsRepeat = value["repeat"]; + JsonVariantConst jsonIsForward = value["forward"]; + JsonVariantConst jsonIsRepeat = value["repeat"]; + JsonVariantConst jsonIconFullPath = value["iconFullPath"]; + JsonVariantConst jsonSpriteSheetFullPath = value["spritesheetFullPath"]; if (false == jsonIsForward.isNull()) { @@ -207,6 +233,38 @@ bool ThreeIconPlugin::setTopic(const String& topic, const JsonObject& value) ; } } + + if (false == jsonIconFullPath.isNull()) + { + String iconFullPath = jsonIconFullPath.as(); + + if (true == iconFullPath.isEmpty()) + { + clearBitmap(iconId); + } + else + { + loadBitmap(iconId, iconFullPath); + } + + isSuccessful = true; + } + + if (false == jsonSpriteSheetFullPath.isNull()) + { + String spriteSheetFullPath = jsonSpriteSheetFullPath.as(); + + if (true == spriteSheetFullPath.isEmpty()) + { + clearSpriteSheet(iconId); + } + else + { + loadSpriteSheet(iconId, spriteSheetFullPath); + } + + isSuccessful = true; + } } } else @@ -230,11 +288,12 @@ bool ThreeIconPlugin::hasTopicChanged(const String& topic) if ((true == status) && (MAX_ICONS > iconId) && - (false != m_isSpriteSheetAvailable[iconId])) + (false == m_spriteSheetPaths[iconId].isEmpty())) { MutexGuard guard(m_mutex); - m_hasTopicChanged = false; + hasTopicChanged = m_hasTopicChanged; + m_hasTopicChanged = false; } } @@ -259,18 +318,25 @@ bool ThreeIconPlugin::isUploadAccepted(const String& topic, const String& srcFil isAccepted = true; } + } + else if (0U != topic.startsWith(String(TOPIC_SPRITESHEET) + "/")) + { + uint32_t indexBeginIconId = topic.lastIndexOf("/") + 1U; + String iconIdStr = topic.substring(indexBeginIconId); + uint8_t iconId = MAX_ICONS; + bool status = Util::strToUInt8(iconIdStr, iconId); + /* Accept upload of a sprite sheet file. */ - else if ( (false != status) && (0U != srcFilename.endsWith(FILE_EXT_SPRITE_SHEET))) + if ((false != status) && (0U != srcFilename.endsWith(FILE_EXT_SPRITE_SHEET))) { dstFilename = getFileName(iconId, FILE_EXT_SPRITE_SHEET); isAccepted = true; } - else - { - /* Not accepted. */ - ; - } + } + else + { + ; } return isAccepted; @@ -288,18 +354,27 @@ void ThreeIconPlugin::start(uint16_t width, uint16_t height) { int16_t x = (ICON_WIDTH + DISTANCE) * iconId + DISTANCE; - (void)m_threeIconCanvas.addWidget(m_bitmapWidget[iconId]); - m_bitmapWidget[iconId].move(x, 0); - - /* If there is already an icon in the filesystem for the respective icon-slot, it will be loaded. - * First check whether it is a animated sprite sheet and if not, try - * to load just a bitmap image. + (void)m_threeIconCanvas.addWidget(m_bitmapWidgets[iconId]); + m_bitmapWidgets[iconId].move(x, 0); + + /* If there is an icon in the filesystem with the plugin UID as filename, + * it will be loaded. First check whether it is a animated sprite sheet + * and if not, try to load just a bitmap image. */ - m_isSpriteSheetAvailable[iconId] = m_bitmapWidget[iconId].loadSpriteSheet(FILESYSTEM, getFileName(iconId, FILE_EXT_SPRITE_SHEET), getFileName(iconId, FILE_EXT_BITMAP)); + m_iconPaths[iconId].clear(); + m_spriteSheetPaths[iconId].clear(); - if (false == m_isSpriteSheetAvailable[iconId]) - { - (void)m_bitmapWidget[iconId].load(FILESYSTEM, getFileName(iconId, FILE_EXT_BITMAP)); + if (false == m_bitmapWidgets[iconId].loadSpriteSheet(FILESYSTEM, getFileName(iconId, FILE_EXT_SPRITE_SHEET), getFileName(iconId, FILE_EXT_BITMAP))) + { + if (true == m_bitmapWidgets[iconId].load(FILESYSTEM, getFileName(iconId, FILE_EXT_BITMAP))) + { + m_iconPaths[iconId] = getFileName(iconId, FILE_EXT_BITMAP); + } + } + else + { + m_iconPaths[iconId] = getFileName(iconId, FILE_EXT_BITMAP); + m_spriteSheetPaths[iconId] = getFileName(iconId, FILE_EXT_SPRITE_SHEET); } } } @@ -331,7 +406,7 @@ void ThreeIconPlugin::update(YAGfx& gfx) m_threeIconCanvas.update(gfx); } -bool ThreeIconPlugin::loadBitmap(const String& filename, uint8_t iconId) +bool ThreeIconPlugin::loadBitmap(uint8_t iconId, const String& filename) { bool status = false; @@ -339,35 +414,43 @@ bool ThreeIconPlugin::loadBitmap(const String& filename, uint8_t iconId) { MutexGuard guard(m_mutex); - if (0U != filename.endsWith(FILE_EXT_BITMAP)) + if (m_iconPaths[iconId] != filename) { - status = m_bitmapWidget[iconId].load(FILESYSTEM, filename); - - /* Ensure that only the bitmap image file exists in the filesystem, - * otherwise after a restart, the obsolete sprite sheet will - * be loaded. - */ - if (false != status) - { - (void)FILESYSTEM.remove(getFileName(iconId, FILE_EXT_SPRITE_SHEET)); + m_iconPaths[iconId] = filename; + m_hasTopicChanged = true; + } - m_isSpriteSheetAvailable[iconId] = false; - } + if (false == m_spriteSheetPaths->isEmpty()) + { + status = m_bitmapWidgets[iconId].loadSpriteSheet(FILESYSTEM, m_iconPaths[iconId], m_spriteSheetPaths[iconId]); } - else if (0U != filename.endsWith(FILE_EXT_SPRITE_SHEET)) + + if (false == status) { - String bmpFilename = filename; + status = m_bitmapWidgets[iconId].load(FILESYSTEM, m_iconPaths[iconId]); + } + } - bmpFilename.replace(FILE_EXT_SPRITE_SHEET, FILE_EXT_BITMAP); + return status; +} - status = m_bitmapWidget[iconId].loadSpriteSheet(FILESYSTEM, filename, bmpFilename); - - m_isSpriteSheetAvailable[iconId] = status; +bool ThreeIconPlugin::loadSpriteSheet(uint8_t iconId, const String& filename) +{ + bool status = false; + + if (MAX_ICONS > iconId) + { + MutexGuard guard(m_mutex); + + if (m_spriteSheetPaths[iconId] != filename) + { + m_spriteSheetPaths[iconId] = filename; + m_hasTopicChanged = true; } - else + + if (false == m_iconPaths[iconId].isEmpty()) { - /* Not supported. */ - ; + status = m_bitmapWidgets[iconId].loadSpriteSheet(FILESYSTEM, m_iconPaths[iconId], m_spriteSheetPaths[iconId]); } } @@ -381,7 +464,7 @@ bool ThreeIconPlugin::getIsForward(uint8_t iconId) const if (MAX_ICONS > iconId) { - state = m_bitmapWidget[iconId].isSpriteSheetForward(); + state = m_bitmapWidgets[iconId].isSpriteSheetForward(); } return state; @@ -393,9 +476,9 @@ void ThreeIconPlugin::setIsForward(uint8_t iconId, bool state) if (MAX_ICONS > iconId) { - if (state != m_bitmapWidget[iconId].isSpriteSheetForward()) + if (state != m_bitmapWidgets[iconId].isSpriteSheetForward()) { - m_bitmapWidget[iconId].setSpriteSheetForward(state); + m_bitmapWidgets[iconId].setSpriteSheetForward(state); m_hasTopicChanged = true; } @@ -409,7 +492,7 @@ bool ThreeIconPlugin::getIsRepeat(uint8_t iconId) const if (MAX_ICONS > iconId) { - state = m_bitmapWidget[iconId].isSpriteSheetRepeatInfinite(); + state = m_bitmapWidgets[iconId].isSpriteSheetRepeatInfinite(); } return state; @@ -421,9 +504,9 @@ void ThreeIconPlugin::setIsRepeat(uint8_t iconId, bool state) if (MAX_ICONS > iconId) { - if (state != m_bitmapWidget[iconId].isSpriteSheetRepeatInfinite()) + if (state != m_bitmapWidgets[iconId].isSpriteSheetRepeatInfinite()) { - m_bitmapWidget[iconId].setSpriteSheetRepeatInfinite(state); + m_bitmapWidgets[iconId].setSpriteSheetRepeatInfinite(state); m_hasTopicChanged = true; } @@ -436,15 +519,53 @@ void ThreeIconPlugin::clearBitmap(uint8_t iconId) { MutexGuard guard(m_mutex); - m_bitmapWidget[iconId].clear(ColorDef::BLACK); + if (false == m_iconPaths[iconId].isEmpty()) + { + m_iconPaths[iconId].clear(); + m_bitmapWidgets[iconId].clear(ColorDef::BLACK); - (void)FILESYSTEM.remove(getFileName(iconId, FILE_EXT_BITMAP)); + m_hasTopicChanged = true; + } + } +} + +void ThreeIconPlugin::clearSpriteSheet(uint8_t iconId) +{ + if (MAX_ICONS > iconId) + { + MutexGuard guard(m_mutex); + + if (false == m_spriteSheetPaths[iconId].isEmpty()) + { + m_spriteSheetPaths[iconId].clear(); + + m_hasTopicChanged = true; + } + + if (false == m_iconPaths[iconId].isEmpty()) + { + (void)m_bitmapWidgets[iconId].load(FILESYSTEM, m_iconPaths[iconId]); + } + } +} + +void ThreeIconPlugin::getIconFilePath(uint8_t iconId, String& fullPath) const +{ + if (MAX_ICONS > iconId) + { + MutexGuard guard(m_mutex); + + fullPath = m_iconPaths[iconId]; + } +} + +void ThreeIconPlugin::getSpriteSheetFilePath(uint8_t iconId, String& fullPath) const +{ + if (MAX_ICONS > iconId) + { + MutexGuard guard(m_mutex); - if (true == m_isSpriteSheetAvailable[iconId]) - { - (void)FILESYSTEM.remove(getFileName(iconId, FILE_EXT_SPRITE_SHEET)); - m_isSpriteSheetAvailable[iconId] = false; - } + fullPath = m_spriteSheetPaths[iconId]; } } diff --git a/lib/ThreeIconPlugin/src/ThreeIconPlugin.h b/lib/ThreeIconPlugin/src/ThreeIconPlugin.h index 4c0bea50..44f471ed 100644 --- a/lib/ThreeIconPlugin/src/ThreeIconPlugin.h +++ b/lib/ThreeIconPlugin/src/ThreeIconPlugin.h @@ -25,7 +25,7 @@ DESCRIPTION *******************************************************************************/ /** - * @brief Three icon plugin + * @brief Three icon plugin * @author Yann Le Glaz * * @addtogroup plugin @@ -74,8 +74,9 @@ class ThreeIconPlugin : public Plugin ThreeIconPlugin(const String& name, uint16_t uid) : Plugin(name, uid), m_threeIconCanvas(), - m_bitmapWidget(), - m_isSpriteSheetAvailable{false}, + m_bitmapWidgets(), + m_iconPaths(), + m_spriteSheetPaths(), m_isUploadError(false), m_mutex(), m_hasTopicChanged(false) @@ -207,19 +208,26 @@ class ThreeIconPlugin : public Plugin void update(YAGfx& gfx) final; /** - * Load bitmap or spritesheet from filesystem and show it on the icon position by - * icon id. - * - * If a animation is required, upload first the bitmap and then the spritesheet. - * Because a bitmap upload will remove any spritesheet. This behaviour is necessary - * to show only bitmaps too. + * Load bitmap image from filesystem. If a sprite sheet is available, the + * bitmap will be automatically used as texture for animation. + * + * @param[in] iconId The icon id. + * @param[in] filename Bitmap image filename. + * + * @return If successul, it will return true otherwise false. + */ + bool loadBitmap(uint8_t iconId, const String& filename); + + /** + * Load sprite sheet from filesystem. If a bitmap is available, it will + * be automatically used as texture for animation. * - * @param[in] filename Bitmap filename. * @param[in] iconId The icon id. + * @param[in] filename Sprite sheet filename. * * @return If successul, it will return true otherwise false. */ - bool loadBitmap(const String& filename, uint8_t iconId); + bool loadSpriteSheet(uint8_t iconId, const String& filename); /** * Get the state of the FORWARD control flag of an icon. @@ -262,6 +270,29 @@ class ThreeIconPlugin : public Plugin */ void clearBitmap(uint8_t iconId); + /** + * Clear sprite sheet by icon id. + * + * @param[in] iconId The icon id. + */ + void clearSpriteSheet(uint8_t iconId); + + /** + * Get icon file path by icon id. + * + * @param[in] iconId The icon id. + * @param[out] fullPath Path to icon file. + */ + void getIconFilePath(uint8_t iconId, String& fullPath) const; + + /** + * Get sprite sheet file path by icon id. + * + * @param[in] iconId The icon id. + * @param[out] fullPath Path to sprite sheet file. + */ + void getSpriteSheetFilePath(uint8_t iconId, String& fullPath) const; + private: /** @@ -269,6 +300,11 @@ class ThreeIconPlugin : public Plugin */ static const char* TOPIC_BITMAP; + /** + * Plugin topic, used for parameter exchange. + */ + static const char* TOPIC_SPRITESHEET; + /** * Plugin topic, used for only for animation control in case a spritesheet * with texture is loaded. @@ -301,12 +337,13 @@ class ThreeIconPlugin : public Plugin static const char* FILE_EXT_SPRITE_SHEET; - WidgetGroup m_threeIconCanvas; /**< Canvas used for the bitmap widget. */ - BitmapWidget m_bitmapWidget[MAX_ICONS]; /**< Bitmap widget, used to show the icon. */ - bool m_isSpriteSheetAvailable[MAX_ICONS]; /**< Flag to indicate whether a spritesheet is used or just a bitmap. */ - bool m_isUploadError; /**< Flag to signal a upload error. */ - mutable MutexRecursive m_mutex; /**< Mutex to protect against concurrent access. */ - bool m_hasTopicChanged; /**< Has the topic content changed? */ + WidgetGroup m_threeIconCanvas; /**< Canvas used for the bitmap widget. */ + BitmapWidget m_bitmapWidgets[MAX_ICONS]; /**< Bitmap widgets, used to show the icon. */ + String m_iconPaths[MAX_ICONS]; /**< Full path to icons. */ + String m_spriteSheetPaths[MAX_ICONS]; /**< Full path to spritesheets. */ + bool m_isUploadError; /**< Flag to signal a upload error. */ + mutable MutexRecursive m_mutex; /**< Mutex to protect against concurrent access. */ + bool m_hasTopicChanged; /**< Has the topic content changed? */ /** * Get image filename with path. diff --git a/lib/ThreeIconPlugin/web/ThreeIconPlugin.html b/lib/ThreeIconPlugin/web/ThreeIconPlugin.html index a9de0220..4c7044ca 100644 --- a/lib/ThreeIconPlugin/web/ThreeIconPlugin.html +++ b/lib/ThreeIconPlugin/web/ThreeIconPlugin.html @@ -67,21 +67,21 @@

    Clear icon

  • STATE: true or false
  • Control animation (Sprite Sheet required)

    -
    POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/animation/<ICON-ID>?forward=<STATE>
    -
    POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/animation/<ICON-ID>?forward=<STATE>
    -
    POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/animation/<ICON-ID>?repeat=<STATE>
    -
    POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/animation/<ICON-ID>?repeat=<STATE>
    - +
    POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/animation/<ICON-ID>?forward=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
    +
    POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/animation/<ICON-ID>?forward=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
    +
    POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/animation/<ICON-ID>?repeat=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
    +
    POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/animation/<ICON-ID>?repeat=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
    • PLUGIN-UID: The plugin unique id.
    • PLUGIN-ALIAS: The plugin alias name.
    • ICON-ID: The id of the icon (starting with 0 from left).
    • STATE: true or false
    • +
    • ICON-FULL-PATH: Full path to the icon.
    • +
    • SPRITESHEET-FULL-PATH: Full path to the sprite sheet.

    Configuration

    -

    Bitmap/Sprite sheet

    -

    Set Icon

    -
    +

    Icon

    +
    + +
    + +
    +

    Sprite sheet

    +
    +
    + + +
    +
    + + +
    +
    + +
    -

    Clear Icon

    +

    Clear Icon/Sprite sheet

    @@ -145,7 +166,15 @@

    Animation configuration

    + +
    +
    + + +
    +
    + +
    @@ -179,11 +208,13 @@

    Animation configuration

    function enableUI() { utils.enableForm("setIconForm", true); + utils.enableForm("setSpriteSheetForm", true); utils.enableForm("myFormAnimation", true); } function disableUI() { utils.enableForm("setIconForm", false); + utils.enableForm("setSpriteSheetForm", false); utils.enableForm("myFormAnimation", false); } @@ -230,11 +261,11 @@

    Animation configuration

    }); }; - function setIconByForm(formId) { + function uploadIconByForm(formId) { var values = $("#" + formId).serializeAllArray(); var pluginUid = 0; var index = 255; - var file = $("#fileBitmap").prop("files")[0] + var file = $("#bitmap").prop("files")[0] $.each(values, function(idx, field) { if (field.name === "pluginUid") { @@ -249,6 +280,25 @@

    Animation configuration

    setIcon(pluginUid, file, index); } + function uploadSpriteSheetByForm(formId) { + var values = $("#" + formId).serializeAllArray(); + var pluginUid = 0; + var index = 255; + var file = $("#spriteSheet").prop("files")[0] + + $.each(values, function(idx, field) { + if (field.name === "pluginUid") { + pluginUid = field.value; + } else if (field.name === "spriteSheetPos") { + index = field.value; + } else { + /* Skip. */ + } + }); + + setSpriteSheet(pluginUid, file, index); + } + function setIcon(pluginUid, file, index) { disableUI(); @@ -263,6 +313,30 @@

    Animation configuration

    "X-File-Size": file.size } }).then(function(rsp) { + updateForms(); + alert("Ok."); + }).catch(function(rsp) { + alert("Failed."); + }).finally(function() { + enableUI(); + }); + } + + function setSpriteSheet(pluginUid, file, index) { + disableUI(); + + return utils.makeRequest({ + method: "POST", + url: "/rest/api/v1/display/uid/" + pluginUid + "/spritesheet/" + index, + isJsonResponse: true, + parameter: { + file: file + }, + headers: { + "X-File-Size": file.size + } + }).then(function(rsp) { + updateForms(); alert("Ok."); }).catch(function(rsp) { alert("Failed."); @@ -294,12 +368,14 @@

    Animation configuration

    return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/bitmap/" + index, + url: "/rest/api/v1/display/uid/" + pluginUid + "/animation/" + index, isJsonResponse: true, parameter: { - clear: true + iconFullPath: "", + spriteSheetFullPath: "" } }).then(function(rsp) { + updateForms(); alert("Ok."); }).catch(function(rsp) { alert("Failed."); @@ -335,6 +411,8 @@

    Animation configuration

    }).then(function(rsp) { var forwardElement = document.getElementById("forwardAnimation"); var repeatElement = document.getElementById("repeatAnimation"); + var iconFullPathInput = document.getElementById("iconFullPath"); + var spriteSheetFullPathInput = document.getElementById("spriteSheetFullPath"); if (false == rsp.data.forward) { forwardElement.selectedIndex = 0; @@ -348,6 +426,9 @@

    Animation configuration

    repeatElement.selectedIndex = 1; } + iconFullPathInput.value = rsp.data.iconFullPath; + spriteSheetFullPathInput.value = rsp.data.spriteSheetFullPath; + }).catch(function(rsp) { alert("Internal error."); }).finally(function() { @@ -361,6 +442,8 @@

    Animation configuration

    var index = 255; var isForward = false; var isRepeat = false; + var iconFullPath = ""; + var spriteSheetFullPath = ""; $.each(values, function(idx, field) { if (field.name === "pluginUid") { @@ -371,15 +454,19 @@

    Animation configuration

    isForward = field.value; } else if (field.name === "repeat") { isRepeat = field.value; + } else if (field.name === "icon") { + iconFullPath = field.value; + } else if (field.name === "spriteSheet") { + spriteSheetFullPath = field.value; } else { /* Skip. */ } }); - setAnimation(pluginUid, isForward, isRepeat, index); + setAnimation(pluginUid, isForward, isRepeat, iconFullPath, spriteSheetFullPath, index); } - function setAnimation(pluginUid, isForward, isRepeat, index) { + function setAnimation(pluginUid, isForward, isRepeat, iconFullPath, spriteSheetFullPath, index) { disableUI(); return utils.makeRequest({ @@ -388,7 +475,9 @@

    Animation configuration

    isJsonResponse: true, parameter: { forward: isForward, - repeat: isRepeat + repeat: isRepeat, + iconFullPath: iconFullPath, + spriteSheetFullPath: spriteSheetFullPath } }).then(function(rsp) { alert("Ok."); @@ -399,6 +488,15 @@

    Animation configuration

    }); } + function updateForms() { + var selectText = document.getElementById("pluginUidText"); + + if (0 <= selectText.selectedIndex) { + + return getAnimationByForm('myFormAnimation'); + } + } + (function ($) { $.fn.serializeAllArray = function () { var data = $(this).serializeArray(); From 09ff6ee0f37c33bf71266978d0840d8ce1171178 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Tue, 24 Oct 2023 23:52:18 +0200 Subject: [PATCH 033/105] TopicHandler's made independent from the plugin interface. This is necessary to provide topics which are plugin independent. #132 --- config/configSmallUlanzi.ini | 4 +- doc/HOMEASSISTANT.md | 2 +- doc/MQTT.md | 8 +- doc/architecture/uml/plugin-service.wsd | 4 +- doc/architecture/uml/plugin_cfg_handling.wsd | 4 +- .../uml/topic_handler_service.wsd | 12 +- .../uml/topic_handling_mindmap.wsd | 28 +- lib/CountdownPlugin/web/CountdownPlugin.html | 12 +- lib/DateTimePlugin/web/DateTimePlugin.html | 12 +- .../web/GrabViaMqttPlugin.html | 10 +- .../web/GrabViaRestPlugin.html | 10 +- lib/GruenbeckPlugin/web/GruenbeckPlugin.html | 12 +- lib/ITopicHandler/src/ITopicHandler.h | 47 +- .../web/IconTextLampPlugin.html | 36 +- lib/IconTextPlugin/web/IconTextPlugin.html | 24 +- lib/JustTextPlugin/web/JustTextPlugin.html | 12 +- lib/MqttApiTopicHandler/library.json | 3 - .../src/MqttApiTopicHandler.cpp | 415 +++++++----------- .../src/MqttApiTopicHandler.h | 114 ++--- lib/MqttService/src/MqttService.cpp | 7 +- .../web/OpenWeatherPlugin.html | 12 +- .../src/RestApiTopicHandler.cpp | 220 +++++----- .../src/RestApiTopicHandler.h | 111 +++-- lib/SensorPlugin/web/SensorPlugin.html | 8 +- .../web/SignalDetectorPlugin.html | 12 +- .../web/SoundReactivePlugin.html | 12 +- lib/SunrisePlugin/web/SunrisePlugin.html | 12 +- lib/ThreeIconPlugin/web/ThreeIconPlugin.html | 26 +- .../src/TopicHandlerService.cpp | 366 +++++++++++---- .../src/TopicHandlerService.h | 140 ++++-- lib/VolumioPlugin/web/VolumioPlugin.html | 12 +- src/Plugin/PluginMgr.cpp | 21 +- src/Plugin/PluginMgr.h | 4 +- 33 files changed, 909 insertions(+), 823 deletions(-) diff --git a/config/configSmallUlanzi.ini b/config/configSmallUlanzi.ini index 3cdc251b..69ff04ff 100644 --- a/config/configSmallUlanzi.ini +++ b/config/configSmallUlanzi.ini @@ -19,7 +19,7 @@ lib_deps = ;CountdownPlugin @ ~0.1.0 DateTimePlugin @ ~0.1.0 ;DDPPlugin @ ~0.1.0 - FirePlugin @ ~0.1.0 + ;FirePlugin @ ~0.1.0 GameOfLifePlugin @ ~0.1.0 GrabViaMqttPlugin @ ~0.1.0 # Requires MqttService GrabViaRestPlugin @ ~0.1.0 @@ -39,6 +39,6 @@ lib_deps = ;ThreeIconPlugin @ ~0.1.0 ;VolumioPlugin @ ~0.1.0 ;WifiStatusPlugin @ ~0.1.0 - WormPlugin @ ~0.1.0 + ;WormPlugin @ ~0.1.0 extra_scripts = pre:./scripts/configure_small_ulanzi.py diff --git a/doc/HOMEASSISTANT.md b/doc/HOMEASSISTANT.md index e67f01c9..fe30d768 100644 --- a/doc/HOMEASSISTANT.md +++ b/doc/HOMEASSISTANT.md @@ -31,7 +31,7 @@ Add the following lines of code to your `configuration.yaml`: ```yaml rest_command: pixelix_just_text: - url: 'http://192.168.178.10/rest/api/v1/display/uid/{{ uid }}/text?text={{ "%5C" + align + "%5C" + color + text }}' + url: 'http://192.168.178.10/rest/api/v1/display/{{ uid }}/text?text={{ "%5C" + align + "%5C" + color + text }}' method: POST ``` You need to replace the IP `192.168.178.10` with your Pixelix instance IP. diff --git a/doc/MQTT.md b/doc/MQTT.md index 4d928163..b58792f8 100644 --- a/doc/MQTT.md +++ b/doc/MQTT.md @@ -42,16 +42,16 @@ After the successful connection establishment to the MQTT broker, Pixelix will s ## Plugin base URI The base URI to access plugin related topics can be setup with the plugin UID or the plugin alias: -* <HOSTNAME>/uid/<PLUGIN-UID>/... -* <HOSTNAME>/alias/<PLUGIN-ALIAS>/... +* <HOSTNAME>/<PLUGIN-UID>/... +* <HOSTNAME>/<PLUGIN-ALIAS>/... ## Topic name The complete topic name can be derived from the REST API documentation. Example: JustTextPlugin -The REST API URL looks like the following: http://<HOSTNAME>/rest/api/v1/display/uid/<PLUGIN-UID>/text?text=<TEXT> -1. Replace the http://<HOSTNAME>/rest/api/v1/display part with <HOSTNAME> --> <HOSTNAME>/uid/<PLUGIN-UID>/text?text=<TEXT> +The REST API URL looks like the following: http://<HOSTNAME>/rest/api/v1/display/<PLUGIN-UID>/text?text=<TEXT> +1. Replace the http://<HOSTNAME>/rest/api/v1/display part with <HOSTNAME> --> <HOSTNAME>/<PLUGIN-UID>/text?text=<TEXT> 2. Every URL parameter, which is in this case show=<TEXT> must be sent in JSON format. ```json diff --git a/doc/architecture/uml/plugin-service.wsd b/doc/architecture/uml/plugin-service.wsd index 00f0a107..80200005 100644 --- a/doc/architecture/uml/plugin-service.wsd +++ b/doc/architecture/uml/plugin-service.wsd @@ -162,8 +162,8 @@ note left of PluginMgr plugin topics. The REST API URL depends on the plugin UID or alias: - * /rest/api/v1/display/uid/ - * /rest/api/v1/display/alias/ + * /rest/api/v1/display/ + * /rest/api/v1/display/ end note class ESPAsyncWebserver diff --git a/doc/architecture/uml/plugin_cfg_handling.wsd b/doc/architecture/uml/plugin_cfg_handling.wsd index b075b34f..9f56f731 100644 --- a/doc/architecture/uml/plugin_cfg_handling.wsd +++ b/doc/architecture/uml/plugin_cfg_handling.wsd @@ -35,7 +35,7 @@ note over restApiTopicHandler,fs Read configuration via REST API. end note --> restApiTopicHandler: GET /rest/api/v1/display/uid// +-> restApiTopicHandler: GET /rest/api/v1/display// note over restApiTopicHandler The topic handler would be able to read the configuration from the filesystem. But this shall be avoided because of @@ -50,7 +50,7 @@ note over restApiTopicHandler,fs Write configuration via REST API. end note --> restApiTopicHandler: POST /rest/api/v1/display/uid// with JSON +-> restApiTopicHandler: POST /rest/api/v1/display// with JSON note over plugin Don't write configuration to flash, because flash access is slow. It shall be written during plugin processing. diff --git a/doc/architecture/uml/topic_handler_service.wsd b/doc/architecture/uml/topic_handler_service.wsd index f65e22ed..afbabd01 100644 --- a/doc/architecture/uml/topic_handler_service.wsd +++ b/doc/architecture/uml/topic_handler_service.wsd @@ -33,7 +33,7 @@ loop for every plugin topicHandlerService -> restApiTopicHandler: register plugin topic (plugin, topic, accessiblity, extra parameters) note over restApiTopicHandler,httpServer - URL: /display/uid// + URL: /display// end note restApiTopicHandler -> httpServer: register REST URL by plugin UID @@ -42,7 +42,7 @@ loop for every plugin alt If plugin alias available note over restApiTopicHandler,httpServer - URL: /display/alias// + URL: /display// end note restApiTopicHandler -> httpServer: register REST URL by plugin alias @@ -64,8 +64,8 @@ loop for every plugin alt If topic is readable note over mqttApiTopicHandler,mqttClient - path: /display/uid///state - path: /display/alias///state + path: /display///state + path: /display///state end note mqttApiTopicHandler -> mqttApiTopicHandler: Store plugin topic in publisher queue @@ -76,7 +76,7 @@ loop for every plugin alt If topic is writeable note over mqttApiTopicHandler,mqttClient - path: /display/uid///set + path: /display///set end note mqttApiTopicHandler -> mqttClient: Subscribe plugin topic path by plugin UID @@ -85,7 +85,7 @@ loop for every plugin alt If plugin alias available note over mqttApiTopicHandler,mqttClient - path: /display/alias///set + path: /display///set end note mqttApiTopicHandler -> mqttClient: Subscribe plugin topic path by plugin alias diff --git a/doc/architecture/uml/topic_handling_mindmap.wsd b/doc/architecture/uml/topic_handling_mindmap.wsd index c51e4c9a..88015f68 100644 --- a/doc/architecture/uml/topic_handling_mindmap.wsd +++ b/doc/architecture/uml/topic_handling_mindmap.wsd @@ -8,12 +8,12 @@ ++++ Base URI: http:///rest/api/v1 ++++ GET -+++++ URL: /display/uid// -+++++ URL: /display/alias// ++++++ URL: /display// ++++++ URL: /display// ++++ POST -+++++ URL: /display/uid// -+++++ URL: /display/alias// ++++++ URL: /display// ++++++ URL: /display// +++++ Parameters via HTTP arguments ******: key=value __converts to__ @@ -88,9 +88,9 @@ "manufacturer": "BlueAndi & Friends", "sw_version": "v7.0.0" }, - "state_topic": "pixelix-facfc834/uid/19583/text/state", + "state_topic": "pixelix-facfc834/19583/text/state", "value_template": "{{ value_json.text }}", - "command_topic": "pixelix-facfc834/uid/19583/text/set", + "command_topic": "pixelix-facfc834/19583/text/set", "command_template": "{\"text\": \"{{ value }}\" }" } ; @@ -109,9 +109,9 @@ "manufacturer": "BlueAndi & Friends", "sw_version": "v7.0.0" }, - "state_topic": "pixelix-facfc834/alias/display/text/state", + "state_topic": "pixelix-facfc834/display/text/state", "value_template": "{{ value_json.text }}", - "command_topic": "pixelix-facfc834/alias/display/text/set", + "command_topic": "pixelix-facfc834/display/text/set", "command_template": "{\"text\": \"{{ value }}\" }" } ; @@ -127,16 +127,16 @@ ++++++ https://www.home-assistant.io/integrations/sensor.mqtt/ ++++ Status -+++++ MQTT topic: /uid///state -+++++ MQTT topic: /alias///state ++++++ MQTT topic: ///state ++++++ MQTT topic: ///state +++++ Direction: Pixelix --> Client(s) ++++ Command -+++++ MQTT topic: /uid///set -+++++ MQTT topic: /alias///set ++++++ MQTT topic: ///set ++++++ MQTT topic: ///set +++++ Direction: Client(s) --> Pixelix ++++ Availability -+++++ MQTT topic: /uid///available -+++++ MQTT topic: /alias///available ++++++ MQTT topic: ///available ++++++ MQTT topic: ///available +++++ Direction: Pixelix --> Client(s) +++++ "online"/"offline" +++++ Skipped for the moment. May be supported in future. diff --git a/lib/CountdownPlugin/web/CountdownPlugin.html b/lib/CountdownPlugin/web/CountdownPlugin.html index e0e2e5ea..02b302b7 100644 --- a/lib/CountdownPlugin/web/CountdownPlugin.html +++ b/lib/CountdownPlugin/web/CountdownPlugin.html @@ -37,15 +37,15 @@

    CountdownPlugin

    The plugin shows the remaining days until a configured target date.

    REST API

    Get target date and target day description.

    -
    GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/countdown
    -
    GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/countdown
    +
    GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/countdown
    +
    GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/countdown
    • PLUGIN-UID: The plugin unique id.
    • PLUGIN-ALIAS: The plugin alias name.

    Set target day and target day desription.

    -
    POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/countdown?day=<DAY>&month=<MONTH>&year=<YEAR>&descPlural=<PLURAL>&descSingular=<SINGULAR>
    -
    POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/countdown?day=<DAY>&month=<MONTH>&year=<YEAR>&descPlural=<PLURAL>&descSingular=<SINGULAR>
    +
    POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/countdown?day=<DAY>&month=<MONTH>&year=<YEAR>&descPlural=<PLURAL>&descSingular=<SINGULAR>
    +
    POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/countdown?day=<DAY>&month=<MONTH>&year=<YEAR>&descPlural=<PLURAL>&descSingular=<SINGULAR>
    • PLUGIN-UID: The plugin unique id.
    • PLUGIN-ALIAS: The plugin alias name.
    • @@ -155,7 +155,7 @@

      Target Date

      disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/countdown", + url: "/rest/api/v1/display/" + pluginUid + "/countdown", isJsonResponse: true }).then(function(rsp) { var targetDateInput = document.getElementById("targetDate"); @@ -176,7 +176,7 @@

      Target Date

      return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/countdown", + url: "/rest/api/v1/display/" + pluginUid + "/countdown", isJsonResponse: true, parameter: { day: date.getDate(), diff --git a/lib/DateTimePlugin/web/DateTimePlugin.html b/lib/DateTimePlugin/web/DateTimePlugin.html index af310eb3..df055ba0 100644 --- a/lib/DateTimePlugin/web/DateTimePlugin.html +++ b/lib/DateTimePlugin/web/DateTimePlugin.html @@ -54,11 +54,11 @@

      Date/Time Format

      By default the local time (see timezone in the settings) is used. It can be overwritten by the plugin configuration.

      REST API

      Get configuration about what the plugin shows.

      -
      GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/dateTime
      -
      GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/dateTime
      +
      GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/dateTime
      +
      GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/dateTime

      Set configuration about what the plugin shall show.

      -
      POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/dateTime?mode=<MODE>
      -
      POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/dateTime?mode=<MODE>
      +
      POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/dateTime?mode=<MODE>
      +
      POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/dateTime?mode=<MODE>
      • PLUGIN-UID: The plugin unique id.
      • PLUGIN-ALIAS: The plugin alias name.
      • @@ -185,7 +185,7 @@

        Display

        disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/dateTime", + url: "/rest/api/v1/display/" + pluginUid + "/dateTime", isJsonResponse: true }).then(function(rsp) { $("#mode").val(rsp.data.mode); @@ -205,7 +205,7 @@

        Display

        disableUI(); return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/dateTime", + url: "/rest/api/v1/display/" + pluginUid + "/dateTime", isJsonResponse: true, parameter: { mode: $("#mode").val(), diff --git a/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html b/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html index 39f8a67c..90d50085 100644 --- a/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html +++ b/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html @@ -37,10 +37,10 @@

        GrabViaMqttPlugin

        The plugin can grab information in JSON format via MQTT and shows it on the display.

        REST API

        Get configuration

        -
        GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/grabConfig
        +
        GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/grabConfig

        Set configuration

        -
        POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/grabConfig?path=<PATH>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
        -
        POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/grabConfig?path=<PATH>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
        +
        POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/grabConfig?path=<PATH>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
        +
        POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/grabConfig?path=<PATH>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
        • PLUGIN-UID: The plugin unique id.
        • PLUGIN-ALIAS: The plugin alias name.
        • @@ -164,7 +164,7 @@

          User

          disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/grabConfig", + url: "/rest/api/v1/display/" + pluginUid + "/grabConfig", isJsonResponse: true }).then(function(rsp) { $("#path").val(rsp.data.path); @@ -185,7 +185,7 @@

          User

          return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/grabConfig", + url: "/rest/api/v1/display/" + pluginUid + "/grabConfig", isJsonResponse: true, parameter: { path: $("#path").val(), diff --git a/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html b/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html index 0257709f..ed3c64bd 100644 --- a/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html +++ b/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html @@ -37,10 +37,10 @@

          GrabViaRestPlugin

          The plugin can grab information in JSON format via REST API and shows it on the display.

          REST API

          Get configuration

          -
          GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/grabConfig
          +
          GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/grabConfig

          Set configuration

          -
          POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/grabConfig?method=<METHOD>&url=<URL>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
          -
          POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/grabConfig?method=<METHOD>&url=<URL>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
          +
          POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/grabConfig?method=<METHOD>&url=<URL>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
          +
          POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/grabConfig?method=<METHOD>&url=<URL>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
          • PLUGIN-UID: The plugin unique id.
          • PLUGIN-ALIAS: The plugin alias name.
          • @@ -169,7 +169,7 @@

            User

            disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/grabConfig", + url: "/rest/api/v1/display/" + pluginUid + "/grabConfig", isJsonResponse: true }).then(function(rsp) { $("#method").val(rsp.data.method); @@ -191,7 +191,7 @@

            User

            return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/grabConfig", + url: "/rest/api/v1/display/" + pluginUid + "/grabConfig", isJsonResponse: true, parameter: { method: $("#method").val(), diff --git a/lib/GruenbeckPlugin/web/GruenbeckPlugin.html b/lib/GruenbeckPlugin/web/GruenbeckPlugin.html index 75753184..afd8efec 100644 --- a/lib/GruenbeckPlugin/web/GruenbeckPlugin.html +++ b/lib/GruenbeckPlugin/web/GruenbeckPlugin.html @@ -37,11 +37,11 @@

            GruenbeckPlugin

            The plugin shows the remaining system capacity (parameter = D_Y_10_1 ) of the Gruenbeck softliQ SC18 via the system's RESTful webservice.

            REST API

            Get ip-address

            -
            GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/ipAddress
            -
            GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/ipAddress
            +
            GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/ipAddress
            +
            GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/ipAddress

            Set ip-address

            -
            POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/ipAddress?gruenbeckIP=<IPADDRESS>
            -
            POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/ipAddress?gruenbeckIP=<IPADDRESS>
            +
            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/ipAddress?gruenbeckIP=<IPADDRESS>
            +
            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/ipAddress?gruenbeckIP=<IPADDRESS>
            • PLUGIN-UID: The plugin unique id.
            • PLUGIN-ALIAS: The plugin alias name.
            • @@ -139,7 +139,7 @@

              IP-address

              disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/ipAddress", + url: "/rest/api/v1/display/" + pluginUid + "/ipAddress", isJsonResponse: true }).then(function(rsp) { $("#gruenbeckIP").val(rsp.data.gruenbeckIP); @@ -154,7 +154,7 @@

              IP-address

              disableUI(); return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/ipAddress", + url: "/rest/api/v1/display/" + pluginUid + "/ipAddress", isJsonResponse: true, parameter: { gruenbeckIP: $("#gruenbeckIP").val() diff --git a/lib/ITopicHandler/src/ITopicHandler.h b/lib/ITopicHandler/src/ITopicHandler.h index d0d644e8..91896513 100644 --- a/lib/ITopicHandler/src/ITopicHandler.h +++ b/lib/ITopicHandler/src/ITopicHandler.h @@ -56,21 +56,21 @@ *****************************************************************************/ /** - * The topic handler interface, used by the plugin manager to register/unregister - * plugin topics. + * The abstract topic handler interface, which will be realized by different + * protocols. */ class ITopicHandler { public: - /** Topic accessibility */ - typedef enum - { - ACCESS_READ_ONLY = 0, /**< Read only */ - ACCESS_READ_WRITE, /**< Read and write */ - ACCESS_WRITE_ONLY /**< Write only */ - - } Access; + /** Function prototype to get topic content. */ + typedef std::function GetTopicFunc; + + /** Function prototype to set topic content. */ + typedef std::function SetTopicFunc; + + /** Function prototype for file upload request. */ + typedef std::function UploadReqFunc; /** * Destroy the interface. @@ -90,22 +90,26 @@ class ITopicHandler virtual void stop() = 0; /** - * Register a single topic of the given plugin. + * Register the topic. * - * @param[in] plugin The plugin which provides the topic. - * @param[in] topic The topic name. - * @param[in] access The topic accessibility. - * @param[in] extra Extra parameters, which depend on the topic handler. + * @param[in] deviceId The device id which represents the physical device. + * @param[in] entityId The entity id which represents the entity of the device. + * @param[in] topic The topic name. + * @param[in] extra Extra parameters, which depend on the topic handler. + * @param[in] getTopicFunc Function to get the topic content. + * @param[in] setTopicFunc Function to set the topic content. + * @param[in] uploadReqFunc Function used for requesting whether an file upload is allowed. */ - virtual void registerTopic(IPluginMaintenance* plugin, const String& topic, Access access, JsonObjectConst& extra) = 0; + virtual void registerTopic(const String& deviceId, const String& entityId, const String& topic, JsonObjectConst& extra, GetTopicFunc getTopicFunc, SetTopicFunc setTopicFunc, UploadReqFunc uploadReqFunc) = 0; /** - * Unregister the topic of the given plugin. + * Unregister the topic. * - * @param[in] plugin The plugin which provides the topic. + * @param[in] deviceId The device id which represents the physical device. + * @param[in] entityId The entity id which represents the entity of the device. * @param[in] topic The topic name. */ - virtual void unregisterTopic(IPluginMaintenance* plugin, const String& topic) = 0; + virtual void unregisterTopic(const String& deviceId, const String& entityId, const String& topic) = 0; /** * Process the topic handler. @@ -115,10 +119,11 @@ class ITopicHandler /** * Notify that the topic has changed. * - * @param[in] plugin The plugin which provides the topic. + * @param[in] deviceId The device id which represents the physical device. + * @param[in] entityId The entity id which represents the entity of the device. * @param[in] topic The topic name. */ - virtual void notify(IPluginMaintenance* plugin, const String& topic) = 0; + virtual void notify(const String& deviceId, const String& entityId, const String& topic) = 0; protected: diff --git a/lib/IconTextLampPlugin/web/IconTextLampPlugin.html b/lib/IconTextLampPlugin/web/IconTextLampPlugin.html index 58a95383..e03a3819 100644 --- a/lib/IconTextLampPlugin/web/IconTextLampPlugin.html +++ b/lib/IconTextLampPlugin/web/IconTextLampPlugin.html @@ -50,15 +50,15 @@

              IconTextLampPlugin

              If MQTT is built in and enabled, it will support Home Assistant MQTT discovery.

              REST API

              Get text

              -
              GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/text
              -
              GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/text
              +
              GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/text
              +
              GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/text
              • PLUGIN-UID: The plugin unique id.
              • PLUGIN-ALIAS: The plugin alias name.

              Set text

              -
              POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
              -
              POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
              +
              POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
              +
              POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
              • PLUGIN-UID: The plugin unique id.
              • PLUGIN-ALIAS: The plugin alias name.
              • @@ -67,29 +67,29 @@

                Set text

              • SPRITESHEET-FULL-PATH: Full path to the sprite sheet.

              Upload icon

              -
              POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/bitmap
              -
              POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/bitmap
              +
              POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/bitmap
              +
              POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/bitmap
              • PLUGIN-UID: The plugin unique id.
              • PLUGIN-ALIAS: The plugin alias name.

              Upload sprite sheet

              -
              POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/spritesheet
              -
              POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/spritesheet
              +
              POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/spritesheet
              +
              POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/spritesheet
              • PLUGIN-UID: The plugin unique id.
              • PLUGIN-ALIAS: The plugin alias name.

              Get all lamp states

              -
              GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/lamps
              -
              GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/lamps
              +
              GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/lamps
              +
              GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/lamps
              • PLUGIN-UID: The plugin unique id.
              • PLUGIN-ALIAS: The plugin alias name.

              Set lamp state

              -
              POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/lamp/<LAMP-ID>?state=<LAMP-STATE>
              -
              POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/lamp/<LAMP-ID>?state=<LAMP-STATE>
              +
              POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/lamp/<LAMP-ID>?state=<LAMP-STATE>
              +
              POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/lamp/<LAMP-ID>?state=<LAMP-STATE>
              • PLUGIN-UID: The plugin unique id.
              • PLUGIN-ALIAS: The plugin alias name.
              • @@ -259,7 +259,7 @@

                Lamp

                return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/bitmap", + url: "/rest/api/v1/display/" + pluginUid + "/bitmap", isJsonResponse: true, parameter: { file: file @@ -282,7 +282,7 @@

                Lamp

                return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/spritesheet", + url: "/rest/api/v1/display/" + pluginUid + "/spritesheet", isJsonResponse: true, parameter: { file: file @@ -304,7 +304,7 @@

                Lamp

                disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/text", + url: "/rest/api/v1/display/" + pluginUid + "/text", isJsonResponse: true }).then(function(rsp) { var justTextInput = document.getElementById(justTextId); @@ -327,7 +327,7 @@

                Lamp

                return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/text", + url: "/rest/api/v1/display/" + pluginUid + "/text", isJsonResponse: true, parameter: { text: justText, @@ -347,7 +347,7 @@

                Lamp

                disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/lamps", + url: "/rest/api/v1/display/" + pluginUid + "/lamps", isJsonResponse: true }).then(function(rsp) { var selectLampState = document.getElementById(lampStateId); @@ -370,7 +370,7 @@

                Lamp

                return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/lamp/" + lampId, + url: "/rest/api/v1/display/" + pluginUid + "/lamp/" + lampId, isJsonResponse: true, parameter: { state: lampState diff --git a/lib/IconTextPlugin/web/IconTextPlugin.html b/lib/IconTextPlugin/web/IconTextPlugin.html index a51835eb..8f963b0c 100644 --- a/lib/IconTextPlugin/web/IconTextPlugin.html +++ b/lib/IconTextPlugin/web/IconTextPlugin.html @@ -50,15 +50,15 @@

                IconTextPlugin

                If MQTT is built in and enabled, it will support Home Assistant MQTT discovery.

                REST API

                Get text

                -
                GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/text
                -
                GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/text
                +
                GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/text
                +
                GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/text
                • PLUGIN-UID: The plugin unique id.
                • PLUGIN-ALIAS: The plugin alias name.

                Set text

                -
                POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                -
                POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                +
                POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                +
                POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                • PLUGIN-UID: The plugin unique id.
                • PLUGIN-ALIAS: The plugin alias name.
                • @@ -67,15 +67,15 @@

                  Set text

                • SPRITESHEET-FULL-PATH: Full path to the sprite sheet.

                Upload icon

                -
                POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/bitmap
                -
                POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/bitmap
                +
                POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/bitmap
                +
                POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/bitmap
                • PLUGIN-UID: The plugin unique id.
                • PLUGIN-ALIAS: The plugin alias name.

                Upload sprite sheet

                -
                POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/spritesheet
                -
                POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/spritesheet
                +
                POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/spritesheet
                +
                POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/spritesheet
                • PLUGIN-UID: The plugin unique id.
                • PLUGIN-ALIAS: The plugin alias name.
                • @@ -216,7 +216,7 @@

                  Text

                  return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/bitmap", + url: "/rest/api/v1/display/" + pluginUid + "/bitmap", isJsonResponse: true, parameter: { file: file @@ -239,7 +239,7 @@

                  Text

                  return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/spritesheet", + url: "/rest/api/v1/display/" + pluginUid + "/spritesheet", isJsonResponse: true, parameter: { file: file @@ -261,7 +261,7 @@

                  Text

                  disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/text", + url: "/rest/api/v1/display/" + pluginUid + "/text", isJsonResponse: true }).then(function(rsp) { var justTextInput = document.getElementById(justTextId); @@ -284,7 +284,7 @@

                  Text

                  return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/text", + url: "/rest/api/v1/display/" + pluginUid + "/text", isJsonResponse: true, parameter: { text: justText, diff --git a/lib/JustTextPlugin/web/JustTextPlugin.html b/lib/JustTextPlugin/web/JustTextPlugin.html index 8b00d233..377eef4d 100644 --- a/lib/JustTextPlugin/web/JustTextPlugin.html +++ b/lib/JustTextPlugin/web/JustTextPlugin.html @@ -38,15 +38,15 @@

                  JustTextPlugin

                  If MQTT is built in and enabled, it will support Home Assistant MQTT discovery.

                  REST API

                  Get text

                  -
                  GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/text
                  -
                  GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/text
                  +
                  GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/text
                  +
                  GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/text
                  • PLUGIN-UID: The plugin unique id.
                  • PLUGIN-ALIAS: The plugin alias name.

                  Set text

                  -
                  POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/text?text=<TEXT>
                  -
                  POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/text?text=<TEXT>
                  +
                  POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/text?text=<TEXT>
                  +
                  POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/text?text=<TEXT>
                  • PLUGIN-UID: The plugin unique id.
                  • PLUGIN-ALIAS: The plugin alias name.
                  • @@ -149,7 +149,7 @@

                    Text

                    disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/text", + url: "/rest/api/v1/display/" + pluginUid + "/text", isJsonResponse: true }).then(function(rsp) { var justTextInput = document.getElementById(justTextId); @@ -168,7 +168,7 @@

                    Text

                    return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/text", + url: "/rest/api/v1/display/" + pluginUid + "/text", isJsonResponse: true, parameter: { text: justText diff --git a/lib/MqttApiTopicHandler/library.json b/lib/MqttApiTopicHandler/library.json index 349d2091..dc643cac 100644 --- a/lib/MqttApiTopicHandler/library.json +++ b/lib/MqttApiTopicHandler/library.json @@ -17,9 +17,6 @@ }, { "name": "MqttService", "version": "~0.1.0" - }, { - "name": "SettingsService", - "version": "~0.1.0" }], "frameworks": "*", "platforms": "*" diff --git a/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp b/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp index 1f301167..2e586c33 100644 --- a/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp +++ b/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp @@ -37,7 +37,6 @@ #include #include -#include #include /****************************************************************************** @@ -80,45 +79,137 @@ void MqttApiTopicHandler::stop() m_haExtension.stop(); } -void MqttApiTopicHandler::registerTopic(IPluginMaintenance* plugin, const String& topic, Access access, JsonObjectConst& extra) +void MqttApiTopicHandler::registerTopic(const String& deviceId, const String& entityId, const String& topic, JsonObjectConst& extra, GetTopicFunc getTopicFunc, SetTopicFunc setTopicFunc, UploadReqFunc uploadReqFunc) { - if ((nullptr != plugin) && + if ((false == deviceId.isEmpty()) && + (false == entityId.isEmpty()) && (false == topic.isEmpty())) { - String baseUri; + String mqttTopicNameBase = deviceId + "/" + entityId + topic; + TopicState* topicState = new(std::nothrow) TopicState(); - /* If plugin has no alias, use the plugin UID for the base URI otherwise use the alias. */ - if (false == plugin->getAlias().isEmpty()) - { - baseUri = getBaseUriByAlias(plugin->getAlias()); - } - else + LOG_INFO("[%s] Register: %s", entityId.c_str(), mqttTopicNameBase.c_str()); + + if (nullptr != topicState) { - baseUri = getBaseUriByUid(plugin->getUID()); - } + String topicUriReadable; + String topicUriWriteable; + + topicState->deviceId = deviceId; + topicState->entityId = entityId; + topicState->topic = topic; + topicState->getTopicFunc = getTopicFunc; + topicState->setTopicFunc = setTopicFunc; + topicState->uploadReqFunc = uploadReqFunc; + topicState->isPublishReq = false; + + /* Is the topic readable? */ + if (nullptr != getTopicFunc) + { + topicUriReadable = mqttTopicNameBase + MQTT_ENDPOINT_READ_ACCESS; - registerTopic(plugin, topic, access, extra, baseUri); + /* Publish initially. */ + topicState->isPublishReq = true; + } + + /* Is the topic writeable? */ + if (nullptr != setTopicFunc) + { + MqttService& mqttService = MqttService::getInstance(); + MqttService::TopicCallback setCallback = + [this, topicState](const String& mqttTopic, const uint8_t* payload, size_t size) + { + if (0U != mqttTopic.endsWith(topicState->topic + MQTT_ENDPOINT_WRITE_ACCESS)) + { + this->write(topicState->deviceId, topicState->entityId, topicState->topic, payload, size, topicState->setTopicFunc, topicState->uploadReqFunc); + } + }; + + topicUriWriteable = mqttTopicNameBase + MQTT_ENDPOINT_WRITE_ACCESS; + + if (false == mqttService.subscribe(topicUriWriteable, setCallback)) + { + LOG_WARNING("[%s] Couldn't subscribe %s.", entityId.c_str(), topicUriWriteable.c_str()); + } + else + { + LOG_INFO("[%s] Subscribed: %s", entityId.c_str(), topicUriWriteable.c_str()); + } + } + + /* Handle Home Assistant extension */ + { + String willTopic = deviceId + "/status"; + + m_haExtension.registerMqttDiscovery(deviceId, entityId, topicUriReadable, topicUriWriteable, willTopic, extra); + } + + m_listOfTopicStates.push_back(topicState); + } } } -void MqttApiTopicHandler::unregisterTopic(IPluginMaintenance* plugin, const String& topic) +void MqttApiTopicHandler::unregisterTopic(const String& deviceId, const String& entityId, const String& topic) { - if ((nullptr != plugin) && + if ((false == deviceId.isEmpty()) && + (false == entityId.isEmpty()) && (false == topic.isEmpty())) { - String baseUri; + String mqttTopicNameBase = deviceId + "/" + entityId + topic; + MqttService& mqttService = MqttService::getInstance(); + ListOfTopicStates::iterator topicStateIt = m_listOfTopicStates.begin(); - /* If plugin has no alias, use the plugin UID for the base URI otherwise use the alias. */ - if (false == plugin->getAlias().isEmpty()) - { - baseUri = getBaseUriByAlias(plugin->getAlias()); - } - else + LOG_INFO("[%s] Unregister: %s", entityId.c_str(), mqttTopicNameBase.c_str()); + + while(m_listOfTopicStates.end() != topicStateIt) { - baseUri = getBaseUriByUid(plugin->getUID()); - } + TopicState* topicState = *topicStateIt; - unregisterTopic(plugin, topic, baseUri); + if ((nullptr != topicState) && + (deviceId == topicState->deviceId) && + (entityId == topicState->entityId) && + (topic == topicState->topic)) + { + String topicUriReadable; + String topicUriWriteable; + + if (nullptr != topicState->getTopicFunc) + { + topicUriReadable = mqttTopicNameBase + MQTT_ENDPOINT_READ_ACCESS; + + /* Purge topic */ + if (false == mqttService.publish(topicUriReadable, "")) + { + LOG_WARNING("[%s] Failed to purge: %s", entityId.c_str(), topicUriReadable.c_str()); + } + else + { + LOG_INFO("[%s] Purged: %s", entityId.c_str(), topicUriReadable.c_str()); + } + } + + if (nullptr != topicState->setTopicFunc) + { + topicUriWriteable = mqttTopicNameBase + MQTT_ENDPOINT_WRITE_ACCESS; + + mqttService.unsubscribe(topicUriWriteable); + + LOG_INFO("[%s] Unsubscribed: %s", entityId.c_str(), topicUriWriteable.c_str()); + } + + /* Handle Home Assistant extension */ + m_haExtension.unregisterMqttDiscovery(deviceId, entityId, topicUriReadable, topicUriWriteable); + + topicStateIt = m_listOfTopicStates.erase(topicStateIt); + + delete topicState; + topicState = nullptr; + } + else + { + ++topicStateIt; + } + } } } @@ -158,16 +249,14 @@ void MqttApiTopicHandler::process() TopicState* topicState = *topicStateIt; if ((nullptr != topicState) && - (nullptr != topicState->plugin) && - ( - (ACCESS_READ_ONLY == topicState->access) || - (ACCESS_READ_WRITE == topicState->access) - )) + (false == topicState->deviceId.isEmpty()) && + (false == topicState->entityId.isEmpty()) && + (nullptr != topicState->getTopicFunc)) { if ((true == publishAll) || (true == topicState->isPublishReq)) { - publish(topicState->topicUri, topicState->plugin, topicState->topic); + publish(topicState->deviceId, topicState->entityId, topicState->topic, topicState->getTopicFunc); topicState->isPublishReq = false; } @@ -181,9 +270,10 @@ void MqttApiTopicHandler::process() m_haExtension.process(m_isMqttConnected); } -void MqttApiTopicHandler::notify(IPluginMaintenance* plugin, const String& topic) +void MqttApiTopicHandler::notify(const String& deviceId, const String& entityId, const String& topic) { - if ((nullptr != plugin) && + if ((false == deviceId.isEmpty()) && + (false == entityId.isEmpty()) && (false == topic.isEmpty())) { ListOfTopicStates::iterator topicStateIt = m_listOfTopicStates.begin(); @@ -193,7 +283,8 @@ void MqttApiTopicHandler::notify(IPluginMaintenance* plugin, const String& topic TopicState* topicState = *topicStateIt; if ((nullptr != topicState) && - (plugin == topicState->plugin) && + (deviceId == topicState->deviceId) && + (entityId == topicState->entityId) && (topic == topicState->topic)) { topicState->isPublishReq = true; @@ -212,141 +303,7 @@ void MqttApiTopicHandler::notify(IPluginMaintenance* plugin, const String& topic * Private Methods *****************************************************************************/ -String MqttApiTopicHandler::getBaseUriByUid(uint16_t uid) -{ - String baseUri; - - if (true == m_hostname.isEmpty()) - { - SettingsService& settingsService = SettingsService::getInstance(); - - if (false == settingsService.open(true)) - { - m_hostname = settingsService.getHostname().getDefault(); - } - else - { - m_hostname = settingsService.getHostname().getValue(); - settingsService.close(); - } - } - - if (false == m_hostname.isEmpty()) - { - baseUri += m_hostname; - baseUri += "/"; - } - - baseUri += "uid/"; - baseUri += uid; - - return baseUri; -} - -String MqttApiTopicHandler::getBaseUriByAlias(const String& alias) -{ - String baseUri; - - if (true == m_hostname.isEmpty()) - { - SettingsService& settingsService = SettingsService::getInstance(); - - if (false == settingsService.open(true)) - { - m_hostname = settingsService.getHostname().getDefault(); - } - else - { - m_hostname = settingsService.getHostname().getValue(); - settingsService.close(); - } - } - - if (false == m_hostname.isEmpty()) - { - baseUri += m_hostname; - baseUri += "/"; - } - - baseUri += "alias/"; - baseUri += alias; - - return baseUri; -} - -void MqttApiTopicHandler::registerTopic(IPluginMaintenance* plugin, const String& topic, Access access, JsonObjectConst& extra, const String& baseUri) -{ - if (nullptr != plugin) - { - String topicUri = baseUri + topic; - TopicState* topicState = new(std::nothrow) TopicState(); - - LOG_INFO("[%s][%u] Register: %s", plugin->getName(), plugin->getUID(), topicUri.c_str()); - - if (nullptr != topicState) - { - String topicUriReadable; - String topicUriWriteable; - - topicState->plugin = plugin; - topicState->topic = topic; - topicState->access = access; - topicState->topicUri = topicUri; - topicState->isPublishReq = false; - - /* Is the topic readable? */ - if ((ACCESS_READ_ONLY == access) || - (ACCESS_READ_WRITE == access)) - { - topicUriReadable = topicUri + MQTT_ENDPOINT_READ_ACCESS; - - /* Publish initially. */ - topicState->isPublishReq = true; - } - - /* Is the topic writeable? */ - if ((ACCESS_READ_WRITE == access) || - (ACCESS_WRITE_ONLY == access)) - { - MqttService& mqttService = MqttService::getInstance(); - MqttService::TopicCallback setCallback = [this, plugin, topic](const String& topicUri, const uint8_t* payload, size_t size) { - if (0U != topicUri.endsWith(topic + MQTT_ENDPOINT_WRITE_ACCESS)) - { - this->write(plugin, topic, payload, size); - } - }; - - topicUriWriteable = topicUri + MQTT_ENDPOINT_WRITE_ACCESS; - - if (false == mqttService.subscribe(topicUriWriteable, setCallback)) - { - LOG_WARNING("Couldn't subscribe %s.", topicUriWriteable.c_str()); - } - else - { - LOG_INFO("[%u] Subscribed: %s", plugin->getUID(), topicUriWriteable.c_str()); - } - } - - /* Handle Home Assistant extension */ - { - int dividerIdx = baseUri.lastIndexOf("/"); - - if (0 <= dividerIdx) - { - String haObjectId = baseUri.substring(dividerIdx + 1); - String willTopic = m_hostname + "/status"; - - m_haExtension.registerMqttDiscovery(m_hostname, haObjectId, topicUriReadable, topicUriWriteable, willTopic, extra); - } - } - - m_listOfTopicStates.push_back(topicState); - } - } -} - -void MqttApiTopicHandler::write(IPluginMaintenance* plugin, const String& topic, const uint8_t* payload, size_t size) +void MqttApiTopicHandler::write(const String& deviceId, const String& entityId, const String& topic, const uint8_t* payload, size_t size, SetTopicFunc setTopicFunc, UploadReqFunc uploadReqFunc) { const size_t JSON_DOC_SIZE = 1024U; DynamicJsonDocument jsonDoc(JSON_DOC_SIZE); @@ -368,9 +325,13 @@ void MqttApiTopicHandler::write(IPluginMaintenance* plugin, const String& topic, String dstFullPath; /* Ask plugin, whether the upload is allowed or not. */ - if (false == plugin->isUploadAccepted(topic, jsonFileName.as(), dstFullPath)) + if (nullptr == uploadReqFunc) { - LOG_WARNING("[%s][%u] Upload not supported.", plugin->getName(), plugin->getUID()); + LOG_WARNING("[%s] Upload not supported.", entityId.c_str()); + } + else if (false == uploadReqFunc(topic, jsonFileName.as(), dstFullPath)) + { + LOG_WARNING("[%s] Upload not supported.", entityId.c_str()); } else { @@ -380,12 +341,12 @@ void MqttApiTopicHandler::write(IPluginMaintenance* plugin, const String& topic, if (MBEDTLS_ERR_BASE64_INVALID_CHARACTER == decodeRet) { - LOG_WARNING("[%s][%u] File encoding contains invalid character.", plugin->getName(), plugin->getUID(), fileSize); + LOG_WARNING("[%s] File encoding contains invalid character.", entityId.c_str(), fileSize); } else if ((MAX_FILE_SIZE < fileSize) || (0U == fileSize)) { - LOG_WARNING("[%s][%u] File size %u not supported.", plugin->getName(), plugin->getUID(), fileSize); + LOG_WARNING("[%s] File size %u not supported.", entityId.c_str(), fileSize); } else { @@ -399,7 +360,7 @@ void MqttApiTopicHandler::write(IPluginMaintenance* plugin, const String& topic, if (0U != decodeRet) { - LOG_WARNING("[%s][%u] File decode error: %d", plugin->getName(), plugin->getUID(), decodeRet); + LOG_WARNING("[%s] File decode error: %d", entityId.c_str(), decodeRet); } else { @@ -408,7 +369,7 @@ void MqttApiTopicHandler::write(IPluginMaintenance* plugin, const String& topic, if (false == fd) { - LOG_ERROR("Couldn't create file: %s", dstFullPath.c_str()); + LOG_ERROR("[%s] Couldn't create file: %s", entityId.c_str(), dstFullPath.c_str()); } else { @@ -428,112 +389,38 @@ void MqttApiTopicHandler::write(IPluginMaintenance* plugin, const String& topic, } } - if (false == plugin->setTopic(topic, jsonDoc.as())) - { - LOG_WARNING("Plugin %u rejected payload.", plugin->getUID()); - } - } -} - -void MqttApiTopicHandler::unregisterTopic(IPluginMaintenance* plugin, const String& topic, const String& baseUri) -{ - if (nullptr != plugin) - { - String topicUri = baseUri + topic; - MqttService& mqttService = MqttService::getInstance(); - ListOfTopicStates::iterator topicStateIt = m_listOfTopicStates.begin(); - - LOG_INFO("[%s][%u] Unregister: %s", plugin->getName(), plugin->getUID(), topicUri.c_str()); - - while(m_listOfTopicStates.end() != topicStateIt) + if (false == setTopicFunc(topic, jsonDoc.as())) { - TopicState* topicState = *topicStateIt; - - if ((nullptr != topicState) && - (plugin == topicState->plugin) && - (topic == topicState->topic)) - { - String topicUriReadable; - String topicUriWriteable; - - if ((ACCESS_READ_ONLY == topicState->access) || - (ACCESS_READ_WRITE == topicState->access)) - { - topicUriReadable = topicUri + MQTT_ENDPOINT_READ_ACCESS; - - /* Purge topic */ - if (false == mqttService.publish(topicUriReadable, "")) - { - LOG_WARNING("[%u] Failed to purge: %s", plugin->getUID(), topicUriReadable.c_str()); - } - else - { - LOG_INFO("[%u] Purged: %s", plugin->getUID(), topicUriReadable.c_str()); - } - } - - if ((ACCESS_READ_WRITE == topicState->access) || - (ACCESS_WRITE_ONLY == topicState->access)) - { - topicUriWriteable = topicUri + MQTT_ENDPOINT_WRITE_ACCESS; - - mqttService.unsubscribe(topicUriWriteable); - - LOG_INFO("[%u] Unsubscribed: %s", plugin->getUID(), topicUriWriteable.c_str()); - } - - /* Handle Home Assistant extension */ - { - /* The object id is the last directory of the base URI. - * Its the plugin UID or the plugin alias. - */ - int dividerIdx = baseUri.lastIndexOf("/"); - - if (0 <= dividerIdx) - { - String haObjectId = baseUri.substring(dividerIdx + 1); - - m_haExtension.unregisterMqttDiscovery(m_hostname, haObjectId, topicUriReadable, topicUriWriteable); - } - } - - topicStateIt = m_listOfTopicStates.erase(topicStateIt); - - delete topicState; - topicState = nullptr; - } - else - { - ++topicStateIt; - } + LOG_WARNING("[%s] Payload rejected.", entityId.c_str()); } } } -void MqttApiTopicHandler::publish(const String& baseUri, IPluginMaintenance* plugin, const String& topic) +void MqttApiTopicHandler::publish(const String& deviceId, const String& entityId, const String& topic, GetTopicFunc getTopicFunc) { - if (nullptr != plugin) + if (nullptr != getTopicFunc) { - const size_t JSON_DOC_SIZE = 1024U; + const size_t JSON_DOC_SIZE = 1024U; DynamicJsonDocument jsonDoc(JSON_DOC_SIZE); - JsonObject jsonObj = jsonDoc.createNestedObject("data"); + JsonObject jsonObj = jsonDoc.createNestedObject("data"); + String mqttTopicNameBase = deviceId + "/" + entityId + topic; - if (true == plugin->getTopic(topic, jsonObj)) + if (true == getTopicFunc(topic, jsonObj)) { String topicContent; if (0U < serializeJson(jsonDoc["data"], topicContent)) { MqttService& mqttService = MqttService::getInstance(); - String topicStateUri = baseUri + MQTT_ENDPOINT_READ_ACCESS; + String topicStateUri = mqttTopicNameBase + MQTT_ENDPOINT_READ_ACCESS; if (false == mqttService.publish(topicStateUri, topicContent)) { - LOG_WARNING("Couldn't publish %s.", topicStateUri.c_str()); + LOG_WARNING("[%s] Couldn't publish %s.", entityId.c_str(), topicStateUri.c_str()); } else { - LOG_INFO("[%s][%u] Published: %s", plugin->getName(), plugin->getUID(), topicStateUri.c_str()); + LOG_INFO("[%s] Published: %s", entityId.c_str(), topicStateUri.c_str()); } } } @@ -551,12 +438,12 @@ void MqttApiTopicHandler::clearTopicStates() if (nullptr != topicState) { - if ((ACCESS_READ_WRITE == topicState->access) || - (ACCESS_WRITE_ONLY == topicState->access)) + if (nullptr != topicState->setTopicFunc) { - String topicUri = topicState->topicUri + MQTT_ENDPOINT_WRITE_ACCESS; + String mqttTopicNameBase = topicState->deviceId + "/" + topicState->entityId + topicState->topic; + String topicStateUri = mqttTopicNameBase + MQTT_ENDPOINT_WRITE_ACCESS; - mqttService.unsubscribe(topicUri); + mqttService.unsubscribe(topicStateUri); } topicStateIt = m_listOfTopicStates.erase(topicStateIt); diff --git a/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.h b/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.h index 86dc5e73..fad92ecd 100644 --- a/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.h +++ b/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.h @@ -45,7 +45,6 @@ *****************************************************************************/ #include #include -#include #include #include "HomeAssistantMqtt.h" @@ -70,7 +69,6 @@ class MqttApiTopicHandler : public ITopicHandler */ MqttApiTopicHandler() : ITopicHandler(), - m_hostname(), m_listOfTopicStates(), m_isMqttConnected(false), m_haExtension() @@ -96,22 +94,26 @@ class MqttApiTopicHandler : public ITopicHandler void stop() final; /** - * Register a single topic of the given plugin. + * Register the topic. * - * @param[in] plugin The plugin which provides the topic. - * @param[in] topic The topic name. - * @param[in] access The topic accessibility. - * @param[in] extra Extra parameters, which depend on the topic handler. + * @param[in] deviceId The device id which represents the physical device. + * @param[in] entityId The entity id which represents the entity of the device. + * @param[in] topic The topic name. + * @param[in] extra Extra parameters, which depend on the topic handler. + * @param[in] getTopicFunc Function to get the topic content. + * @param[in] setTopicFunc Function to set the topic content. + * @param[in] uploadReqFunc Function used for requesting whether an file upload is allowed. */ - void registerTopic(IPluginMaintenance* plugin, const String& topic, Access access, JsonObjectConst& extra) final; + void registerTopic(const String& deviceId, const String& entityId, const String& topic, JsonObjectConst& extra, GetTopicFunc getTopicFunc, SetTopicFunc setTopicFunc, UploadReqFunc uploadReqFunc) final; /** - * Unregister the topic of the given plugin. + * Unregister the topic. * - * @param[in] plugin The plugin which provides the topic. + * @param[in] deviceId The device id which represents the physical device. + * @param[in] entityId The entity id which represents the entity of the device. * @param[in] topic The topic name. */ - void unregisterTopic(IPluginMaintenance* plugin, const String& topic) final; + void unregisterTopic(const String& deviceId, const String& entityId, const String& topic) final; /** * Process the topic handler. @@ -121,28 +123,33 @@ class MqttApiTopicHandler : public ITopicHandler /** * Notify that the topic has changed. * - * @param[in] plugin The plugin which provides the topic. + * @param[in] deviceId The device id which represents the physical device. + * @param[in] entityId The entity id which represents the entity of the device. * @param[in] topic The topic name. */ - void notify(IPluginMaintenance* plugin, const String& topic) final; + void notify(const String& deviceId, const String& entityId, const String& topic) final; private: /** A topic state is published by a plugin. */ struct TopicState { - IPluginMaintenance* plugin; /**< The plugin which provides the topic. */ - String topic; /**< The topic which provides its state. */ - String topicUri; /**< The topic MQTT URI without endpoint. */ - Access access; /**< The topic accessibility. */ - bool isPublishReq; /**< Is it required to publish the state? */ + String deviceId; /**< The device id. */ + String entityId; /**< The entity id. */ + String topic; /**< The topic which provides its state. */ + GetTopicFunc getTopicFunc; /**< Function used to get topic content. */ + SetTopicFunc setTopicFunc; /**< Function used to set topic content. */ + UploadReqFunc uploadReqFunc; /**< Function used to check whether a file upload is allowed. */ + bool isPublishReq; /**< Is it required to publish the state? */ /** Construct topic state. */ TopicState() : - plugin(nullptr), + deviceId(), + entityId(), topic(), - topicUri(), - access(ACCESS_READ_WRITE), + getTopicFunc(nullptr), + setTopicFunc(nullptr), + uploadReqFunc(nullptr), isPublishReq(false) { } @@ -162,70 +169,35 @@ class MqttApiTopicHandler : public ITopicHandler /** MQTT path endpoint for write access. */ static const char* MQTT_ENDPOINT_WRITE_ACCESS; - String m_hostname; /**< Hostname cache used for the base URI */ - ListOfTopicStates m_listOfTopicStates; /**< List of registered plugins. */ + ListOfTopicStates m_listOfTopicStates; /**< List of registered topic states. */ bool m_isMqttConnected; /**< Is the MQTT connection to the broker established? */ HomeAssistantMqtt m_haExtension; /**< Home Assistant extension */ MqttApiTopicHandler(const MqttApiTopicHandler& adapter); MqttApiTopicHandler& operator=(const MqttApiTopicHandler& adapter); - /** - * Get plugin MQTT base URI to identify plugin by UID. - * - * @param[in] uid Plugin UID - * - * @return Plugin MQTT API base URI - */ - String getBaseUriByUid(uint16_t uid); - - /** - * Get plugin MQTT base URI to identify plugin by alias name. - * - * @param[in] alias Plugin alias name - * - * @return Plugin MQTT API base URI - */ - String getBaseUriByAlias(const String& alias); - - /** - * Register a single topic of the given plugin. - * - * @param[in] plugin The plugin which provides the topic. - * @param[in] topic The topic name. - * @param[in] access The topic accessibility. - * @param[in] extra Extra parameters, which depend on the topic handler. - * @param[in] baseUri The REST API base URI which to use. - */ - void registerTopic(IPluginMaintenance* plugin, const String& topic, Access access, JsonObjectConst& extra, const String& baseUri); - /** * Write topic data. * - * @param[in] plugin The plugin which relates to the topic. - * @param[in] topicUri The topic URI - * @param[in] payload The payload data. - * @param[in] size The payload size in byte. - */ - void write(IPluginMaintenance* plugin, const String& topicUri, const uint8_t* payload, size_t size); - - /** - * Unregister a single topic of the given plugin. - * - * @param[in] plugin The related plugin. - * @param[in] topic The topic. - * @param[in] baseUri The MQTT API base URI. + * @param[in] deviceId The device id which represents the physical device. + * @param[in] entityId The entity id which represents the entity of the device. + * @param[in] topic The topic name. + * @param[in] payload The payload data. + * @param[in] size The payload size in byte. + * @param[in] setTopicFunc Function to set the topic content. + * @param[in] uploadReqFunc Function used for requesting whether an file upload is allowed. */ - void unregisterTopic(IPluginMaintenance* plugin, const String& topic, const String& baseUri); + void write(const String& deviceId, const String& entityId, const String& topic, const uint8_t* payload, size_t size, SetTopicFunc setTopicFunc, UploadReqFunc uploadReqFunc); /** - * Publish plugin specific topic data. + * Publish topic data. * - * @param[in] baseUri The MQTT API base URI. - * @param[in] plugin The related plugin. - * @param[in] topic The topic. + * @param[in] deviceId The device id which represents the physical device. + * @param[in] entityId The entity id which represents the entity of the device. + * @param[in] topic The topic name. + * @param[in] getTopicFunc Function to get the topic content. */ - void publish(const String& baseUri, IPluginMaintenance* plugin, const String& topic); + void publish(const String& deviceId, const String& entityId, const String& topic, GetTopicFunc getTopicFunc); /** * Clear all topic states. diff --git a/lib/MqttService/src/MqttService.cpp b/lib/MqttService/src/MqttService.cpp index 1c7b7b1a..8582d554 100644 --- a/lib/MqttService/src/MqttService.cpp +++ b/lib/MqttService/src/MqttService.cpp @@ -343,7 +343,7 @@ void MqttService::disconnectedState() void MqttService::connectedState() { /* Connection with broker lost? */ - if (false == m_mqttClient.connected()) + if (false == m_mqttClient.loop()) { LOG_INFO("Connection to MQTT broker disconnected."); m_state = STATE_DISCONNECTED; @@ -351,11 +351,6 @@ void MqttService::connectedState() /* Try to reconnect later. */ m_reconnectTimer.restart(); } - /* Connection to broker still established. */ - else - { - (void)m_mqttClient.loop(); - } } void MqttService::idleState() diff --git a/lib/OpenWeatherPlugin/web/OpenWeatherPlugin.html b/lib/OpenWeatherPlugin/web/OpenWeatherPlugin.html index 7cf62a51..e675a355 100644 --- a/lib/OpenWeatherPlugin/web/OpenWeatherPlugin.html +++ b/lib/OpenWeatherPlugin/web/OpenWeatherPlugin.html @@ -37,15 +37,15 @@

                    OpenWeatherPlugin

                    The plugin shows the current weather condition (icon and temperature) and one aditional information (see configuration below) provided by https://openweathermap.org/.

                    REST API

                    Get the OpenWeather related configuration .

                    -
                    GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/weather
                    -
                    GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/weather
                    +
                    GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/weather
                    +
                    GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/weather
                    • PLUGIN-UID: The plugin unique id.
                    • PLUGIN-ALIAS: The plugin alias name.

                    Set OpenWeather related configuration.

                    -
                    POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/weather?apiKey=<API-KEY>&latitude=<LATITUDE>&longitude=<LONGITUDE>&other=<OTHER>&units=<UNITS>
                    -
                    POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/weather?apiKey=<API-KEY>&latitude=<LATITUDE>&longitude=<LONGITUDE>&other=<OTHER>&units=<UNITS>
                    +
                    POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/weather?apiKey=<API-KEY>&latitude=<LATITUDE>&longitude=<LONGITUDE>&other=<OTHER>&units=<UNITS>
                    +
                    POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/weather?apiKey=<API-KEY>&latitude=<LATITUDE>&longitude=<LONGITUDE>&other=<OTHER>&units=<UNITS>
                    • PLUGIN-UID: The plugin unique id.
                    • PLUGIN-ALIAS: The plugin alias name.
                    • @@ -171,7 +171,7 @@

                      Configuration

                      disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/weather", + url: "/rest/api/v1/display/" + pluginUid + "/weather", isJsonResponse: true }).then(function(rsp) { $("#apiKey").val(rsp.data.apiKey); @@ -191,7 +191,7 @@

                      Configuration

                      return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/weather", + url: "/rest/api/v1/display/" + pluginUid + "/weather", isJsonResponse: true, parameter: { apiKey: $("#apiKey").val(), diff --git a/lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp b/lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp index 141c94bf..be4f33fc 100644 --- a/lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp +++ b/lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp @@ -66,56 +66,80 @@ * Public Methods *****************************************************************************/ -void RestApiTopicHandler::registerTopic(IPluginMaintenance* plugin, const String& topic, Access access, JsonObjectConst& extra) +void RestApiTopicHandler::registerTopic(const String& deviceId, const String& entityId, const String& topic, JsonObjectConst& extra, GetTopicFunc getTopicFunc, SetTopicFunc setTopicFunc, UploadReqFunc uploadReqFunc) { - if ((nullptr != plugin) && + if ((false == deviceId.isEmpty()) && + (false == entityId.isEmpty()) && (false == topic.isEmpty())) { - String baseUriByUid = getBaseUriByUid(plugin->getUID()); + TopicMetaData* topicMetaData = new(std::nothrow) TopicMetaData(); - registerTopic(plugin, topic, access, extra, baseUriByUid); - - if (false == plugin->getAlias().isEmpty()) + if (nullptr != topicMetaData) { - String baseUriByAlias = getBaseUriByAlias(plugin->getAlias()); + String baseUri = getBaseUri(entityId); + ArRequestHandlerFunction onRequest = + [this, topicMetaData](AsyncWebServerRequest *request) + { + this->webReqHandler(request, topicMetaData); + }; + ArUploadHandlerFunction onUpload = + [this, topicMetaData](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) + { + this->uploadHandler(request, filename, index, data, len, final, topicMetaData); + }; + + topicMetaData->deviceId = deviceId; + topicMetaData->entityId = entityId; + topicMetaData->topic = topic; + topicMetaData->getTopicFunc = getTopicFunc; + topicMetaData->setTopicFunc = setTopicFunc; + topicMetaData->uploadReqFunc = uploadReqFunc; + topicMetaData->uri = baseUri + topic; + topicMetaData->webHandler = &MyWebServer::getInstance().on(topicMetaData->uri.c_str(), HTTP_ANY, onRequest, onUpload); + + UTIL_NOT_USED(extra); - registerTopic(plugin, topic, access, extra, baseUriByAlias); + m_listOfTopicMetaData.push_back(topicMetaData); + + LOG_INFO("[%s] Registered: %s", entityId.c_str(), topicMetaData->uri.c_str()); } } } -void RestApiTopicHandler::unregisterTopic(IPluginMaintenance* plugin, const String& topic) +void RestApiTopicHandler::unregisterTopic(const String& deviceId, const String& entityId, const String& topic) { - if ((nullptr != plugin) && + if ((false == deviceId.isEmpty()) && + (false == entityId.isEmpty()) && (false == topic.isEmpty())) { - PluginTopicList::iterator pluginTopicListIt = m_pluginTopicList.begin(); + ListOfTopicMetaData::iterator topicMetaDataIt = m_listOfTopicMetaData.begin(); - while(m_pluginTopicList.end() != pluginTopicListIt) + while(m_listOfTopicMetaData.end() != topicMetaDataIt) { - PluginTopic* pluginTopic = *pluginTopicListIt; + TopicMetaData* topicMetaData = *topicMetaDataIt; - if ((nullptr != pluginTopic) && - (plugin == pluginTopic->plugin) && - (topic == pluginTopic->topic)) + if ((nullptr != topicMetaData) && + (deviceId == topicMetaData->deviceId) && + (entityId == topicMetaData->entityId) && + (topic == topicMetaData->topic)) { - if (false == MyWebServer::getInstance().removeHandler(pluginTopic->webHandler)) + if (false == MyWebServer::getInstance().removeHandler(topicMetaData->webHandler)) { - LOG_WARNING("[%s][%u] Failed to unregister: %s", pluginTopic->plugin->getName(), pluginTopic->plugin->getUID(), pluginTopic->uri.c_str()); + LOG_WARNING("[%s] Failed to unregister: %s", topicMetaData->entityId.c_str(), topicMetaData->uri.c_str()); } else { - LOG_INFO("[%s][%u] Unregistered: %s", pluginTopic->plugin->getName(), pluginTopic->plugin->getUID(), pluginTopic->uri.c_str()); + LOG_INFO("[%s] Unregistered: %s", topicMetaData->entityId.c_str(), topicMetaData->uri.c_str()); } - pluginTopicListIt = m_pluginTopicList.erase(pluginTopicListIt); + topicMetaDataIt = m_listOfTopicMetaData.erase(topicMetaDataIt); - delete pluginTopic; - pluginTopic = nullptr; + delete topicMetaData; + topicMetaData = nullptr; } else { - ++pluginTopicListIt; + ++topicMetaDataIt; } } } @@ -129,83 +153,37 @@ void RestApiTopicHandler::unregisterTopic(IPluginMaintenance* plugin, const Stri * Private Methods *****************************************************************************/ -String RestApiTopicHandler::getBaseUriByUid(uint16_t uid) -{ - String baseUri = RestApi::BASE_URI; - baseUri += "/display"; - baseUri += "/uid/"; - baseUri += uid; - - return baseUri; -} - -String RestApiTopicHandler::getBaseUriByAlias(const String& alias) +String RestApiTopicHandler::getBaseUri(const String& entityId) { String baseUri = RestApi::BASE_URI; - baseUri += "/display"; - baseUri += "/alias/"; - baseUri += alias; + baseUri += "/display/"; + baseUri += entityId; return baseUri; } -void RestApiTopicHandler::registerTopic(IPluginMaintenance* plugin, const String& topic, Access access, JsonObjectConst& extra, const String& baseUri) -{ - if (nullptr != plugin) - { - PluginTopic* pluginTopic = new(std::nothrow) PluginTopic(); - - if (nullptr != pluginTopic) - { - pluginTopic->plugin = plugin; - pluginTopic->topic = topic; - pluginTopic->access = access; - pluginTopic->uri = baseUri + topic; - pluginTopic->webHandler = &MyWebServer::getInstance().on( - pluginTopic->uri.c_str(), - HTTP_ANY, - [this, pluginTopic](AsyncWebServerRequest *request) - { - this->webReqHandler(request, pluginTopic); - }, - [this, pluginTopic](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) - { - this->uploadHandler(request, filename, index, data, len, final, pluginTopic); - }); - - UTIL_NOT_USED(extra); - - m_pluginTopicList.push_back(pluginTopic); - - LOG_INFO("[%s][%u] Registered: %s", pluginTopic->plugin->getName(), pluginTopic->plugin->getUID(), pluginTopic->uri.c_str()); - } - } -} - -void RestApiTopicHandler::webReqHandler(AsyncWebServerRequest *request, PluginTopic* pluginTopic) +void RestApiTopicHandler::webReqHandler(AsyncWebServerRequest *request, TopicMetaData* topicMetaData) { String content; - const size_t JSON_DOC_SIZE = 1024U; + const size_t JSON_DOC_SIZE = 2048U; DynamicJsonDocument jsonDoc(JSON_DOC_SIZE); JsonObject dataObj = jsonDoc.createNestedObject("data"); uint32_t httpStatusCode = HttpStatus::STATUS_CODE_OK; if ((nullptr == request) || - (nullptr == pluginTopic) || - (nullptr == pluginTopic->plugin)) + (nullptr == topicMetaData)) { return; } if ((HTTP_GET == request->method()) && - ( - (ACCESS_READ_ONLY == pluginTopic->access) || - (ACCESS_READ_WRITE == pluginTopic->access) - )) + (nullptr != topicMetaData->getTopicFunc)) { /* Topic data will be transported in the HTTP body as JSON. */ - if (false == pluginTopic->plugin->getTopic(pluginTopic->topic, dataObj)) + if (false == topicMetaData->getTopicFunc(topicMetaData->topic, dataObj)) { + LOG_WARNING("[%s] Topic \"%s\" not supported.", topicMetaData->entityId, topicMetaData->topic); + RestUtil::prepareRspError(jsonDoc, "Requested topic not supported."); jsonDoc.remove("data"); @@ -218,34 +196,35 @@ void RestApiTopicHandler::webReqHandler(AsyncWebServerRequest *request, PluginTo httpStatusCode = HttpStatus::STATUS_CODE_OK; } } - else if ((HTTP_POST == request->method())&& - ( - (ACCESS_READ_WRITE == pluginTopic->access) || - (ACCESS_WRITE_ONLY == pluginTopic->access) - )) + else if ((HTTP_POST == request->method()) && + (nullptr != topicMetaData->setTopicFunc)) { DynamicJsonDocument jsonDocPar(JSON_DOC_SIZE); + JsonObject jsonValue; /* Topic data is in the HTTP parameters and needs to be converted to JSON. */ par2Json(jsonDocPar, request); /* Add uploaded file */ - if ((false == pluginTopic->isUploadError) && - (false == pluginTopic->fullPath.isEmpty())) + if ((false == topicMetaData->isUploadError) && + (false == topicMetaData->fullPath.isEmpty())) { - jsonDocPar["fullPath"] = pluginTopic->fullPath; + jsonDocPar["fullPath"] = topicMetaData->fullPath; } - if (false == pluginTopic->plugin->setTopic(pluginTopic->topic, jsonDocPar.as())) + jsonValue = jsonDocPar.as(); /* Assign after par2Json conversion! Otherwise there will be a empty object. */ + if (false == topicMetaData->setTopicFunc(topicMetaData->topic, jsonValue)) { + LOG_WARNING("[%s] Topic \"%s\" not supported or invalid data.", topicMetaData->entityId, topicMetaData->topic); + RestUtil::prepareRspError(jsonDoc, "Requested topic not supported or invalid data."); jsonDoc.remove("data"); /* If a file is available, it will be removed now. */ - if (false == pluginTopic->fullPath.isEmpty()) + if (false == topicMetaData->fullPath.isEmpty()) { - (void)FILESYSTEM.remove(pluginTopic->fullPath); + (void)FILESYSTEM.remove(topicMetaData->fullPath); } httpStatusCode = HttpStatus::STATUS_CODE_NOT_FOUND; @@ -268,7 +247,7 @@ void RestApiTopicHandler::webReqHandler(AsyncWebServerRequest *request, PluginTo RestUtil::sendJsonRsp(request, jsonDoc, httpStatusCode); } -void RestApiTopicHandler::uploadHandler(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final, PluginTopic* pluginTopic) +void RestApiTopicHandler::uploadHandler(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final, TopicMetaData* topicMetaData) { /* Begin of upload? */ if (0 == index) @@ -291,48 +270,49 @@ void RestApiTopicHandler::uploadHandler(AsyncWebServerRequest *request, const St if (fileSystemSpace <= fileSize) { LOG_WARNING("Upload of %s aborted. Not enough space.", filename.c_str()); - pluginTopic->isUploadError = true; - pluginTopic->fullPath.clear(); + topicMetaData->isUploadError = true; + topicMetaData->fullPath.clear(); } else { LOG_INFO("Upload of %s (%d bytes) starts.", filename.c_str(), fileSize); - pluginTopic->isUploadError = false; - pluginTopic->fullPath.clear(); + topicMetaData->isUploadError = false; + topicMetaData->fullPath.clear(); /* Ask plugin, whether the upload is allowed or not. */ - if (false == pluginTopic->plugin->isUploadAccepted(pluginTopic->topic, filename, pluginTopic->fullPath)) + if ((nullptr == topicMetaData->uploadReqFunc) || + (false == topicMetaData->uploadReqFunc(topicMetaData->topic, filename, topicMetaData->fullPath))) { - LOG_WARNING("[%s][%u] Upload not supported.", pluginTopic->plugin->getName(), pluginTopic->plugin->getUID()); - pluginTopic->isUploadError = true; - pluginTopic->fullPath.clear(); + LOG_WARNING("[%s] Upload not supported.", topicMetaData->entityId.c_str()); + topicMetaData->isUploadError = true; + topicMetaData->fullPath.clear(); } else { /* Create a new file and overwrite a existing one. */ - pluginTopic->fd = FILESYSTEM.open(pluginTopic->fullPath, "w"); + topicMetaData->fd = FILESYSTEM.open(topicMetaData->fullPath, "w"); - if (false == pluginTopic->fd) + if (false == topicMetaData->fd) { - LOG_ERROR("Couldn't create file: %s", pluginTopic->fullPath.c_str()); - pluginTopic->isUploadError = true; - pluginTopic->fullPath.clear(); + LOG_ERROR("Couldn't create file: %s", topicMetaData->fullPath.c_str()); + topicMetaData->isUploadError = true; + topicMetaData->fullPath.clear(); } } } } - if (false == pluginTopic->isUploadError) + if (false == topicMetaData->isUploadError) { /* If file is open, write data to it. */ - if (true == pluginTopic->fd) + if (true == topicMetaData->fd) { - if (len != pluginTopic->fd.write(data, len)) + if (len != topicMetaData->fd.write(data, len)) { LOG_ERROR("Less data written, upload aborted."); - pluginTopic->isUploadError = true; - pluginTopic->fullPath.clear(); - pluginTopic->fd.close(); + topicMetaData->isUploadError = true; + topicMetaData->fullPath.clear(); + topicMetaData->fd.close(); } } @@ -341,7 +321,7 @@ void RestApiTopicHandler::uploadHandler(AsyncWebServerRequest *request, const St { LOG_INFO("Upload of %s finished.", filename.c_str()); - pluginTopic->fd.close(); + topicMetaData->fd.close(); } } } @@ -413,24 +393,24 @@ void RestApiTopicHandler::par2Json(JsonDocument& jsonDocPar, AsyncWebServerReque void RestApiTopicHandler::clearPluginTopics() { - PluginTopicList::iterator pluginTopicListIt = m_pluginTopicList.begin(); + ListOfTopicMetaData::iterator topicMetaDataIt = m_listOfTopicMetaData.begin(); - while(m_pluginTopicList.end() != pluginTopicListIt) + while(m_listOfTopicMetaData.end() != topicMetaDataIt) { - PluginTopic* pluginTopic = *pluginTopicListIt; + TopicMetaData* topicMetaData = *topicMetaDataIt; - if (nullptr != pluginTopic) + if (nullptr != topicMetaData) { - (void)MyWebServer::getInstance().removeHandler(pluginTopic->webHandler); + (void)MyWebServer::getInstance().removeHandler(topicMetaData->webHandler); - pluginTopicListIt = m_pluginTopicList.erase(pluginTopicListIt); + topicMetaDataIt = m_listOfTopicMetaData.erase(topicMetaDataIt); - delete pluginTopic; - pluginTopic = nullptr; + delete topicMetaData; + topicMetaData = nullptr; } else { - ++pluginTopicListIt; + ++topicMetaDataIt; } } } diff --git a/lib/RestApiTopicHandler/src/RestApiTopicHandler.h b/lib/RestApiTopicHandler/src/RestApiTopicHandler.h index 4c92fb93..94607446 100644 --- a/lib/RestApiTopicHandler/src/RestApiTopicHandler.h +++ b/lib/RestApiTopicHandler/src/RestApiTopicHandler.h @@ -45,7 +45,6 @@ *****************************************************************************/ #include #include -#include #include #include @@ -69,7 +68,7 @@ class RestApiTopicHandler : public ITopicHandler */ RestApiTopicHandler() : ITopicHandler(), - m_pluginTopicList() + m_listOfTopicMetaData() { } @@ -98,22 +97,26 @@ class RestApiTopicHandler : public ITopicHandler } /** - * Register a single topic of the given plugin. + * Register the topic. * - * @param[in] plugin The plugin which provides the topic. - * @param[in] topic The topic name. - * @param[in] access The topic accessibility. - * @param[in] extra Extra parameters, which depend on the topic handler. + * @param[in] deviceId The device id which represents the physical device. + * @param[in] entityId The entity id which represents the entity of the device. + * @param[in] topic The topic name. + * @param[in] extra Extra parameters, which depend on the topic handler. + * @param[in] getTopicFunc Function to get the topic content. + * @param[in] setTopicFunc Function to set the topic content. + * @param[in] uploadReqFunc Function used for requesting whether an file upload is allowed. */ - void registerTopic(IPluginMaintenance* plugin, const String& topic, Access access, JsonObjectConst& extra) final; + void registerTopic(const String& deviceId, const String& entityId, const String& topic, JsonObjectConst& extra, GetTopicFunc getTopicFunc, SetTopicFunc setTopicFunc, UploadReqFunc uploadReqFunc) final; /** - * Unregister the topic of the given plugin. + * Unregister the topic. * - * @param[in] plugin The plugin which provides the topic. + * @param[in] deviceId The device id which represents the physical device. + * @param[in] entityId The entity id which represents the entity of the device. * @param[in] topic The topic name. */ - void unregisterTopic(IPluginMaintenance* plugin, const String& topic) final; + void unregisterTopic(const String& deviceId, const String& entityId, const String& topic) final; /** * Process the topic handler. @@ -126,105 +129,95 @@ class RestApiTopicHandler : public ITopicHandler /** * Notify that the topic has changed. * - * @param[in] plugin The plugin which provides the topic. + * @param[in] deviceId The device id which represents the physical device. + * @param[in] entityId The entity id which represents the entity of the device. * @param[in] topic The topic name. */ - void notify(IPluginMaintenance* plugin, const String& topic) final + void notify(const String& deviceId, const String& entityId, const String& topic) final { /* Nothing to do. */ + (void)deviceId; + (void)entityId; + (void)topic; } private: /** - * Plugin topic relevant data. + * Topic meta data. */ - struct PluginTopic + struct TopicMetaData { - IPluginMaintenance* plugin; /**< The plugin which provides the topic. */ + String deviceId; /**< The device id. */ + String entityId; /**< The entity id. */ String topic; /**< The plugin topic. */ + GetTopicFunc getTopicFunc; /**< Function used to get topic content. */ + SetTopicFunc setTopicFunc; /**< Function used to set topic content. */ + UploadReqFunc uploadReqFunc; /**< Function used to check whether a file upload is allowed. */ AsyncCallbackWebHandler* webHandler; /**< Webhandler callback, necessary to remove it later again. */ String uri; /**< URI where the handler is registered. */ bool isUploadError; /**< If upload error happened, it will be true otherwise false. */ String fullPath; /**< Full path of uploaded file. If empty, there is no file available. */ File fd; /**< Upload file descriptor */ - Access access; /**< Access to the topic (r, rw, w) */ /** - * Initialize the web handler data. + * Initialize topic meta data. */ - PluginTopic() : - plugin(nullptr), + TopicMetaData() : + deviceId(), + entityId(), topic(), + getTopicFunc(nullptr), + setTopicFunc(nullptr), + uploadReqFunc(nullptr), webHandler(nullptr), uri(), isUploadError(false), fullPath(), - fd(), - access(ACCESS_READ_WRITE) + fd() { } }; /** - * List of plugin object data. + * List of topic meta data. */ - typedef std::vector PluginTopicList; + typedef std::vector ListOfTopicMetaData; - PluginTopicList m_pluginTopicList; /**< List of plugin topics with webhandler data. */ + ListOfTopicMetaData m_listOfTopicMetaData; /**< List of topic meta data. */ RestApiTopicHandler(const RestApiTopicHandler& adapter); RestApiTopicHandler& operator=(const RestApiTopicHandler& adapter); /** - * Get plugin REST base URI to identify plugin by UID. - * - * @param[in] uid Plugin UID - * - * @return Plugin REST API base URI - */ - String getBaseUriByUid(uint16_t uid); - - /** - * Get plugin REST base URI to identify plugin by alias name. + * Get plugin REST base URI to identify plugin. * - * @param[in] alias Plugin alias name + * @param[in] entityId The entity id which represents the entity of the device. * * @return Plugin REST API base URI */ - String getBaseUriByAlias(const String& alias); - - /** - * Register a single topic of the given plugin. - * - * @param[in] plugin The plugin which provides the topic. - * @param[in] topic The topic name. - * @param[in] access The topic accessibility. - * @param[in] extra Extra parameters, which depend on the topic handler. - * @param[in] baseUri The REST API base URI which to use. - */ - void registerTopic(IPluginMaintenance* plugin, const String& topic, Access access, JsonObjectConst& extra, const String& baseUri); + String getBaseUri(const String& entityId); /** * The web request handler handles all incoming HTTP requests for every plugin topic. * * @param[in] request The web request information from the client. - * @param[in] pluginTopic The related plugin topic data. + * @param[in] topicMetaData The related topic meta data. */ - void webReqHandler(AsyncWebServerRequest *request, PluginTopic* pluginTopic); + void webReqHandler(AsyncWebServerRequest *request, TopicMetaData* topicMetaData); /** * File upload handler. * - * @param[in] request HTTP request. - * @param[in] filename Name of the uploaded file. - * @param[in] index Current file offset. - * @param[in] data Next data part of file, starting at offset. - * @param[in] len Data part size in byte. - * @param[in] final Is final packet or not. - * @param[in] pluginTopic The related plugin topic data. - */ - void uploadHandler(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final, PluginTopic* pluginTopic); + * @param[in] request HTTP request. + * @param[in] filename Name of the uploaded file. + * @param[in] index Current file offset. + * @param[in] data Next data part of file, starting at offset. + * @param[in] len Data part size in byte. + * @param[in] final Is final packet or not. + * @param[in] topicMetaData The related topic meta data. + */ + void uploadHandler(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final, TopicMetaData* topicMetaData); /** * Convert HTTP parameters to JSON format. diff --git a/lib/SensorPlugin/web/SensorPlugin.html b/lib/SensorPlugin/web/SensorPlugin.html index 36767c27..01852bce 100644 --- a/lib/SensorPlugin/web/SensorPlugin.html +++ b/lib/SensorPlugin/web/SensorPlugin.html @@ -39,9 +39,9 @@

                      REST API

                      Get all installed sensor drivers and their provided channels.

                      GET {{ORIGIN}}/rest/api/v1/sensors

                      Get current selected sensor and channel.

                      -
                      GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/channel
                      +
                      GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/channel

                      Set sensor and channel, which values to show.

                      -
                      POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/channel?sensorIndex=<SENSOR-IDX>&channelIndex=<CHANNEL-IDX>
                      +
                      POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/channel?sensorIndex=<SENSOR-IDX>&channelIndex=<CHANNEL-IDX>
                      • SENSOR-IDX: The sensor index.
                      • CHANNEL-IDX: The channel index of the sensor.
                      • @@ -176,7 +176,7 @@

                        Sensor and Channel

                        disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/channel", + url: "/rest/api/v1/display/" + pluginUid + "/channel", isJsonResponse: true }).then(function(rsp) { @@ -215,7 +215,7 @@

                        Sensor and Channel

                        return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/channel", + url: "/rest/api/v1/display/" + pluginUid + "/channel", isJsonResponse: true, parameter: { sensorIndex: $("#sensorList").val(), diff --git a/lib/SignalDetectorPlugin/web/SignalDetectorPlugin.html b/lib/SignalDetectorPlugin/web/SignalDetectorPlugin.html index 6bc981a8..76776147 100644 --- a/lib/SignalDetectorPlugin/web/SignalDetectorPlugin.html +++ b/lib/SignalDetectorPlugin/web/SignalDetectorPlugin.html @@ -41,15 +41,15 @@

                        SignalDetectorPlugin

                        Additional a push notification can be configured. By default a GET is triggered. Using "GET" or "POST" as prefix its configureable. Example: "POST http://..."

                        REST API

                        Get configuration

                        -
                        GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/signalDetector
                        -
                        GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/signalDetector
                        +
                        GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/signalDetector
                        +
                        GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/signalDetector
                        • PLUGIN-UID: The plugin unique id.
                        • PLUGIN-ALIAS: The plugin alias name.

                        Set configuration

                        -
                        POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/signalDetector?text=<TEXT>&pushUrl=<PUSH-URL>&frequency=<FREQUENCY>&minDuration=<MIN-DURATON>&threshold=<THRESHOLD>
                        -
                        POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/signalDetector?text=<TEXT>&pushUrl=<PUSH-URL>&frequency=<FREQUENCY>&minDuration=<MIN-DURATON>&threshold=<THRESHOLD>
                        +
                        POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/signalDetector?text=<TEXT>&pushUrl=<PUSH-URL>&frequency=<FREQUENCY>&minDuration=<MIN-DURATON>&threshold=<THRESHOLD>
                        +
                        POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/signalDetector?text=<TEXT>&pushUrl=<PUSH-URL>&frequency=<FREQUENCY>&minDuration=<MIN-DURATON>&threshold=<THRESHOLD>
                        • PLUGIN-UID: The plugin unique id.
                        • PLUGIN-ALIAS: The plugin alias name.
                        • @@ -190,7 +190,7 @@

                          Configuration

                          disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/signalDetector", + url: "/rest/api/v1/display/" + pluginUid + "/signalDetector", isJsonResponse: true }).then(function(rsp) { var index = 0; @@ -215,7 +215,7 @@

                          Configuration

                          disableUI(); return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/signalDetector", + url: "/rest/api/v1/display/" + pluginUid + "/signalDetector", isJsonResponse: true, parameter: { text: $("#text").val(), diff --git a/lib/SoundReactivePlugin/web/SoundReactivePlugin.html b/lib/SoundReactivePlugin/web/SoundReactivePlugin.html index 3992bbed..3267e48a 100644 --- a/lib/SoundReactivePlugin/web/SoundReactivePlugin.html +++ b/lib/SoundReactivePlugin/web/SoundReactivePlugin.html @@ -38,11 +38,11 @@

                          SoundReactivePlugin

                          Required: A digital microphone (INMP441) is required, connected to the I2S port.

                          REST API

                          Get configuration about how many frequency bands are shown.

                          -
                          GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/config
                          -
                          GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/config
                          +
                          GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/config
                          +
                          GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/config

                          Set configuration about how many frequency bands shall be shown.

                          -
                          POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/config?freqBandLen=<FREQ-BAND-LEN>
                          -
                          POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/config?freqBandLen=<FREQ-BAND-LEN>
                          +
                          POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/config?freqBandLen=<FREQ-BAND-LEN>
                          +
                          POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/config?freqBandLen=<FREQ-BAND-LEN>
                          • PLUGIN-UID: The plugin unique id.
                          • PLUGIN-ALIAS: The plugin alias name.
                          • @@ -147,7 +147,7 @@

                            Number of frequency bands

                            disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/config", + url: "/rest/api/v1/display/" + pluginUid + "/config", isJsonResponse: true }).then(function(rsp) { $("#freqBandLen").val(rsp.data.freqBandLen); @@ -162,7 +162,7 @@

                            Number of frequency bands

                            disableUI(); return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/config", + url: "/rest/api/v1/display/" + pluginUid + "/config", isJsonResponse: true, parameter: { freqBandLen: $("#freqBandLen").val() diff --git a/lib/SunrisePlugin/web/SunrisePlugin.html b/lib/SunrisePlugin/web/SunrisePlugin.html index 813d0199..a7ef65a5 100644 --- a/lib/SunrisePlugin/web/SunrisePlugin.html +++ b/lib/SunrisePlugin/web/SunrisePlugin.html @@ -38,15 +38,15 @@

                            SunrisePlugin

                            Configure the time format in the plugin configuration JSON file. The format itself is according to strftime(). For colorization text properties can be added.

                            REST API

                            Get location

                            -
                            GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/location
                            -
                            GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/location
                            +
                            GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/location
                            +
                            GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/location
                            • PLUGIN-UID: The plugin unique id.
                            • PLUGIN-ALIAS: The plugin alias name.

                            Set location

                            -
                            POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/location?longitude=<LONGITUDE>&latitude=<LATITUDE>
                            -
                            POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/location?longitude=<LONGITUDE>&latitude=<LATITUDE>
                            +
                            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/location?longitude=<LONGITUDE>&latitude=<LATITUDE>
                            +
                            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/location?longitude=<LONGITUDE>&latitude=<LATITUDE>
                            • PLUGIN-UID: The plugin unique id.
                            • PLUGIN-ALIAS: The plugin alias name.
                            • @@ -149,7 +149,7 @@

                              Location

                              disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/location", + url: "/rest/api/v1/display/" + pluginUid + "/location", isJsonResponse: true }).then(function(rsp) { $("#longitude").val(rsp.data.longitude); @@ -165,7 +165,7 @@

                              Location

                              disableUI(); return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/location", + url: "/rest/api/v1/display/" + pluginUid + "/location", isJsonResponse: true, parameter: { latitude: $("#latitude").val(), diff --git a/lib/ThreeIconPlugin/web/ThreeIconPlugin.html b/lib/ThreeIconPlugin/web/ThreeIconPlugin.html index 4c7044ca..2d3acd0e 100644 --- a/lib/ThreeIconPlugin/web/ThreeIconPlugin.html +++ b/lib/ThreeIconPlugin/web/ThreeIconPlugin.html @@ -50,16 +50,16 @@

                              ThreeIconPlugin

                            REST API

                            Set icon

                            -
                            POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/bitmap/<ICON-ID>
                            -
                            POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/bitmap/<ICON-ID>
                            +
                            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/bitmap/<ICON-ID>
                            +
                            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/bitmap/<ICON-ID>
                            • PLUGIN-UID: The plugin unique id.
                            • PLUGIN-ALIAS: The plugin alias name.
                            • ICON-ID: The id of the icon (starting with 0 from left).

                            Clear icon

                            -
                            POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/bitmap/<ICON-ID>?clear=<STATE>
                            -
                            POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/bitmap/<ICON-ID>?clear=<STATE>
                            +
                            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/bitmap/<ICON-ID>?clear=<STATE>
                            +
                            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/bitmap/<ICON-ID>?clear=<STATE>
                            • PLUGIN-UID: The plugin unique id.
                            • PLUGIN-ALIAS: The plugin alias name.
                            • @@ -67,10 +67,10 @@

                              Clear icon

                            • STATE: true or false

                            Control animation (Sprite Sheet required)

                            -
                            POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/animation/<ICON-ID>?forward=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                            -
                            POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/animation/<ICON-ID>?forward=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                            -
                            POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/animation/<ICON-ID>?repeat=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                            -
                            POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/animation/<ICON-ID>?repeat=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                            +
                            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/animation/<ICON-ID>?forward=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                            +
                            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/animation/<ICON-ID>?forward=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                            +
                            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/animation/<ICON-ID>?repeat=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                            +
                            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/animation/<ICON-ID>?repeat=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                            • PLUGIN-UID: The plugin unique id.
                            • PLUGIN-ALIAS: The plugin alias name.
                            • @@ -304,7 +304,7 @@

                              Animation configuration

                              return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/bitmap/" + index, + url: "/rest/api/v1/display/" + pluginUid + "/bitmap/" + index, isJsonResponse: true, parameter: { file: file @@ -327,7 +327,7 @@

                              Animation configuration

                              return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/spritesheet/" + index, + url: "/rest/api/v1/display/" + pluginUid + "/spritesheet/" + index, isJsonResponse: true, parameter: { file: file @@ -368,7 +368,7 @@

                              Animation configuration

                              return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/animation/" + index, + url: "/rest/api/v1/display/" + pluginUid + "/animation/" + index, isJsonResponse: true, parameter: { iconFullPath: "", @@ -406,7 +406,7 @@

                              Animation configuration

                              disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/animation/" + index, + url: "/rest/api/v1/display/" + pluginUid + "/animation/" + index, isJsonResponse: true }).then(function(rsp) { var forwardElement = document.getElementById("forwardAnimation"); @@ -471,7 +471,7 @@

                              Animation configuration

                              return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/animation/" + index, + url: "/rest/api/v1/display/" + pluginUid + "/animation/" + index, isJsonResponse: true, parameter: { forward: isForward, diff --git a/lib/TopicHandlerService/src/TopicHandlerService.cpp b/lib/TopicHandlerService/src/TopicHandlerService.cpp index 3f5db9bd..b7d99df2 100644 --- a/lib/TopicHandlerService/src/TopicHandlerService.cpp +++ b/lib/TopicHandlerService/src/TopicHandlerService.cpp @@ -57,6 +57,9 @@ * Local Variables *****************************************************************************/ +/* Initialize static values. */ +const char* TopicHandlerService::DEFAULT_ACCESS = "rw"; + /****************************************************************************** * Public Methods *****************************************************************************/ @@ -101,9 +104,10 @@ void TopicHandlerService::process() } } -void TopicHandlerService::registerTopics(IPluginMaintenance* plugin) +void TopicHandlerService::registerTopics(const String& deviceId, IPluginMaintenance* plugin) { - if (nullptr != plugin) + if ((false == deviceId.isEmpty()) && + (nullptr != plugin)) { const size_t JSON_DOC_SIZE = 512U; DynamicJsonDocument topicsDoc(JSON_DOC_SIZE); @@ -117,9 +121,13 @@ void TopicHandlerService::registerTopics(IPluginMaintenance* plugin) { for (JsonVariantConst jsonTopic : jsonTopics) { - String topicName; - ITopicHandler::Access topicAccess = DEFAULT_ACCESS; - JsonObjectConst extra; + String topicName; + String entityId; + JsonObjectConst extra; + String topicAccess = DEFAULT_ACCESS; + ITopicHandler::GetTopicFunc getTopicFunc = nullptr; + ITopicHandler::SetTopicFunc setTopicFunc = nullptr; + ITopicHandler::UploadReqFunc uploadReqFunc = nullptr; /* Topic specific parameter available? */ if (true == jsonTopic.is()) @@ -134,7 +142,7 @@ void TopicHandlerService::registerTopics(IPluginMaintenance* plugin) if (true == jsonTopicAccess.is()) { - topicAccess = strToAccess(jsonTopicAccess.as()); + topicAccess = jsonTopicAccess.as(); } extra = jsonTopic; @@ -149,16 +157,33 @@ void TopicHandlerService::registerTopics(IPluginMaintenance* plugin) /* Skip */ ; } - - registerTopic(plugin, topicName, topicAccess, extra); + + if (false == topicName.isEmpty()) + { + strToAccess(plugin, topicAccess, getTopicFunc, setTopicFunc, uploadReqFunc); + + /* Register plugin topic with plugin UID as entity id. */ + entityId = plugin->getUID(); + registerTopic(deviceId, entityId, topicName, extra, getTopicFunc, nullptr, setTopicFunc, uploadReqFunc); + + /* Register plugin topic with plugin alias as entity id (if possible). */ + entityId = plugin->getAlias(); + if (false == entityId.isEmpty()) + { + registerTopic(deviceId, entityId, topicName, extra, getTopicFunc, nullptr, setTopicFunc, uploadReqFunc); + } + + addToPluginMetaDataList(deviceId, plugin, topicName); + } } } } } -void TopicHandlerService::unregisterTopics(IPluginMaintenance* plugin) +void TopicHandlerService::unregisterTopics(const String& deviceId, IPluginMaintenance* plugin) { - if (nullptr != plugin) + if ((false == deviceId.isEmpty()) && + (nullptr != plugin)) { const size_t JSON_DOC_SIZE = 512U; DynamicJsonDocument topicsDoc(JSON_DOC_SIZE); @@ -173,6 +198,7 @@ void TopicHandlerService::unregisterTopics(IPluginMaintenance* plugin) for (JsonVariantConst jsonTopic : jsonTopics) { String topicName; + String entityId; /* Topic specific parameter available? */ if (true == jsonTopic.is()) @@ -194,57 +220,84 @@ void TopicHandlerService::unregisterTopics(IPluginMaintenance* plugin) /* Skip */ ; } - - unregisterTopic(plugin, topicName); + + if (false == topicName.isEmpty()) + { + /* Unregister plugin topic with plugin UID as entity id. */ + entityId = plugin->getUID(); + unregisterTopic(deviceId, entityId, topicName); + + /* Unregister plugin topic with plugin UID as entity id (if possible). */ + entityId = plugin->getAlias(); + if (false == entityId.isEmpty()) + { + unregisterTopic(deviceId, entityId, topicName); + } + + removeFromPluginMetaDataList(deviceId, plugin); + } } } } } -/****************************************************************************** - * Protected Methods - *****************************************************************************/ - -/****************************************************************************** - * Private Methods - *****************************************************************************/ - -void TopicHandlerService::registerTopic(IPluginMaintenance* plugin, const String& topic, ITopicHandler::Access access, JsonObjectConst& extra) +void TopicHandlerService::registerTopic(const String& deviceId, const String& entityId, const String& topic, JsonObjectConst& extra, ITopicHandler::GetTopicFunc getTopicFunc, HasChangedFunc hasChangedFunc, ITopicHandler::SetTopicFunc setTopicFunc, ITopicHandler::UploadReqFunc uploadReqFunc) { - if ((nullptr != plugin) && + if ((false == deviceId.isEmpty()) && + (false == entityId.isEmpty()) && (false == topic.isEmpty())) { - uint8_t idx = 0U; - uint8_t count = 0U; - ITopicHandler** topicHandlerList = TopicHandlers::getList(count); + bool isReadAccess = false; + bool isWriteAccess = false; - /* Register topic by every known topic handler. */ - while(count > idx) + /* Determine the kind of accessability. */ + if (nullptr != getTopicFunc) { - ITopicHandler* handler = topicHandlerList[idx]; - - if (nullptr != handler) - { - handler->registerTopic(plugin, topic, access, extra); - } + isReadAccess = true; + } - ++idx; + if ((nullptr != setTopicFunc) && + (nullptr != uploadReqFunc)) + { + isWriteAccess = true; } - /* Store every readable topic in a list for automatic publishing - * on topic change. - */ - if ((ITopicHandler::ACCESS_READ_ONLY == access) || - (ITopicHandler::ACCESS_READ_WRITE == access)) + if ((true == isReadAccess) || + (true == isWriteAccess)) { - addToList(plugin, topic); + uint8_t idx = 0U; + uint8_t count = 0U; + ITopicHandler** topicHandlerList = TopicHandlers::getList(count); + + /* Register topic by every known topic handler. */ + while(count > idx) + { + ITopicHandler* handler = topicHandlerList[idx]; + + if (nullptr != handler) + { + handler->registerTopic(deviceId, entityId, topic, extra, getTopicFunc, setTopicFunc, uploadReqFunc); + } + + ++idx; + } + + /* Store every readable topic in a list for automatic publishing on topic change, + * except topics from plugins. They will be considered separately. + */ + if ((true == isReadAccess) && + (nullptr != hasChangedFunc)) + { + addToTopicMetaDataList(deviceId, entityId, topic, hasChangedFunc); + } } } } -void TopicHandlerService::unregisterTopic(IPluginMaintenance* plugin, const String& topic) +void TopicHandlerService::unregisterTopic(const String& deviceId, const String& entityId, const String& topic) { - if ((nullptr != plugin) && + if ((false == deviceId.isEmpty()) && + (false == entityId.isEmpty()) && (false == topic.isEmpty())) { uint8_t idx = 0U; @@ -258,98 +311,224 @@ void TopicHandlerService::unregisterTopic(IPluginMaintenance* plugin, const Stri if (nullptr != handler) { - handler->unregisterTopic(plugin, topic); + handler->unregisterTopic(deviceId, entityId, topic); } ++idx; } /* If topic is stored for automatic publishing, it will be removed. */ - removeFromList(plugin, topic); + removeFromTopicMetaDataList(deviceId, entityId, topic); } } -ITopicHandler::Access TopicHandlerService::strToAccess(const String& strAccess) const -{ - ITopicHandler::Access access = ITopicHandler::ACCESS_READ_ONLY; +/****************************************************************************** + * Protected Methods + *****************************************************************************/ - if (true == strAccess.equalsIgnoreCase("rw")) +/****************************************************************************** + * Private Methods + *****************************************************************************/ + +void TopicHandlerService::strToAccess(IPluginMaintenance* plugin, const String& strAccess, ITopicHandler::GetTopicFunc& getTopicFunc, ITopicHandler::SetTopicFunc& setTopicFunc, ITopicHandler::UploadReqFunc& uploadReqFunc) const +{ + if (nullptr != plugin) { - access = ITopicHandler::ACCESS_READ_WRITE; + bool isReadAccess = false; + bool isWriteAccess = false; + + if (true == strAccess.equalsIgnoreCase("rw")) + { + /* Read/Write access */ + isReadAccess = true; + isWriteAccess = true; + } + else if (true == strAccess.equalsIgnoreCase("w")) + { + /* Write only access */ + isWriteAccess = true; + } + else + { + /* Read only access */ + isReadAccess = true; + } + + if (true == isReadAccess) + { + getTopicFunc = [plugin](const String& topic, JsonObject& value) -> bool + { + bool status = false; + + if (nullptr != plugin) + { + status = plugin->getTopic(topic, value); + } + + return status; + }; + } + + if (true == isWriteAccess) + { + setTopicFunc = [plugin](const String& topic, const JsonObject& value) -> bool + { + bool status = false; + + if (nullptr != plugin) + { + status = plugin->setTopic(topic, value); + } + + return status; + }; + + uploadReqFunc = [plugin](const String& topic, const String& srcFilename, String& dstFilename) -> bool + { + bool status = false; + + if (nullptr != plugin) + { + status = plugin->isUploadAccepted(topic, srcFilename, dstFilename); + } + + return status; + }; + } } - else if (true == strAccess.equalsIgnoreCase("w")) +} + +void TopicHandlerService::addToTopicMetaDataList(const String& deviceId, const String& entityId, const String& topic, HasChangedFunc hasChangedFunc) +{ + if ((false == deviceId.isEmpty()) && + (false == entityId.isEmpty()) && + (false == topic.isEmpty()) && + (nullptr != hasChangedFunc)) { - access = ITopicHandler::ACCESS_WRITE_ONLY; + TopicMetaData* topicMetaData = new(std::nothrow) TopicMetaData(); + + if (nullptr != topicMetaData) + { + topicMetaData->deviceId = deviceId; + topicMetaData->entityId = entityId; + topicMetaData->topic = topic; + topicMetaData->hasChangedFunc = hasChangedFunc; + + m_topicMetaDataList.push_back(topicMetaData); + } } - else +} + +void TopicHandlerService::removeFromTopicMetaDataList(const String& deviceId, const String& entityId, const String& topic) +{ + TopicMetaDataList::iterator topicMetaDataListIt = m_topicMetaDataList.begin(); + + while(m_topicMetaDataList.end() != topicMetaDataListIt) { - /* Keep init value. */ - ; - } + TopicMetaData* topicMetaData = *topicMetaDataListIt; - return access; + if ((nullptr != topicMetaData) && + (deviceId == topicMetaData->deviceId) && + (entityId == topicMetaData->entityId) && + (topic == topicMetaData->topic)) + { + topicMetaDataListIt = m_topicMetaDataList.erase(topicMetaDataListIt); + + delete topicMetaData; + topicMetaData = nullptr; + } + else + { + ++topicMetaDataListIt; + } + } } -void TopicHandlerService::addToList(IPluginMaintenance* plugin, const String& topic) +void TopicHandlerService::addToPluginMetaDataList(const String& deviceId, IPluginMaintenance* plugin, const String& topic) { - if ((nullptr != plugin) && + if ((false == deviceId.isEmpty()) && + (nullptr != plugin) && (false == topic.isEmpty())) { - PluginTopic* pluginTopic = new(std::nothrow) PluginTopic(); + PluginMetaData* pluginMetaData = new(std::nothrow) PluginMetaData(); - if (nullptr != pluginTopic) + if (nullptr != pluginMetaData) { - pluginTopic->plugin = plugin; - pluginTopic->topic = topic; + pluginMetaData->deviceId = deviceId; + pluginMetaData->plugin = plugin; + pluginMetaData->topic = topic; - m_pluginTopicList.push_back(pluginTopic); + m_pluginMetaDataList.push_back(pluginMetaData); } } } -void TopicHandlerService::removeFromList(IPluginMaintenance* plugin, const String& topic) +void TopicHandlerService::removeFromPluginMetaDataList(const String& deviceId, IPluginMaintenance* plugin) { - if ((nullptr != plugin) && - (false == topic.isEmpty())) + PluginMetaDataList::iterator pluginMetaDataListIt = m_pluginMetaDataList.begin(); + + while(m_pluginMetaDataList.end() != pluginMetaDataListIt) { - PluginTopicList::iterator pluginTopicListIt = m_pluginTopicList.begin(); + PluginMetaData* pluginMetaData = *pluginMetaDataListIt; - while(m_pluginTopicList.end() != pluginTopicListIt) + if ((nullptr != pluginMetaData) && + (deviceId == pluginMetaData->deviceId) && + (plugin == pluginMetaData->plugin)) { - PluginTopic* pluginTopic = *pluginTopicListIt; - - if ((nullptr != pluginTopic) && - (plugin == pluginTopic->plugin) && - (topic == pluginTopic->topic)) - { - pluginTopicListIt = m_pluginTopicList.erase(pluginTopicListIt); - - delete pluginTopic; - pluginTopic = nullptr; - } - else - { - ++pluginTopicListIt; - } + pluginMetaDataListIt = m_pluginMetaDataList.erase(pluginMetaDataListIt); + + delete pluginMetaData; + pluginMetaData = nullptr; + } + else + { + ++pluginMetaDataListIt; } } } void TopicHandlerService::processOnChange() { - PluginTopicList::iterator pluginTopicListIt = m_pluginTopicList.begin(); + PluginMetaDataList::iterator pluginMetaDataListIt = m_pluginMetaDataList.begin(); + TopicMetaDataList::iterator topicMetaDataListIt = m_topicMetaDataList.begin(); + + /** Process all plugin related topics. */ + while(m_pluginMetaDataList.end() != pluginMetaDataListIt) + { + PluginMetaData* pluginMetaData = *pluginMetaDataListIt; + + if ((nullptr != pluginMetaData) && + (nullptr != pluginMetaData->plugin) && + (true == pluginMetaData->plugin->hasTopicChanged(pluginMetaData->topic))) + { + String entityId; + + entityId = pluginMetaData->plugin->getUID(); + notifyAllHandlers(pluginMetaData->deviceId, entityId, pluginMetaData->topic); - while(m_pluginTopicList.end() != pluginTopicListIt) + entityId = pluginMetaData->plugin->getAlias(); + if (false == entityId.isEmpty()) + { + notifyAllHandlers(pluginMetaData->deviceId, entityId, pluginMetaData->topic); + } + } + + ++pluginMetaDataListIt; + } + + /** Proces all topics which are independent from plugins. */ + while(m_topicMetaDataList.end() != topicMetaDataListIt) { - PluginTopic* pluginTopic = *pluginTopicListIt; + TopicMetaData* topicMetaData = *topicMetaDataListIt; - if ((nullptr != pluginTopic) && - (true == pluginTopic->plugin->hasTopicChanged(pluginTopic->topic))) + if ((nullptr != topicMetaData) && + (nullptr != topicMetaData->hasChangedFunc) && + (true == topicMetaData->hasChangedFunc(topicMetaData->topic))) { - notifyAllHandlers(pluginTopic->plugin, pluginTopic->topic); + notifyAllHandlers(topicMetaData->deviceId, topicMetaData->entityId, topicMetaData->topic); } - ++pluginTopicListIt; + ++topicMetaDataListIt; } } @@ -410,10 +589,9 @@ void TopicHandlerService::processAllHandlers() } } -void TopicHandlerService::notifyAllHandlers(IPluginMaintenance* plugin, const String& topic) +void TopicHandlerService::notifyAllHandlers(const String& deviceId, const String& entityId, const String& topic) { - if ((nullptr != plugin) && - (false == topic.isEmpty())) + if (false == topic.isEmpty()) { uint8_t idx = 0U; uint8_t count = 0U; @@ -425,7 +603,7 @@ void TopicHandlerService::notifyAllHandlers(IPluginMaintenance* plugin, const St if (nullptr != handler) { - handler->notify(plugin, topic); + handler->notify(deviceId, entityId, topic); } ++idx; diff --git a/lib/TopicHandlerService/src/TopicHandlerService.h b/lib/TopicHandlerService/src/TopicHandlerService.h index 2ce7182a..24500271 100644 --- a/lib/TopicHandlerService/src/TopicHandlerService.h +++ b/lib/TopicHandlerService/src/TopicHandlerService.h @@ -65,6 +65,9 @@ class TopicHandlerService : public IService { public: + /** Function prototype to check whether the topic content changed since last time. */ + typedef std::function HasChangedFunc; + /** * Get the topic handler service instance. * @@ -95,37 +98,91 @@ class TopicHandlerService : public IService /** * Register all topics of the given plugin. * + * @param[in] deviceId The device id which represents the physical device. * @param[in] plugin The plugin, which topics shall be registered. */ - void registerTopics(IPluginMaintenance* plugin); + void registerTopics(const String& deviceId, IPluginMaintenance* plugin); /** * Unregister all topics of the given plugin. * + * @param[in] deviceId The device id which represents the physical device. * @param[in] plugin The plugin, which topics to unregister. */ - void unregisterTopics(IPluginMaintenance* plugin); + void unregisterTopics(const String& deviceId, IPluginMaintenance* plugin); + + /** + * Register a topic. + * + * @param[in] deviceId The device id which represents the physical device. + * @param[in] entityId The entity id which represents the entity of the device. + * @param[in] topic The topic which to register. + * @param[in] extra Extra JSON parameters for concrete topic handlers, which are pushed through. + * @param[in] getTopicFunc Function which is called to read the topic. + * @param[in] hasChangedFunc Function which is periodically called to check whether the topic has changed. + * @param[in] setTopicFunc Function which is called to set the topic. + * @param[in] uploadReqFunc Function which is called to accept a file upload or not. + */ + void registerTopic(const String& deviceId, const String& entityId, const String& topic, JsonObjectConst& extra, ITopicHandler::GetTopicFunc getTopicFunc, HasChangedFunc hasChangedFunc, ITopicHandler::SetTopicFunc setTopicFunc, ITopicHandler::UploadReqFunc uploadReqFunc); + + /** + * Unregister a topic. + * + * @param[in] deviceId The device id which represents the physical device. + * @param[in] entityId The entity id which represents the entity of the device. + * @param[in] topic The topic which to unregister. + */ + void unregisterTopic(const String& deviceId, const String& entityId, const String& topic); private: /** Default topic accessibility. */ - static const ITopicHandler::Access DEFAULT_ACCESS = ITopicHandler::ACCESS_READ_WRITE; + static const char* DEFAULT_ACCESS; /** Period in ms to check for changed topics. */ - static const uint32_t ON_CHANGE_PERIOD = 500U; + static const uint32_t ON_CHANGE_PERIOD = 500U; /** - * Plugin topic data, used for automatic publishing. + * Topic meta data, used for automatic publishing. */ - struct PluginTopic + struct TopicMetaData { - IPluginMaintenance* plugin; /**< The plugin */ - String topic; /**< The plugin topic */ + String deviceId; /**< Id of the device this data is related to. */ + String entityId; /**< Id of the entity this data is related to. */ + String topic; /**< The topic this data is related to. */ + HasChangedFunc hasChangedFunc; /**< Function to check whether the topic content has changed. */ /** - * Construct plugin topic data instance. + * Construct topic meta data instance. */ - PluginTopic() : + TopicMetaData() : + deviceId(), + entityId(), + topic(), + hasChangedFunc(nullptr) + { + } + }; + + /** + * List of topic meta data. + */ + typedef std::vector TopicMetaDataList; + + /** + * Plugin meta data, used to determine whether publishing + * of a topic shall take place or not. + */ + struct PluginMetaData + { + String deviceId; /**< Id of the device this data is related to. */ + IPluginMaintenance* plugin; /**< Plugin which topics are handled. */ + String topic; /**< The topic this data is related to. */ + + /** + * Construct topic meta data instance. + */ + PluginMetaData() : plugin(nullptr), topic() { @@ -133,19 +190,21 @@ class TopicHandlerService : public IService }; /** - * List of plugin topics. + * List of plugin meta data. */ - typedef std::vector PluginTopicList; + typedef std::vector PluginMetaDataList; - PluginTopicList m_pluginTopicList; /**< List of readable plugin topics. */ - SimpleTimer m_onChangeTimer; /**< Timer for on change processing period. */ + TopicMetaDataList m_topicMetaDataList; /**< List of readable topics and the required meta data. */ + PluginMetaDataList m_pluginMetaDataList; /**< List of plugins, which topics are handled. */ + SimpleTimer m_onChangeTimer; /**< Timer for on change processing period. */ /** * Constructs the service instance. */ TopicHandlerService() : IService(), - m_pluginTopicList(), + m_topicMetaDataList(), + m_pluginMetaDataList(), m_onChangeTimer() { } @@ -163,47 +222,51 @@ class TopicHandlerService : public IService TopicHandlerService& operator=(const TopicHandlerService& service); /** - * Converts the topic access in string form to the enumeration form. + * Generates the access functions depended on the plugin accessibility. * - * @param[in] strAccess Topic accessibility as string (r, rw, w) - * - * @return Topic accessibility + * @param[in] plugin The plugin which to consider. + * @param[in] strAccess Topic accessibility as string (r, rw, w). + * @param[out] getTopicFunc Function to get the topic content. + * @param[out] setTopicFunc Function to set the topic content. + * @param[out] uploadReqFunc Function used for requesting whether an file upload is allowed. */ - ITopicHandler::Access strToAccess(const String& strAccess) const; + void strToAccess(IPluginMaintenance* plugin, const String& strAccess, ITopicHandler::GetTopicFunc& getTopicFunc, ITopicHandler::SetTopicFunc& setTopicFunc, ITopicHandler::UploadReqFunc& uploadReqFunc) const; /** - * Register a single topic of the given plugin. + * Add topic meta data to list of automatic publishing on change. * - * @param[in] plugin The plugin which provides the topic. - * @param[in] topic The topic name. - * @param[in] access The topic accessibility. - * @param[in] extra Extra parameters, which depend on the topic handler. + * @param[in] deviceId The device id which represents the physical device. + * @param[in] entityId The entity id which represents the entity of the device. + * @param[in] topic The topic name. + * @param[in] hasChangedFunc Function to retrieve whether the topic changes since last time. */ - void registerTopic(IPluginMaintenance* plugin, const String& topic, ITopicHandler::Access access, JsonObjectConst& extra); + void addToTopicMetaDataList(const String& deviceId, const String& entityId, const String& topic, HasChangedFunc hasChangedFunc); /** - * Unregister the topic of the given plugin. + * Remove topic meta data from list of automatic publishing on change. * - * @param[in] plugin The plugin which provides the topic. + * @param[in] deviceId The device id which represents the physical device. + * @param[in] entityId The entity id which represents the entity of the device. * @param[in] topic The topic name. */ - void unregisterTopic(IPluginMaintenance* plugin, const String& topic); + void removeFromTopicMetaDataList(const String& deviceId, const String& entityId, const String& topic); /** - * Add plugin topic to list of automatic publishing on change. + * Add plugin meta data to list of automatic publishing on change. * - * @param[in] plugin The plugin which provides the topic. + * @param[in] deviceId The device id which represents the physical device. + * @param[in] plugin The related plugin. * @param[in] topic The topic name. */ - void addToList(IPluginMaintenance* plugin, const String& topic); + void addToPluginMetaDataList(const String& deviceId, IPluginMaintenance* plugin, const String& topic); /** - * Remove plugin topic from list of automatic publishing on change. + * Remove plugin meta data from list of automatic publishing on change. * - * @param[in] plugin The plugin which provides the topic. - * @param[in] topic The topic name. + * @param[in] deviceId The device id which represents the physical device. + * @param[in] plugin The related plugin. */ - void removeFromList(IPluginMaintenance* plugin, const String& topic); + void removeFromPluginMetaDataList(const String& deviceId, IPluginMaintenance* plugin); /** * Process all plugin topics to check which one has changed. @@ -229,10 +292,11 @@ class TopicHandlerService : public IService /** * Notify all topic handlers about changed topic. * - * @param[in] plugin The plugin which provides the topic. + * @param[in] deviceId The device id which represents the physical device. + * @param[in] entityId The entity id which represents the entity of the device. * @param[in] topic The topic name. */ - void notifyAllHandlers(IPluginMaintenance* plugin, const String& topic); + void notifyAllHandlers(const String& deviceId, const String& entityId, const String& topic); }; /****************************************************************************** diff --git a/lib/VolumioPlugin/web/VolumioPlugin.html b/lib/VolumioPlugin/web/VolumioPlugin.html index d8c6c73d..a57a3f4d 100644 --- a/lib/VolumioPlugin/web/VolumioPlugin.html +++ b/lib/VolumioPlugin/web/VolumioPlugin.html @@ -38,11 +38,11 @@

                              VolumioPlugin

                              If the VOLUMIO server is offline, the plugin gets automatically disabled, otherwise enabled.

                              REST API

                              Get VOLUMIO host address

                              -
                              GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/host
                              -
                              GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/host
                              +
                              GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/host
                              +
                              GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/host

                              Set VOLUMIO host address

                              -
                              POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/host?host=<HOST-ADDRESS>
                              -
                              POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/host?host=<HOST-ADDRESS>
                              +
                              POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/host?host=<HOST-ADDRESS>
                              +
                              POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/host?host=<HOST-ADDRESS>
                              • PLUGIN-UID: The plugin unique id.
                              • PLUGIN-ALIAS: The plugin alias name.
                              • @@ -140,7 +140,7 @@

                                Host Address

                                disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/uid/" + pluginUid + "/host", + url: "/rest/api/v1/display/" + pluginUid + "/host", isJsonResponse: true }).then(function(rsp) { $("#host").val(rsp.data.host); @@ -155,7 +155,7 @@

                                Host Address

                                disableUI(); return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/uid/" + pluginUid + "/host", + url: "/rest/api/v1/display/" + pluginUid + "/host", isJsonResponse: true, parameter: { host: $("#host").val() diff --git a/src/Plugin/PluginMgr.cpp b/src/Plugin/PluginMgr.cpp index 12366cd9..5ffa694d 100644 --- a/src/Plugin/PluginMgr.cpp +++ b/src/Plugin/PluginMgr.cpp @@ -74,6 +74,19 @@ const char* PluginMgr::MQTT_SPECIAL_CHARACTERS = "+#*>$"; /* See MQTT specifica void PluginMgr::begin() { + SettingsService& settings = SettingsService::getInstance(); + + if (false == settings.open(true)) + { + m_deviceId = settings.getHostname().getDefault(); + } + else + { + m_deviceId = settings.getHostname().getValue(); + + settings.close(); + } + createPluginConfigDirectory(); } @@ -108,7 +121,7 @@ bool PluginMgr::uninstall(IPluginMaintenance* plugin) if (true == status) { - TopicHandlerService::getInstance().unregisterTopics(plugin); + TopicHandlerService::getInstance().unregisterTopics(m_deviceId, plugin); m_pluginFactory.destroyPlugin(plugin); } @@ -136,13 +149,13 @@ bool PluginMgr::setPluginAliasName(IPluginMaintenance* plugin, const String& ali (true == isPluginAliasValid(alias))) { /* First remove current registered topics. */ - TopicHandlerService::getInstance().unregisterTopics(plugin); + TopicHandlerService::getInstance().unregisterTopics(m_deviceId, plugin); /* Set new alias */ plugin->setAlias(alias); /* Register web API, based on new alias. */ - TopicHandlerService::getInstance().registerTopics(plugin); + TopicHandlerService::getInstance().registerTopics(m_deviceId, plugin); isSuccessful = true; } @@ -378,7 +391,7 @@ bool PluginMgr::install(IPluginMaintenance* plugin, uint8_t slotId) if (true == isSuccessful) { - TopicHandlerService::getInstance().registerTopics(plugin); + TopicHandlerService::getInstance().registerTopics(m_deviceId, plugin); } } diff --git a/src/Plugin/PluginMgr.h b/src/Plugin/PluginMgr.h index a90c2354..603f397f 100644 --- a/src/Plugin/PluginMgr.h +++ b/src/Plugin/PluginMgr.h @@ -162,12 +162,14 @@ class PluginMgr static const char* MQTT_SPECIAL_CHARACTERS; PluginFactory m_pluginFactory; /**< The plugin factory with the plugin type registry. */ + String m_deviceId; /**< Device id, used for topic registration. */ /** * Constructs the plugin manager. */ PluginMgr() : - m_pluginFactory() + m_pluginFactory(), + m_deviceId() { } From 8c4910aafdac9a7de86d1cd7ac4f1bdee6f23078 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Wed, 25 Oct 2023 22:54:25 +0200 Subject: [PATCH 034/105] Use JsonObjectConst instead of JsonObject if possible. --- lib/CountdownPlugin/src/CountdownPlugin.cpp | 2 +- lib/CountdownPlugin/src/CountdownPlugin.h | 2 +- lib/DateTimePlugin/src/DateTimePlugin.cpp | 2 +- lib/DateTimePlugin/src/DateTimePlugin.h | 2 +- lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp | 2 +- lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.h | 2 +- lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp | 2 +- lib/GrabViaRestPlugin/src/GrabViaRestPlugin.h | 2 +- lib/GruenbeckPlugin/src/GruenbeckPlugin.cpp | 2 +- lib/GruenbeckPlugin/src/GruenbeckPlugin.h | 2 +- lib/ITopicHandler/src/ITopicHandler.h | 2 +- lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp | 2 +- lib/IconTextLampPlugin/src/IconTextLampPlugin.h | 2 +- lib/IconTextPlugin/src/IconTextPlugin.cpp | 2 +- lib/IconTextPlugin/src/IconTextPlugin.h | 2 +- lib/JustTextPlugin/src/JustTextPlugin.cpp | 2 +- lib/JustTextPlugin/src/JustTextPlugin.h | 2 +- lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp | 8 +++++--- lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp | 2 +- lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h | 2 +- lib/Plugin/src/IPluginMaintenance.hpp | 2 +- lib/Plugin/src/Plugin.hpp | 2 +- lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp | 4 ++-- lib/SensorPlugin/src/SensorPlugin.cpp | 2 +- lib/SensorPlugin/src/SensorPlugin.h | 2 +- lib/SignalDetectorPlugin/src/SignalDetectorPlugin.cpp | 2 +- lib/SignalDetectorPlugin/src/SignalDetectorPlugin.h | 2 +- lib/SoundReactivePlugin/src/SoundReactivePlugin.cpp | 2 +- lib/SoundReactivePlugin/src/SoundReactivePlugin.h | 2 +- lib/SunrisePlugin/src/SunrisePlugin.cpp | 2 +- lib/SunrisePlugin/src/SunrisePlugin.h | 2 +- lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp | 2 +- lib/ThreeIconPlugin/src/ThreeIconPlugin.h | 2 +- lib/TopicHandlerService/src/TopicHandlerService.cpp | 2 +- lib/VolumioPlugin/src/VolumioPlugin.cpp | 2 +- lib/VolumioPlugin/src/VolumioPlugin.h | 2 +- 36 files changed, 41 insertions(+), 39 deletions(-) diff --git a/lib/CountdownPlugin/src/CountdownPlugin.cpp b/lib/CountdownPlugin/src/CountdownPlugin.cpp index a93db2f2..b67b07f9 100644 --- a/lib/CountdownPlugin/src/CountdownPlugin.cpp +++ b/lib/CountdownPlugin/src/CountdownPlugin.cpp @@ -87,7 +87,7 @@ bool CountdownPlugin::getTopic(const String& topic, JsonObject& value) const return isSuccessful; } -bool CountdownPlugin::setTopic(const String& topic, const JsonObject& value) +bool CountdownPlugin::setTopic(const String& topic, const JsonObjectConst& value) { bool isSuccessful = false; diff --git a/lib/CountdownPlugin/src/CountdownPlugin.h b/lib/CountdownPlugin/src/CountdownPlugin.h index 5c2508cb..acb15062 100644 --- a/lib/CountdownPlugin/src/CountdownPlugin.h +++ b/lib/CountdownPlugin/src/CountdownPlugin.h @@ -300,7 +300,7 @@ class CountdownPlugin : public Plugin, private PluginConfigFsHandler * * @return If successful it will return true otherwise false. */ - bool setTopic(const String& topic, const JsonObject& value) final; + bool setTopic(const String& topic, const JsonObjectConst& value) final; /** * Is the topic content changed since last time? diff --git a/lib/DateTimePlugin/src/DateTimePlugin.cpp b/lib/DateTimePlugin/src/DateTimePlugin.cpp index a7ce6e01..35c75800 100644 --- a/lib/DateTimePlugin/src/DateTimePlugin.cpp +++ b/lib/DateTimePlugin/src/DateTimePlugin.cpp @@ -95,7 +95,7 @@ bool DateTimePlugin::getTopic(const String& topic, JsonObject& value) const return isSuccessful; } -bool DateTimePlugin::setTopic(const String& topic, const JsonObject& value) +bool DateTimePlugin::setTopic(const String& topic, const JsonObjectConst& value) { bool isSuccessful = false; diff --git a/lib/DateTimePlugin/src/DateTimePlugin.h b/lib/DateTimePlugin/src/DateTimePlugin.h index 4fda4566..483a6d8f 100644 --- a/lib/DateTimePlugin/src/DateTimePlugin.h +++ b/lib/DateTimePlugin/src/DateTimePlugin.h @@ -174,7 +174,7 @@ class DateTimePlugin : public Plugin, private PluginConfigFsHandler * * @return If successful it will return true otherwise false. */ - bool setTopic(const String& topic, const JsonObject& value) final; + bool setTopic(const String& topic, const JsonObjectConst& value) final; /** * Is the topic content changed since last time? diff --git a/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp b/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp index 96de92f1..e9caae4d 100644 --- a/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp +++ b/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp @@ -83,7 +83,7 @@ bool GrabViaMqttPlugin::getTopic(const String& topic, JsonObject& value) const return isSuccessful; } -bool GrabViaMqttPlugin::setTopic(const String& topic, const JsonObject& value) +bool GrabViaMqttPlugin::setTopic(const String& topic, const JsonObjectConst& value) { bool isSuccessful = false; diff --git a/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.h b/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.h index d4596eef..c6e920ca 100644 --- a/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.h +++ b/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.h @@ -194,7 +194,7 @@ class GrabViaMqttPlugin : public Plugin, private PluginConfigFsHandler * * @return If successful it will return true otherwise false. */ - bool setTopic(const String& topic, const JsonObject& value) final; + bool setTopic(const String& topic, const JsonObjectConst& value) final; /** * Is the topic content changed since last time? diff --git a/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp b/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp index 6091b13d..f04e7c3d 100644 --- a/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp +++ b/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp @@ -82,7 +82,7 @@ bool GrabViaRestPlugin::getTopic(const String& topic, JsonObject& value) const return isSuccessful; } -bool GrabViaRestPlugin::setTopic(const String& topic, const JsonObject& value) +bool GrabViaRestPlugin::setTopic(const String& topic, const JsonObjectConst& value) { bool isSuccessful = false; diff --git a/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.h b/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.h index 898d3b38..c7eb28fb 100644 --- a/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.h +++ b/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.h @@ -212,7 +212,7 @@ class GrabViaRestPlugin : public Plugin, private PluginConfigFsHandler * * @return If successful it will return true otherwise false. */ - bool setTopic(const String& topic, const JsonObject& value) final; + bool setTopic(const String& topic, const JsonObjectConst& value) final; /** * Is the topic content changed since last time? diff --git a/lib/GruenbeckPlugin/src/GruenbeckPlugin.cpp b/lib/GruenbeckPlugin/src/GruenbeckPlugin.cpp index 4126b696..ed23b8d2 100644 --- a/lib/GruenbeckPlugin/src/GruenbeckPlugin.cpp +++ b/lib/GruenbeckPlugin/src/GruenbeckPlugin.cpp @@ -86,7 +86,7 @@ bool GruenbeckPlugin::getTopic(const String& topic, JsonObject& value) const return isSuccessful; } -bool GruenbeckPlugin::setTopic(const String& topic, const JsonObject& value) +bool GruenbeckPlugin::setTopic(const String& topic, const JsonObjectConst& value) { bool isSuccessful = false; diff --git a/lib/GruenbeckPlugin/src/GruenbeckPlugin.h b/lib/GruenbeckPlugin/src/GruenbeckPlugin.h index dd30a656..59141da9 100644 --- a/lib/GruenbeckPlugin/src/GruenbeckPlugin.h +++ b/lib/GruenbeckPlugin/src/GruenbeckPlugin.h @@ -208,7 +208,7 @@ class GruenbeckPlugin : public Plugin, private PluginConfigFsHandler * * @return If successful it will return true otherwise false. */ - bool setTopic(const String& topic, const JsonObject& value) final; + bool setTopic(const String& topic, const JsonObjectConst& value) final; /** * Is the topic content changed since last time? diff --git a/lib/ITopicHandler/src/ITopicHandler.h b/lib/ITopicHandler/src/ITopicHandler.h index 91896513..26f18282 100644 --- a/lib/ITopicHandler/src/ITopicHandler.h +++ b/lib/ITopicHandler/src/ITopicHandler.h @@ -67,7 +67,7 @@ class ITopicHandler typedef std::function GetTopicFunc; /** Function prototype to set topic content. */ - typedef std::function SetTopicFunc; + typedef std::function SetTopicFunc; /** Function prototype for file upload request. */ typedef std::function UploadReqFunc; diff --git a/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp b/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp index 5aa38426..17a34c74 100644 --- a/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp +++ b/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp @@ -177,7 +177,7 @@ bool IconTextLampPlugin::getTopic(const String& topic, JsonObject& value) const return isSuccessful; } -bool IconTextLampPlugin::setTopic(const String& topic, const JsonObject& value) +bool IconTextLampPlugin::setTopic(const String& topic, const JsonObjectConst& value) { bool isSuccessful = false; diff --git a/lib/IconTextLampPlugin/src/IconTextLampPlugin.h b/lib/IconTextLampPlugin/src/IconTextLampPlugin.h index 21a40c9e..e11037e2 100644 --- a/lib/IconTextLampPlugin/src/IconTextLampPlugin.h +++ b/lib/IconTextLampPlugin/src/IconTextLampPlugin.h @@ -165,7 +165,7 @@ class IconTextLampPlugin : public Plugin * * @return If successful it will return true otherwise false. */ - bool setTopic(const String& topic, const JsonObject& value) final; + bool setTopic(const String& topic, const JsonObjectConst& value) final; /** * Is the topic content changed since last time? diff --git a/lib/IconTextPlugin/src/IconTextPlugin.cpp b/lib/IconTextPlugin/src/IconTextPlugin.cpp index 03fefd67..a2fa62dc 100644 --- a/lib/IconTextPlugin/src/IconTextPlugin.cpp +++ b/lib/IconTextPlugin/src/IconTextPlugin.cpp @@ -135,7 +135,7 @@ bool IconTextPlugin::getTopic(const String& topic, JsonObject& value) const return isSuccessful; } -bool IconTextPlugin::setTopic(const String& topic, const JsonObject& value) +bool IconTextPlugin::setTopic(const String& topic, const JsonObjectConst& value) { bool isSuccessful = false; diff --git a/lib/IconTextPlugin/src/IconTextPlugin.h b/lib/IconTextPlugin/src/IconTextPlugin.h index 69a7d649..e4390ced 100644 --- a/lib/IconTextPlugin/src/IconTextPlugin.h +++ b/lib/IconTextPlugin/src/IconTextPlugin.h @@ -193,7 +193,7 @@ class IconTextPlugin : public Plugin * * @return If successful it will return true otherwise false. */ - bool setTopic(const String& topic, const JsonObject& value) final; + bool setTopic(const String& topic, const JsonObjectConst& value) final; /** * Is the topic content changed since last time? diff --git a/lib/JustTextPlugin/src/JustTextPlugin.cpp b/lib/JustTextPlugin/src/JustTextPlugin.cpp index 55b836e4..630ee2f6 100644 --- a/lib/JustTextPlugin/src/JustTextPlugin.cpp +++ b/lib/JustTextPlugin/src/JustTextPlugin.cpp @@ -108,7 +108,7 @@ bool JustTextPlugin::getTopic(const String& topic, JsonObject& value) const return isSuccessful; } -bool JustTextPlugin::setTopic(const String& topic, const JsonObject& value) +bool JustTextPlugin::setTopic(const String& topic, const JsonObjectConst& value) { bool isSuccessful = false; diff --git a/lib/JustTextPlugin/src/JustTextPlugin.h b/lib/JustTextPlugin/src/JustTextPlugin.h index ae5beef7..a48b3cb1 100644 --- a/lib/JustTextPlugin/src/JustTextPlugin.h +++ b/lib/JustTextPlugin/src/JustTextPlugin.h @@ -184,7 +184,7 @@ class JustTextPlugin : public Plugin * * @return If successful it will return true otherwise false. */ - bool setTopic(const String& topic, const JsonObject& value) final; + bool setTopic(const String& topic, const JsonObjectConst& value) final; /** * Is the topic content changed since last time? diff --git a/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp b/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp index 2e586c33..dc2e7714 100644 --- a/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp +++ b/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp @@ -315,8 +315,9 @@ void MqttApiTopicHandler::write(const String& deviceId, const String& entityId, } else { - JsonVariantConst jsonFileName = jsonDoc["fileName"]; - JsonVariantConst jsonFileBase64 = jsonDoc["file"]; + JsonVariantConst jsonFileName = jsonDoc["fileName"]; + JsonVariantConst jsonFileBase64 = jsonDoc["file"]; + JsonObjectConst jsonValue; /* File transfer? */ if ((true == jsonFileName.is()) && @@ -389,7 +390,8 @@ void MqttApiTopicHandler::write(const String& deviceId, const String& entityId, } } - if (false == setTopicFunc(topic, jsonDoc.as())) + jsonValue = jsonDoc.as(); + if (false == setTopicFunc(topic, jsonValue)) { LOG_WARNING("[%s] Payload rejected.", entityId.c_str()); } diff --git a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp index 138cec57..dc549a36 100644 --- a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp +++ b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp @@ -134,7 +134,7 @@ bool OpenWeatherPlugin::getTopic(const String& topic, JsonObject& value) const return isSuccessful; } -bool OpenWeatherPlugin::setTopic(const String& topic, const JsonObject& value) +bool OpenWeatherPlugin::setTopic(const String& topic, const JsonObjectConst& value) { bool isSuccessful = false; diff --git a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h index b7817407..2b11cbcd 100644 --- a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h +++ b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h @@ -230,7 +230,7 @@ class OpenWeatherPlugin : public Plugin, private PluginConfigFsHandler * * @return If successful it will return true otherwise false. */ - bool setTopic(const String& topic, const JsonObject& value) final; + bool setTopic(const String& topic, const JsonObjectConst& value) final; /** * Is the topic content changed since last time? diff --git a/lib/Plugin/src/IPluginMaintenance.hpp b/lib/Plugin/src/IPluginMaintenance.hpp index 42524a3b..de065fcb 100644 --- a/lib/Plugin/src/IPluginMaintenance.hpp +++ b/lib/Plugin/src/IPluginMaintenance.hpp @@ -173,7 +173,7 @@ class IPluginMaintenance * * @return If successful it will return true otherwise false. */ - virtual bool setTopic(const String& topic, const JsonObject& value) = 0; + virtual bool setTopic(const String& topic, const JsonObjectConst& value) = 0; /** * Has the topic content changed since last time? diff --git a/lib/Plugin/src/Plugin.hpp b/lib/Plugin/src/Plugin.hpp index f9e89a77..15696c4d 100644 --- a/lib/Plugin/src/Plugin.hpp +++ b/lib/Plugin/src/Plugin.hpp @@ -199,7 +199,7 @@ class Plugin : public IPluginMaintenance * * @return If successful it will return true otherwise false. */ - bool setTopic(const String& topic, const JsonObject& value) override + bool setTopic(const String& topic, const JsonObjectConst& value) override { PLUGIN_NOT_USED(topic); PLUGIN_NOT_USED(value); diff --git a/lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp b/lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp index be4f33fc..4b212953 100644 --- a/lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp +++ b/lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp @@ -200,7 +200,7 @@ void RestApiTopicHandler::webReqHandler(AsyncWebServerRequest *request, TopicMet (nullptr != topicMetaData->setTopicFunc)) { DynamicJsonDocument jsonDocPar(JSON_DOC_SIZE); - JsonObject jsonValue; + JsonObjectConst jsonValue; /* Topic data is in the HTTP parameters and needs to be converted to JSON. */ par2Json(jsonDocPar, request); @@ -212,7 +212,7 @@ void RestApiTopicHandler::webReqHandler(AsyncWebServerRequest *request, TopicMet jsonDocPar["fullPath"] = topicMetaData->fullPath; } - jsonValue = jsonDocPar.as(); /* Assign after par2Json conversion! Otherwise there will be a empty object. */ + jsonValue = jsonDocPar.as(); /* Assign after par2Json conversion! Otherwise there will be a empty object. */ if (false == topicMetaData->setTopicFunc(topicMetaData->topic, jsonValue)) { LOG_WARNING("[%s] Topic \"%s\" not supported or invalid data.", topicMetaData->entityId, topicMetaData->topic); diff --git a/lib/SensorPlugin/src/SensorPlugin.cpp b/lib/SensorPlugin/src/SensorPlugin.cpp index 0b521fb0..057b1158 100644 --- a/lib/SensorPlugin/src/SensorPlugin.cpp +++ b/lib/SensorPlugin/src/SensorPlugin.cpp @@ -84,7 +84,7 @@ bool SensorPlugin::getTopic(const String& topic, JsonObject& value) const return isSuccessful; } -bool SensorPlugin::setTopic(const String& topic, const JsonObject& value) +bool SensorPlugin::setTopic(const String& topic, const JsonObjectConst& value) { bool isSuccessful = false; diff --git a/lib/SensorPlugin/src/SensorPlugin.h b/lib/SensorPlugin/src/SensorPlugin.h index 569f4b1e..4db3b65e 100644 --- a/lib/SensorPlugin/src/SensorPlugin.h +++ b/lib/SensorPlugin/src/SensorPlugin.h @@ -186,7 +186,7 @@ class SensorPlugin : public Plugin, private PluginConfigFsHandler * * @return If successful it will return true otherwise false. */ - bool setTopic(const String& topic, const JsonObject& value) final; + bool setTopic(const String& topic, const JsonObjectConst& value) final; /** * Is the topic content changed since last time? diff --git a/lib/SignalDetectorPlugin/src/SignalDetectorPlugin.cpp b/lib/SignalDetectorPlugin/src/SignalDetectorPlugin.cpp index b6498d8a..867a577c 100644 --- a/lib/SignalDetectorPlugin/src/SignalDetectorPlugin.cpp +++ b/lib/SignalDetectorPlugin/src/SignalDetectorPlugin.cpp @@ -103,7 +103,7 @@ bool SignalDetectorPlugin::getTopic(const String& topic, JsonObject& value) cons return isSuccessful; } -bool SignalDetectorPlugin::setTopic(const String& topic, const JsonObject& value) +bool SignalDetectorPlugin::setTopic(const String& topic, const JsonObjectConst& value) { bool isSuccessful = false; diff --git a/lib/SignalDetectorPlugin/src/SignalDetectorPlugin.h b/lib/SignalDetectorPlugin/src/SignalDetectorPlugin.h index 3d4dadab..d4582625 100644 --- a/lib/SignalDetectorPlugin/src/SignalDetectorPlugin.h +++ b/lib/SignalDetectorPlugin/src/SignalDetectorPlugin.h @@ -198,7 +198,7 @@ class SignalDetectorPlugin : public Plugin, private PluginConfigFsHandler * * @return If successful it will return true otherwise false. */ - bool setTopic(const String& topic, const JsonObject& value) final; + bool setTopic(const String& topic, const JsonObjectConst& value) final; /** * Is the topic content changed since last time? diff --git a/lib/SoundReactivePlugin/src/SoundReactivePlugin.cpp b/lib/SoundReactivePlugin/src/SoundReactivePlugin.cpp index 02d89344..d1305291 100644 --- a/lib/SoundReactivePlugin/src/SoundReactivePlugin.cpp +++ b/lib/SoundReactivePlugin/src/SoundReactivePlugin.cpp @@ -103,7 +103,7 @@ bool SoundReactivePlugin::getTopic(const String& topic, JsonObject& value) const return isSuccessful; } -bool SoundReactivePlugin::setTopic(const String& topic, const JsonObject& value) +bool SoundReactivePlugin::setTopic(const String& topic, const JsonObjectConst& value) { bool isSuccessful = false; diff --git a/lib/SoundReactivePlugin/src/SoundReactivePlugin.h b/lib/SoundReactivePlugin/src/SoundReactivePlugin.h index 54be843e..48732c99 100644 --- a/lib/SoundReactivePlugin/src/SoundReactivePlugin.h +++ b/lib/SoundReactivePlugin/src/SoundReactivePlugin.h @@ -176,7 +176,7 @@ class SoundReactivePlugin : public Plugin, private PluginConfigFsHandler * * @return If successful it will return true otherwise false. */ - bool setTopic(const String& topic, const JsonObject& value) final; + bool setTopic(const String& topic, const JsonObjectConst& value) final; /** * Is the topic content changed since last time? diff --git a/lib/SunrisePlugin/src/SunrisePlugin.cpp b/lib/SunrisePlugin/src/SunrisePlugin.cpp index 794e09d9..53fe2437 100644 --- a/lib/SunrisePlugin/src/SunrisePlugin.cpp +++ b/lib/SunrisePlugin/src/SunrisePlugin.cpp @@ -91,7 +91,7 @@ bool SunrisePlugin::getTopic(const String& topic, JsonObject& value) const return isSuccessful; } -bool SunrisePlugin::setTopic(const String& topic, const JsonObject& value) +bool SunrisePlugin::setTopic(const String& topic, const JsonObjectConst& value) { bool isSuccessful = false; diff --git a/lib/SunrisePlugin/src/SunrisePlugin.h b/lib/SunrisePlugin/src/SunrisePlugin.h index 6035abbe..46fa1bb9 100644 --- a/lib/SunrisePlugin/src/SunrisePlugin.h +++ b/lib/SunrisePlugin/src/SunrisePlugin.h @@ -213,7 +213,7 @@ class SunrisePlugin : public Plugin, private PluginConfigFsHandler * * @return If successful it will return true otherwise false. */ - bool setTopic(const String& topic, const JsonObject& value) final; + bool setTopic(const String& topic, const JsonObjectConst& value) final; /** * Is the topic content changed since last time? diff --git a/lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp b/lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp index dcc33e32..050b5a2d 100644 --- a/lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp +++ b/lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp @@ -131,7 +131,7 @@ bool ThreeIconPlugin::getTopic(const String& topic, JsonObject& value) const return isSuccessful; } -bool ThreeIconPlugin::setTopic(const String& topic, const JsonObject& value) +bool ThreeIconPlugin::setTopic(const String& topic, const JsonObjectConst& value) { bool isSuccessful = false; diff --git a/lib/ThreeIconPlugin/src/ThreeIconPlugin.h b/lib/ThreeIconPlugin/src/ThreeIconPlugin.h index 44f471ed..bd07b91a 100644 --- a/lib/ThreeIconPlugin/src/ThreeIconPlugin.h +++ b/lib/ThreeIconPlugin/src/ThreeIconPlugin.h @@ -155,7 +155,7 @@ class ThreeIconPlugin : public Plugin * * @return If successful it will return true otherwise false. */ - bool setTopic(const String& topic, const JsonObject& value) final; + bool setTopic(const String& topic, const JsonObjectConst& value) final; /** * Is the topic content changed since last time? diff --git a/lib/TopicHandlerService/src/TopicHandlerService.cpp b/lib/TopicHandlerService/src/TopicHandlerService.cpp index b7d99df2..ee050b03 100644 --- a/lib/TopicHandlerService/src/TopicHandlerService.cpp +++ b/lib/TopicHandlerService/src/TopicHandlerService.cpp @@ -371,7 +371,7 @@ void TopicHandlerService::strToAccess(IPluginMaintenance* plugin, const String& if (true == isWriteAccess) { - setTopicFunc = [plugin](const String& topic, const JsonObject& value) -> bool + setTopicFunc = [plugin](const String& topic, const JsonObjectConst& value) -> bool { bool status = false; diff --git a/lib/VolumioPlugin/src/VolumioPlugin.cpp b/lib/VolumioPlugin/src/VolumioPlugin.cpp index 97fde88c..02ffe0d2 100644 --- a/lib/VolumioPlugin/src/VolumioPlugin.cpp +++ b/lib/VolumioPlugin/src/VolumioPlugin.cpp @@ -94,7 +94,7 @@ bool VolumioPlugin::getTopic(const String& topic, JsonObject& value) const return isSuccessful; } -bool VolumioPlugin::setTopic(const String& topic, const JsonObject& value) +bool VolumioPlugin::setTopic(const String& topic, const JsonObjectConst& value) { bool isSuccessful = false; diff --git a/lib/VolumioPlugin/src/VolumioPlugin.h b/lib/VolumioPlugin/src/VolumioPlugin.h index 84366bfc..9c2ed420 100644 --- a/lib/VolumioPlugin/src/VolumioPlugin.h +++ b/lib/VolumioPlugin/src/VolumioPlugin.h @@ -189,7 +189,7 @@ class VolumioPlugin : public Plugin, private PluginConfigFsHandler * * @return If successful it will return true otherwise false. */ - bool setTopic(const String& topic, const JsonObject& value) final; + bool setTopic(const String& topic, const JsonObjectConst& value) final; /** * Is the topic content changed since last time? From 0962989f47822d2c973478bf78852ff4c75a4334 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 29 Oct 2023 15:11:01 +0100 Subject: [PATCH 035/105] TOCs updated. --- doc/CUSTOM-IDF-LIBRARIES.md | 12 +++---- doc/HOMEASSISTANT.md | 24 ++++++------- doc/ICONS.md | 18 +++++----- doc/PLUGIN-DEV.md | 30 ++++++++-------- doc/REQUIREMENTS.md | 26 +++++++------- doc/SPRITESHEET.md | 12 +++---- doc/WEBSOCKET.md | 46 ++++++++++++------------ doc/architecture/README.md | 52 ++++++++++++++-------------- doc/config/README.md | 22 ++++++------ doc/config/SW-BUILD.md | 12 +++---- doc/config/SW-UPDATE.md | 34 +++++++++--------- doc/config/TOOLCHAIN-INSTALLATION.md | 24 ++++++------- 12 files changed, 156 insertions(+), 156 deletions(-) diff --git a/doc/CUSTOM-IDF-LIBRARIES.md b/doc/CUSTOM-IDF-LIBRARIES.md index b095eb3d..945daaf8 100644 --- a/doc/CUSTOM-IDF-LIBRARIES.md +++ b/doc/CUSTOM-IDF-LIBRARIES.md @@ -5,12 +5,12 @@ # Building ESP-IDF Libraries for Arduino using custom sdkconfig options -- [Building ESP-IDF Libraries](#Building-ESP-ID-Libraries) -- [Purpose](#purpose) -- [Instructions](#instructions) -- [Issues, Ideas And Bugs](#issues-ideas-and-bugs) -- [License](#license) -- [Contribution](#contribution) +* [Building ESP-IDF Libraries for Arduino using custom sdkconfig options](#building-esp-idf-libraries-for-arduino-using-custom-sdkconfig-options) +* [Purpose](#purpose) +* [Instructions](#instructions) +* [Issues, Ideas And Bugs](#issues-ideas-and-bugs) +* [License](#license) +* [Contribution](#contribution) # Purpose diff --git a/doc/HOMEASSISTANT.md b/doc/HOMEASSISTANT.md index fe30d768..754ffaf9 100644 --- a/doc/HOMEASSISTANT.md +++ b/doc/HOMEASSISTANT.md @@ -4,18 +4,18 @@ # Home Assistant -- [Purpose](#purpose) -- [REST API](#rest-api) - - [Installation](#installation) - - [Setting up REST command](#setting-up-rest-command) - - [Import Blueprint](#import-blueprint) - - [Generate an automation](#generate-an-automation) -- [MQTT](#mqtt) - - [Installation](#installation-1) - - [MQTT Discovery](#mqtt-discovery) -- [Issues, Ideas And Bugs](#issues-ideas-and-bugs) -- [License](#license) -- [Contribution](#contribution) +* [Purpose](#purpose) +* [REST API](#rest-api) + * [Installation](#installation) + * [Setting up REST command](#setting-up-rest-command) + * [Import Blueprint](#import-blueprint) + * [Generate an automation](#generate-an-automation) +* [MQTT](#mqtt) + * [Installation](#installation-1) + * [MQTT Discovery](#mqtt-discovery) +* [Issues, Ideas And Bugs](#issues-ideas-and-bugs) +* [License](#license) +* [Contribution](#contribution) # Purpose To display e.g. sensor data from your [Home Assistant](https://www.home-assistant.io/) instance, there are two possibilitis. Using the REST API or MQTT. diff --git a/doc/ICONS.md b/doc/ICONS.md index ed71d38c..81eb407c 100644 --- a/doc/ICONS.md +++ b/doc/ICONS.md @@ -5,15 +5,15 @@ # Alternative Icons -- [Alternative Icons](#alternative-icons) -- [Purpose](#purpose) -- [OpenWeatherPlugin](#openweatherplugin) - - [Color Intense](#color-intense) - - [Animated](#animated) -- [BTCQuotePlugin](#btcquoteplugin) -- [Issues, Ideas And Bugs](#issues-ideas-and-bugs) -- [License](#license) -- [Contribution](#contribution) +* [Alternative Icons](#alternative-icons) +* [Purpose](#purpose) +* [OpenWeatherPlugin](#openweatherplugin) + * [Color Intense](#color-intense) + * [Animated](#animated) +* [BTCQuotePlugin](#btcquoteplugin) +* [Issues, Ideas And Bugs](#issues-ideas-and-bugs) +* [License](#license) +* [Contribution](#contribution) # Purpose diff --git a/doc/PLUGIN-DEV.md b/doc/PLUGIN-DEV.md index 0531fe54..45fbec8d 100644 --- a/doc/PLUGIN-DEV.md +++ b/doc/PLUGIN-DEV.md @@ -5,21 +5,21 @@ # Plugin development -- [What must be done?](#what-must-be-done) -- [Rules](#rules) -- [Recommendations](#recommendations) -- [Typical use cases](#typical-use-cases) - - [Initial configuration in filesystem](#initial-configuration-in-filesystem) - - [Reload configuration from filesystem periodically](#reload-configuration-from-filesystem-periodically) - - [Request information from URL periodically](#request-information-from-url-periodically) -- [Traps and pitfalls](#traps-and-pitfalls) - - [active/inactive](#activeinactive) -- [SW Architecture](#sw-architecture) - - [Static View](#static-view) - - [Dynamic View](#dynamic-view) -- [Issues, Ideas And Bugs](#issues-ideas-and-bugs) -- [License](#license) -- [Contribution](#contribution) +* [What must be done?](#what-must-be-done) +* [Rules](#rules) +* [Recommendations](#recommendations) +* [Typical use cases](#typical-use-cases) + * [Initial configuration in filesystem](#initial-configuration-in-filesystem) + * [Reload configuration from filesystem periodically](#reload-configuration-from-filesystem-periodically) + * [Request information from URL periodically](#request-information-from-url-periodically) +* [Traps and pitfalls](#traps-and-pitfalls) + * [active/inactive](#activeinactive) +* [SW Architecture](#sw-architecture) + * [Static View](#static-view) + * [Dynamic View](#dynamic-view) +* [Issues, Ideas And Bugs](#issues-ideas-and-bugs) +* [License](#license) +* [Contribution](#contribution) # What must be done? 1. Create the plugin as library: diff --git a/doc/REQUIREMENTS.md b/doc/REQUIREMENTS.md index c4d5de95..ca824b04 100644 --- a/doc/REQUIREMENTS.md +++ b/doc/REQUIREMENTS.md @@ -5,19 +5,19 @@ # Requirements -- [General](#general) -- [Communication](#communication) -- [User Interaction](#user-interaction) - - [Wifi](#wifi) - - [REST](#rest) - - [Websocket](#websocket) -- [Firmware update](#firmware-update) -- [TCP/IP Server](#tcpip-server) -- [Brightness Control](#brightness-control) -- [Real Time Clock](#real-time-clock) -- [Issues, Ideas And Bugs](#issues-ideas-and-bugs) -- [License](#license) -- [Contribution](#contribution) +* [General](#general) +* [Communication](#communication) +* [User Interaction](#user-interaction) + * [Wifi](#wifi) + * [REST](#rest) + * [Websocket](#websocket) +* [Firmware update](#firmware-update) +* [TCP/IP Server](#tcpip-server) +* [Brightness Control](#brightness-control) +* [Real Time Clock](#real-time-clock) +* [Issues, Ideas And Bugs](#issues-ideas-and-bugs) +* [License](#license) +* [Contribution](#contribution) Next available REQ id: 21 diff --git a/doc/SPRITESHEET.md b/doc/SPRITESHEET.md index 3e27958c..b7c7dd8f 100644 --- a/doc/SPRITESHEET.md +++ b/doc/SPRITESHEET.md @@ -5,12 +5,12 @@ # Sprite Sheet -- [Purpose](#purpose) -- [Tools And Scripts](#tools-and-scripts) -- [Limitations](#limitations) -- [Issues, Ideas And Bugs](#issues-ideas-and-bugs) -- [License](#license) -- [Contribution](#contribution) +* [Purpose](#purpose) +* [Tools And Scripts](#tools-and-scripts) +* [Limitations](#limitations) +* [Issues, Ideas And Bugs](#issues-ideas-and-bugs) +* [License](#license) +* [Contribution](#contribution) # Purpose Use a sprite sheet for 2d animations. diff --git a/doc/WEBSOCKET.md b/doc/WEBSOCKET.md index 61633ae6..27079ae9 100644 --- a/doc/WEBSOCKET.md +++ b/doc/WEBSOCKET.md @@ -5,29 +5,29 @@ # Websocket API -- [Get display pixel colors](#get-display-pixel-colors) -- [Get slots information](#get-slots-information) -- [Reset](#reset) -- [Brightness](#brightness) - - [Get brightness information](#get-brightness-information) - - [Set brightness](#set-brightness) - - [Set brightness and enable/enable automatic brightness adjustment](#set-brightness-and-enableenable-automatic-brightness-adjustment) - - [Response](#response) - - [Get plugins information](#get-plugins-information) -- [Install a plugin](#install-a-plugin) -- [Uninstall a plugin](#uninstall-a-plugin) -- [Move a plugin](#move-a-plugin) -- [Enable/Disable logging](#enabledisable-logging) - - [Is logging enabled?](#is-logging-enabled) - - [Enable/Disable logging to websocket](#enabledisable-logging-to-websocket) -- [Enable/Disable iperf](#enabledisable-iperf) - - [Is iperf enabled?](#is-iperf-enabled) - - [Start/Stop iperf server](#startstop-iperf-server) -- [Trigger virtual user button](#trigger-virtual-user-button) -- [Switch to next fade effect](#switch-to-next-fade-effect) -- [Issues, Ideas And Bugs](#issues-ideas-and-bugs) -- [License](#license) -- [Contribution](#contribution) +* [Get display pixel colors](#get-display-pixel-colors) +* [Get slots information](#get-slots-information) +* [Reset](#reset) +* [Brightness](#brightness) + * [Get brightness information](#get-brightness-information) + * [Set brightness](#set-brightness) + * [Set brightness and enable/enable automatic brightness adjustment](#set-brightness-and-enableenable-automatic-brightness-adjustment) + * [Response](#response) + * [Get plugins information](#get-plugins-information) +* [Install a plugin](#install-a-plugin) +* [Uninstall a plugin](#uninstall-a-plugin) +* [Move a plugin](#move-a-plugin) +* [Enable/Disable logging](#enabledisable-logging) + * [Is logging enabled?](#is-logging-enabled) + * [Enable/Disable logging to websocket](#enabledisable-logging-to-websocket) +* [Enable/Disable iperf](#enabledisable-iperf) + * [Is iperf enabled?](#is-iperf-enabled) + * [Start/Stop iperf server](#startstop-iperf-server) +* [Trigger virtual user button](#trigger-virtual-user-button) +* [Switch to next fade effect](#switch-to-next-fade-effect) +* [Issues, Ideas And Bugs](#issues-ideas-and-bugs) +* [License](#license) +* [Contribution](#contribution) # Get display pixel colors Command: ```GETDISP``` diff --git a/doc/architecture/README.md b/doc/architecture/README.md index 87b6fc08..aa05cb8e 100644 --- a/doc/architecture/README.md +++ b/doc/architecture/README.md @@ -5,32 +5,32 @@ # SW Architecture -- [Purpose](#purpose) -- [Scope](#scope) -- [Context](#context) -- [Deployment](#deployment) -- [Layers](#layers) - - [Application](#application) - - [Services](#services) - - [Hardware Abstraction Layer (HAL)](#hardware-abstraction-layer-hal) -- [System Startup](#system-startup) - - [High Level](#high-level) -- [System States](#system-states) -- [Details](#details) - - [Hardware Abstraction Layer (HAL)](#hardware-abstraction-layer-hal-1) - - [Graphic Base Functionality](#graphic-base-functionality) - - [Widgets](#widgets) - - [User Button Handling](#user-button-handling) - - [Audio Service](#audio-service) - - [Topic Handler Service](#topic-handler-service) - - [Plugin Handling](#plugin-handling) - - [Static View](#static-view) - - [Dynamic View](#dynamic-view) - - [Spectrum Analyzer](#spectrum-analyzer) - - [Slot Handling](#slot-handling) -- [Issues, Ideas And Bugs](#issues-ideas-and-bugs) -- [License](#license) -- [Contribution](#contribution) +* [Purpose](#purpose) +* [Scope](#scope) +* [Context](#context) +* [Deployment](#deployment) +* [Layers](#layers) + * [Application](#application) + * [Services](#services) + * [Hardware Abstraction Layer (HAL)](#hardware-abstraction-layer-hal) +* [System Startup](#system-startup) + * [High Level](#high-level) +* [System States](#system-states) +* [Details](#details) + * [Hardware Abstraction Layer (HAL)](#hardware-abstraction-layer-hal-1) + * [Graphic Base Functionality](#graphic-base-functionality) + * [Widgets](#widgets) + * [User Button Handling](#user-button-handling) + * [Audio Service](#audio-service) + * [Topic Handler Service](#topic-handler-service) + * [Plugin Handling](#plugin-handling) + * [Static View](#static-view) + * [Dynamic View](#dynamic-view) + * [Spectrum Analyzer](#spectrum-analyzer) + * [Slot Handling](#slot-handling) +* [Issues, Ideas And Bugs](#issues-ideas-and-bugs) +* [License](#license) +* [Contribution](#contribution) # Purpose The SW architecture provides an overview regarding the relationships of different classes and components. It serves for understanding, maintaining, fixing and extending the software. diff --git a/doc/config/README.md b/doc/config/README.md index cb06a32a..76430f35 100644 --- a/doc/config/README.md +++ b/doc/config/README.md @@ -5,17 +5,17 @@ # SW Configuration Management -- [Purpose](#purpose) -- [Scope](#scope) -- [PlatformIO](#platformio) - - [Logical Configuration Hierarchy And Dependencies](#logical-configuration-hierarchy-and-dependencies) -- [Version Numbers](#version-numbers) -- [Development Strategy](#development-strategy) -- [Work Instructions](#work-instructions) - - [How to release?](#how-to-release) -- [Issues, Ideas And Bugs](#issues-ideas-and-bugs) -- [License](#license) -- [Contribution](#contribution) +* [Purpose](#purpose) +* [Scope](#scope) +* [PlatformIO](#platformio) + * [Logical Configuration Hierarchy And Dependencies](#logical-configuration-hierarchy-and-dependencies) +* [Version Numbers](#version-numbers) +* [Development Strategy](#development-strategy) +* [Work Instructions](#work-instructions) + * [How to release?](#how-to-release) +* [Issues, Ideas And Bugs](#issues-ideas-and-bugs) +* [License](#license) +* [Contribution](#contribution) # Purpose The SW configuration management document provides a general overview about what is configured and how it is done. diff --git a/doc/config/SW-BUILD.md b/doc/config/SW-BUILD.md index 046f3d30..dad2fa78 100644 --- a/doc/config/SW-BUILD.md +++ b/doc/config/SW-BUILD.md @@ -5,12 +5,12 @@ # Build The Software -- [Retrieve The Project](#retrieve-the-project) -- [Build Project](#build-project) -- [Next Step](#next-step) -- [Issues, Ideas And Bugs](#issues-ideas-and-bugs) -- [License](#license) -- [Contribution](#contribution) +* [Retrieve The Project](#retrieve-the-project) +* [Build Project](#build-project) +* [Next Step](#next-step) +* [Issues, Ideas And Bugs](#issues-ideas-and-bugs) +* [License](#license) +* [Contribution](#contribution) # Retrieve The Project diff --git a/doc/config/SW-UPDATE.md b/doc/config/SW-UPDATE.md index f8aacd27..7065d084 100644 --- a/doc/config/SW-UPDATE.md +++ b/doc/config/SW-UPDATE.md @@ -5,23 +5,23 @@ # Update The Software -- [Purpose](#purpose) -- [Recommendations](#recommendations) - - [Non-Developer](#non-developer) - - [Developer](#developer) -- [Update possibilities](#update-possibilities) - - [Use Espressif Flash Download Tool (Windows only)](#use-espressif-flash-download-tool-windows-only) - - [Use esptool](#use-esptool) - - [Debian Linux](#debian-linux) - - [Windows](#windows) - - [Common after python and esptool are installed](#common-after-python-and-esptool-are-installed) - - [Use VSCode and Platformio](#use-vscode-and-platformio) -- [Update via USB](#update-via-usb) -- [Use the browser](#use-the-browser) -- [Flash Layout Information](#flash-layout-information) -- [Issues, Ideas And Bugs](#issues-ideas-and-bugs) -- [License](#license) -- [Contribution](#contribution) +* [Purpose](#purpose) +* [Recommendations](#recommendations) + * [Non-Developer](#non-developer) + * [Developer](#developer) +* [Update possibilities](#update-possibilities) + * [Use Espressif Flash Download Tool (Windows only)](#use-espressif-flash-download-tool-windows-only) + * [Use esptool](#use-esptool) + * [Debian Linux](#debian-linux) + * [Windows](#windows) + * [Common after python and esptool are installed](#common-after-python-and-esptool-are-installed) + * [Use VSCode and Platformio](#use-vscode-and-platformio) +* [Update via USB](#update-via-usb) +* [Use the browser](#use-the-browser) +* [Flash Layout Information](#flash-layout-information) +* [Issues, Ideas And Bugs](#issues-ideas-and-bugs) +* [License](#license) +* [Contribution](#contribution) # Purpose The software can be uploaded/updated to the development board in 4 different ways. Not all of them can be used in any case. Take a look to the recommendations which variant might be the best for you. diff --git a/doc/config/TOOLCHAIN-INSTALLATION.md b/doc/config/TOOLCHAIN-INSTALLATION.md index 5d8b1ed3..64c494df 100644 --- a/doc/config/TOOLCHAIN-INSTALLATION.md +++ b/doc/config/TOOLCHAIN-INSTALLATION.md @@ -5,18 +5,18 @@ # Toolchain Installation -- [Purpose](#purpose) -- [Installation Of VSCode](#installation-of-vscode) -- [Installation Of git](#installation-of-git) - - [Linux (Debian, Ubuntu)](#linux-debian-ubuntu) - - [Windows](#windows) -- [Installation Of Test Environment](#installation-of-test-environment) - - [Linux (Debian, Ubuntu)](#linux-debian-ubuntu-1) - - [Windows](#windows-1) -- [Next Step](#next-step) -- [Issues, Ideas And Bugs](#issues-ideas-and-bugs) -- [License](#license) -- [Contribution](#contribution) +* [Purpose](#purpose) +* [Installation Of VSCode](#installation-of-vscode) +* [Installation Of git](#installation-of-git) + * [Linux (Debian, Ubuntu)](#linux-debian-ubuntu) + * [Windows](#windows) +* [Installation Of Test Environment](#installation-of-test-environment) + * [Linux (Debian, Ubuntu)](#linux-debian-ubuntu-1) + * [Windows](#windows-1) +* [Next Step](#next-step) +* [Issues, Ideas And Bugs](#issues-ideas-and-bugs) +* [License](#license) +* [Contribution](#contribution) # Purpose From 3962e00f9c058bcb2b0db263a36c16ab2ad4dfe3 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 29 Oct 2023 16:25:00 +0100 Subject: [PATCH 036/105] Debug log removed. --- lib/AsyncHttpClient/src/AsyncHttpClient.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/AsyncHttpClient/src/AsyncHttpClient.cpp b/lib/AsyncHttpClient/src/AsyncHttpClient.cpp index a6b6abb4..8d468bf2 100644 --- a/lib/AsyncHttpClient/src/AsyncHttpClient.cpp +++ b/lib/AsyncHttpClient/src/AsyncHttpClient.cpp @@ -727,8 +727,6 @@ void AsyncHttpClient::onData(const uint8_t* data, size_t len) * [ message-body ] */ - LOG_DEBUG("onData(): len = %u", len); - while((len > index) && (false == isError)) { switch(m_rspPart) From d9a0855dc76a670934fc4a8d137f1365fd81691f Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 29 Oct 2023 16:36:10 +0100 Subject: [PATCH 037/105] Home Assistant support fixed. #132 --- config/configSmallUlanzi.ini | 2 +- doc/HOMEASSISTANT.md | 2 +- doc/MQTT.md | 8 +- doc/architecture/uml/plugin-service.wsd | 4 +- doc/architecture/uml/plugin_cfg_handling.wsd | 4 +- .../uml/topic_handler_service.wsd | 55 +++-- .../uml/topic_handling_mindmap.wsd | 58 +++-- lib/CountdownPlugin/web/CountdownPlugin.html | 12 +- lib/DateTimePlugin/web/DateTimePlugin.html | 12 +- .../web/GrabViaMqttPlugin.html | 10 +- .../web/GrabViaRestPlugin.html | 10 +- lib/GruenbeckPlugin/web/GruenbeckPlugin.html | 12 +- .../src/IconTextLampPlugin.cpp | 11 +- .../web/IconTextLampPlugin.html | 36 +-- lib/IconTextPlugin/src/IconTextPlugin.cpp | 11 +- lib/IconTextPlugin/web/IconTextPlugin.html | 24 +- lib/JustTextPlugin/src/JustTextPlugin.cpp | 15 +- lib/JustTextPlugin/web/JustTextPlugin.html | 12 +- .../src/HomeAssistantMqtt.cpp | 112 ++++----- .../src/HomeAssistantMqtt.h | 43 ++-- .../src/MqttApiTopicHandler.cpp | 39 ++-- .../web/OpenWeatherPlugin.html | 12 +- .../src/RestApiTopicHandler.cpp | 16 +- lib/SensorPlugin/web/SensorPlugin.html | 8 +- .../web/SignalDetectorPlugin.html | 12 +- .../web/SoundReactivePlugin.html | 12 +- lib/SunrisePlugin/web/SunrisePlugin.html | 12 +- lib/ThreeIconPlugin/web/ThreeIconPlugin.html | 26 +-- .../src/TopicHandlerService.cpp | 28 ++- .../src/TopicHandlerService.h | 18 ++ lib/VolumioPlugin/web/VolumioPlugin.html | 12 +- src/Hal/SensorDataProvider.cpp | 215 +++++++++++++++++- src/Hal/SensorDataProvider.h | 20 ++ src/StateMachine/RestartState.cpp | 2 + 34 files changed, 583 insertions(+), 302 deletions(-) diff --git a/config/configSmallUlanzi.ini b/config/configSmallUlanzi.ini index 69ff04ff..7b326c9f 100644 --- a/config/configSmallUlanzi.ini +++ b/config/configSmallUlanzi.ini @@ -30,7 +30,7 @@ lib_deps = MatrixPlugin @ ~0.1.0 OpenWeatherPlugin @ ~0.1.0 ;RainbowPlugin @ ~0.1.0 - SensorPlugin @ ~0.1.0 + ;SensorPlugin @ ~0.1.0 ;SignalDetectorPlugin @ ~0.1.0 # Requires AudioService ;SoundReactivePlugin @ ~0.1.0 # Requires AudioService SunrisePlugin @ ~0.1.0 diff --git a/doc/HOMEASSISTANT.md b/doc/HOMEASSISTANT.md index 754ffaf9..06c69b06 100644 --- a/doc/HOMEASSISTANT.md +++ b/doc/HOMEASSISTANT.md @@ -31,7 +31,7 @@ Add the following lines of code to your `configuration.yaml`: ```yaml rest_command: pixelix_just_text: - url: 'http://192.168.178.10/rest/api/v1/display/{{ uid }}/text?text={{ "%5C" + align + "%5C" + color + text }}' + url: 'http://192.168.178.10/rest/api/v1/display/uid/{{ uid }}/text?text={{ "%5C" + align + "%5C" + color + text }}' method: POST ``` You need to replace the IP `192.168.178.10` with your Pixelix instance IP. diff --git a/doc/MQTT.md b/doc/MQTT.md index b58792f8..4d928163 100644 --- a/doc/MQTT.md +++ b/doc/MQTT.md @@ -42,16 +42,16 @@ After the successful connection establishment to the MQTT broker, Pixelix will s ## Plugin base URI The base URI to access plugin related topics can be setup with the plugin UID or the plugin alias: -* <HOSTNAME>/<PLUGIN-UID>/... -* <HOSTNAME>/<PLUGIN-ALIAS>/... +* <HOSTNAME>/uid/<PLUGIN-UID>/... +* <HOSTNAME>/alias/<PLUGIN-ALIAS>/... ## Topic name The complete topic name can be derived from the REST API documentation. Example: JustTextPlugin -The REST API URL looks like the following: http://<HOSTNAME>/rest/api/v1/display/<PLUGIN-UID>/text?text=<TEXT> -1. Replace the http://<HOSTNAME>/rest/api/v1/display part with <HOSTNAME> --> <HOSTNAME>/<PLUGIN-UID>/text?text=<TEXT> +The REST API URL looks like the following: http://<HOSTNAME>/rest/api/v1/display/uid/<PLUGIN-UID>/text?text=<TEXT> +1. Replace the http://<HOSTNAME>/rest/api/v1/display part with <HOSTNAME> --> <HOSTNAME>/uid/<PLUGIN-UID>/text?text=<TEXT> 2. Every URL parameter, which is in this case show=<TEXT> must be sent in JSON format. ```json diff --git a/doc/architecture/uml/plugin-service.wsd b/doc/architecture/uml/plugin-service.wsd index 80200005..00f0a107 100644 --- a/doc/architecture/uml/plugin-service.wsd +++ b/doc/architecture/uml/plugin-service.wsd @@ -162,8 +162,8 @@ note left of PluginMgr plugin topics. The REST API URL depends on the plugin UID or alias: - * /rest/api/v1/display/ - * /rest/api/v1/display/ + * /rest/api/v1/display/uid/ + * /rest/api/v1/display/alias/ end note class ESPAsyncWebserver diff --git a/doc/architecture/uml/plugin_cfg_handling.wsd b/doc/architecture/uml/plugin_cfg_handling.wsd index 9f56f731..b075b34f 100644 --- a/doc/architecture/uml/plugin_cfg_handling.wsd +++ b/doc/architecture/uml/plugin_cfg_handling.wsd @@ -35,7 +35,7 @@ note over restApiTopicHandler,fs Read configuration via REST API. end note --> restApiTopicHandler: GET /rest/api/v1/display// +-> restApiTopicHandler: GET /rest/api/v1/display/uid// note over restApiTopicHandler The topic handler would be able to read the configuration from the filesystem. But this shall be avoided because of @@ -50,7 +50,7 @@ note over restApiTopicHandler,fs Write configuration via REST API. end note --> restApiTopicHandler: POST /rest/api/v1/display// with JSON +-> restApiTopicHandler: POST /rest/api/v1/display/uid// with JSON note over plugin Don't write configuration to flash, because flash access is slow. It shall be written during plugin processing. diff --git a/doc/architecture/uml/topic_handler_service.wsd b/doc/architecture/uml/topic_handler_service.wsd index afbabd01..10789523 100644 --- a/doc/architecture/uml/topic_handler_service.wsd +++ b/doc/architecture/uml/topic_handler_service.wsd @@ -18,7 +18,7 @@ end note -> pluginMgr loop for every plugin - pluginMgr -> topicHandlerService: register plugin topics + pluginMgr -> topicHandlerService: register plugin topics with topicHandlerService -> plugin: get topics topicHandlerService <-- plugin: topics @@ -30,29 +30,45 @@ loop for every plugin loop for every topic handler - topicHandlerService -> restApiTopicHandler: register plugin topic (plugin, topic, accessiblity, extra parameters) + note over topicHandlerService + by plugin UID = "display/uid/" + end note + + topicHandlerService -> restApiTopicHandler: register topic (device id, entity id, topic, accessiblity, extra parameters) note over restApiTopicHandler,httpServer - URL: /display// + URL: // end note - restApiTopicHandler -> httpServer: register REST URL by plugin UID + restApiTopicHandler -> httpServer: register REST URL restApiTopicHandler <-- httpServer - alt If plugin alias available + topicHandlerService <-- restApiTopicHandler + + alt If plugin alias available + + note over topicHandlerService + by plugin alias = "display/alias/" + end note + + topicHandlerService -> restApiTopicHandler: register topic (device id, entity id, topic, accessiblity, extra parameters) note over restApiTopicHandler,httpServer - URL: /display// + URL: // end note - restApiTopicHandler -> httpServer: register REST URL by plugin alias + restApiTopicHandler -> httpServer: register REST URL restApiTopicHandler <-- httpServer - end alt + topicHandlerService <-- restApiTopicHandler - topicHandlerService <-- restApiTopicHandler + end alt + + note over topicHandlerService + by plugin UID = "display/uid/" + end note - topicHandlerService -> mqttApiTopicHandler: register plugin topic (plugin, topic, accessiblity, extra parameters) + topicHandlerService -> mqttApiTopicHandler: register topic (device id, entity id, topic, accessiblity, extra parameters) note over mqttApiTopicHandler Readable topics will be published during processing: @@ -64,8 +80,8 @@ loop for every plugin alt If topic is readable note over mqttApiTopicHandler,mqttClient - path: /display///state - path: /display///state + path: ///state + path: ///state end note mqttApiTopicHandler -> mqttApiTopicHandler: Store plugin topic in publisher queue @@ -76,7 +92,7 @@ loop for every plugin alt If topic is writeable note over mqttApiTopicHandler,mqttClient - path: /display///set + path: ///set end note mqttApiTopicHandler -> mqttClient: Subscribe plugin topic path by plugin UID @@ -85,7 +101,7 @@ loop for every plugin alt If plugin alias available note over mqttApiTopicHandler,mqttClient - path: /display///set + path: ///set end note mqttApiTopicHandler -> mqttClient: Subscribe plugin topic path by plugin alias @@ -97,6 +113,17 @@ loop for every plugin topicHandlerService <-- mqttApiTopicHandler + alt If plugin alias available + + note over topicHandlerService + by plugin alias = "display/alias/" + end note + + topicHandlerService -> mqttApiTopicHandler: ... + topicHandlerService <-- mqttApiTopicHandler + + end alt + end loop end loop diff --git a/doc/architecture/uml/topic_handling_mindmap.wsd b/doc/architecture/uml/topic_handling_mindmap.wsd index 88015f68..d8b19b7c 100644 --- a/doc/architecture/uml/topic_handling_mindmap.wsd +++ b/doc/architecture/uml/topic_handling_mindmap.wsd @@ -1,6 +1,7 @@ @startmindmap +[#lightgreen] Topic + ++ Access +++ REST API @@ -8,12 +9,16 @@ ++++ Base URI: http:///rest/api/v1 ++++ GET -+++++ URL: /display// -+++++ URL: /display// ++++++ URL: // +++++++ Examples ++++++++ /display/uid/1234/text ++++++++ /display/alias/myPlugin/text ++++ POST -+++++ URL: /display// -+++++ URL: /display// ++++++ URL: // +++++++ Examples ++++++++ /display/uid/1234/text ++++++++ /display/alias/myPlugin/text +++++ Parameters via HTTP arguments ******: key=value __converts to__ @@ -73,13 +78,15 @@ +++ MQTT API ++++ Homeassistant auto discovery -+++++ Topic with plugin UID: ////config ++++++ General path +++++++ ////config ++++++ Topic with plugin UID: ///display/uid//config *****: { "name": "MQTT text", - "object_id": "19583", - "unique_id": "pixelix-facfc834/19583", + "object_id": "display/uid/19583", + "unique_id": "pixelix-facfc834/display/uid/19583", "device": { "identifiers": "F4:12:FA:CF:C8:34", "configuration_url": "http://192.168.2.62", @@ -88,19 +95,19 @@ "manufacturer": "BlueAndi & Friends", "sw_version": "v7.0.0" }, - "state_topic": "pixelix-facfc834/19583/text/state", + "state_topic": "pixelix-facfc834/display/uid/19583/text/state", "value_template": "{{ value_json.text }}", - "command_topic": "pixelix-facfc834/19583/text/set", + "command_topic": "pixelix-facfc834/display/uid/19583/text/set", "command_template": "{\"text\": \"{{ value }}\" }" } ; -+++++ Topic with plugin alias: ////config ++++++ Topic with plugin alias: ///display/alias//config *****: { "name": "MQTT text", - "object_id": "display", - "unique_id": "pixelix-facfc834/display", + "object_id": "display/alias/myPlugin", + "unique_id": "pixelix-facfc834/display/alias/myPlugin", "device": { "identifiers": "F4:12:FA:CF:C8:34", "configuration_url": "http://192.168.2.62", @@ -109,9 +116,9 @@ "manufacturer": "BlueAndi & Friends", "sw_version": "v7.0.0" }, - "state_topic": "pixelix-facfc834/display/text/state", + "state_topic": "pixelix-facfc834/display/alias/myPlugin/text/state", "value_template": "{{ value_json.text }}", - "command_topic": "pixelix-facfc834/display/text/set", + "command_topic": "pixelix-facfc834/display/alias/myPlugin/text/set", "command_template": "{\"text\": \"{{ value }}\" }" } ; @@ -127,25 +134,34 @@ ++++++ https://www.home-assistant.io/integrations/sensor.mqtt/ ++++ Status -+++++ MQTT topic: ///state -+++++ MQTT topic: ///state ++++++ MQTT topic by plugin UID: /display/uid///state ++++++ MQTT topic by plugin alias: /display/alias///state ++++++ MQTT topic by sensor: /sensors///state +++++ Direction: Pixelix --> Client(s) ++++ Command -+++++ MQTT topic: ///set -+++++ MQTT topic: ///set ++++++ MQTT topic: /display/uid///set ++++++ MQTT topic: /display/alias///set +++++ Direction: Client(s) --> Pixelix ++++ Availability -+++++ MQTT topic: ///available -+++++ MQTT topic: ///available ++++++ MQTT topic: /display/uid///available ++++++ MQTT topic: /display/alias///available +++++ Direction: Pixelix --> Client(s) +++++ "online"/"offline" +++++ Skipped for the moment. May be supported in future. -- A plugin deals only with topics\nand doesn't know about REST or MQTT. --- Device identification +-- Device identification\n(DEVICE-ID) --- Unique hostname (HOSTNAME) +-- Entity identification\n(ENTITY-ID) +--- Plugins +---- display/uid/ +---- display/alias/ +--- Sensors +---- sensors/ -- Plugin identification --- Unique plugin ID (PLUGIN-UID) assigned at instance creation --- Unique plugin alias (PLUGIN-ALIAS) assigned by user +-- Sensor/Channel identification +--- The provided sensor/channel information is part of a array.\nThe array index is the . @endmindmap diff --git a/lib/CountdownPlugin/web/CountdownPlugin.html b/lib/CountdownPlugin/web/CountdownPlugin.html index 02b302b7..e0e2e5ea 100644 --- a/lib/CountdownPlugin/web/CountdownPlugin.html +++ b/lib/CountdownPlugin/web/CountdownPlugin.html @@ -37,15 +37,15 @@

                                CountdownPlugin

                                The plugin shows the remaining days until a configured target date.

                                REST API

                                Get target date and target day description.

                                -
                                GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/countdown
                                -
                                GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/countdown
                                +
                                GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/countdown
                                +
                                GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/countdown
                                • PLUGIN-UID: The plugin unique id.
                                • PLUGIN-ALIAS: The plugin alias name.

                                Set target day and target day desription.

                                -
                                POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/countdown?day=<DAY>&month=<MONTH>&year=<YEAR>&descPlural=<PLURAL>&descSingular=<SINGULAR>
                                -
                                POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/countdown?day=<DAY>&month=<MONTH>&year=<YEAR>&descPlural=<PLURAL>&descSingular=<SINGULAR>
                                +
                                POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/countdown?day=<DAY>&month=<MONTH>&year=<YEAR>&descPlural=<PLURAL>&descSingular=<SINGULAR>
                                +
                                POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/countdown?day=<DAY>&month=<MONTH>&year=<YEAR>&descPlural=<PLURAL>&descSingular=<SINGULAR>
                                • PLUGIN-UID: The plugin unique id.
                                • PLUGIN-ALIAS: The plugin alias name.
                                • @@ -155,7 +155,7 @@

                                  Target Date

                                  disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/" + pluginUid + "/countdown", + url: "/rest/api/v1/display/uid/" + pluginUid + "/countdown", isJsonResponse: true }).then(function(rsp) { var targetDateInput = document.getElementById("targetDate"); @@ -176,7 +176,7 @@

                                  Target Date

                                  return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/countdown", + url: "/rest/api/v1/display/uid/" + pluginUid + "/countdown", isJsonResponse: true, parameter: { day: date.getDate(), diff --git a/lib/DateTimePlugin/web/DateTimePlugin.html b/lib/DateTimePlugin/web/DateTimePlugin.html index df055ba0..af310eb3 100644 --- a/lib/DateTimePlugin/web/DateTimePlugin.html +++ b/lib/DateTimePlugin/web/DateTimePlugin.html @@ -54,11 +54,11 @@

                                  Date/Time Format

                                  By default the local time (see timezone in the settings) is used. It can be overwritten by the plugin configuration.

                                  REST API

                                  Get configuration about what the plugin shows.

                                  -
                                  GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/dateTime
                                  -
                                  GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/dateTime
                                  +
                                  GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/dateTime
                                  +
                                  GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/dateTime

                                  Set configuration about what the plugin shall show.

                                  -
                                  POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/dateTime?mode=<MODE>
                                  -
                                  POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/dateTime?mode=<MODE>
                                  +
                                  POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/dateTime?mode=<MODE>
                                  +
                                  POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/dateTime?mode=<MODE>
                                  • PLUGIN-UID: The plugin unique id.
                                  • PLUGIN-ALIAS: The plugin alias name.
                                  • @@ -185,7 +185,7 @@

                                    Display

                                    disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/" + pluginUid + "/dateTime", + url: "/rest/api/v1/display/uid/" + pluginUid + "/dateTime", isJsonResponse: true }).then(function(rsp) { $("#mode").val(rsp.data.mode); @@ -205,7 +205,7 @@

                                    Display

                                    disableUI(); return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/dateTime", + url: "/rest/api/v1/display/uid/" + pluginUid + "/dateTime", isJsonResponse: true, parameter: { mode: $("#mode").val(), diff --git a/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html b/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html index 90d50085..39f8a67c 100644 --- a/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html +++ b/lib/GrabViaMqttPlugin/web/GrabViaMqttPlugin.html @@ -37,10 +37,10 @@

                                    GrabViaMqttPlugin

                                    The plugin can grab information in JSON format via MQTT and shows it on the display.

                                    REST API

                                    Get configuration

                                    -
                                    GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/grabConfig
                                    +
                                    GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/grabConfig

                                    Set configuration

                                    -
                                    POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/grabConfig?path=<PATH>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
                                    -
                                    POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/grabConfig?path=<PATH>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
                                    +
                                    POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/grabConfig?path=<PATH>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
                                    +
                                    POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/grabConfig?path=<PATH>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
                                    • PLUGIN-UID: The plugin unique id.
                                    • PLUGIN-ALIAS: The plugin alias name.
                                    • @@ -164,7 +164,7 @@

                                      User

                                      disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/" + pluginUid + "/grabConfig", + url: "/rest/api/v1/display/uid/" + pluginUid + "/grabConfig", isJsonResponse: true }).then(function(rsp) { $("#path").val(rsp.data.path); @@ -185,7 +185,7 @@

                                      User

                                      return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/grabConfig", + url: "/rest/api/v1/display/uid/" + pluginUid + "/grabConfig", isJsonResponse: true, parameter: { path: $("#path").val(), diff --git a/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html b/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html index ed3c64bd..0257709f 100644 --- a/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html +++ b/lib/GrabViaRestPlugin/web/GrabViaRestPlugin.html @@ -37,10 +37,10 @@

                                      GrabViaRestPlugin

                                      The plugin can grab information in JSON format via REST API and shows it on the display.

                                      REST API

                                      Get configuration

                                      -
                                      GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/grabConfig
                                      +
                                      GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/grabConfig

                                      Set configuration

                                      -
                                      POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/grabConfig?method=<METHOD>&url=<URL>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
                                      -
                                      POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/grabConfig?method=<METHOD>&url=<URL>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
                                      +
                                      POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/grabConfig?method=<METHOD>&url=<URL>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
                                      +
                                      POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/grabConfig?method=<METHOD>&url=<URL>&filter=<FILTER>&iconPath=<ICON-PATH>&format=<FORMAT>&MULTIPLIER=<MULTIPLIER>&OFFSET=<OFFSET>
                                      • PLUGIN-UID: The plugin unique id.
                                      • PLUGIN-ALIAS: The plugin alias name.
                                      • @@ -169,7 +169,7 @@

                                        User

                                        disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/" + pluginUid + "/grabConfig", + url: "/rest/api/v1/display/uid/" + pluginUid + "/grabConfig", isJsonResponse: true }).then(function(rsp) { $("#method").val(rsp.data.method); @@ -191,7 +191,7 @@

                                        User

                                        return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/grabConfig", + url: "/rest/api/v1/display/uid/" + pluginUid + "/grabConfig", isJsonResponse: true, parameter: { method: $("#method").val(), diff --git a/lib/GruenbeckPlugin/web/GruenbeckPlugin.html b/lib/GruenbeckPlugin/web/GruenbeckPlugin.html index afd8efec..75753184 100644 --- a/lib/GruenbeckPlugin/web/GruenbeckPlugin.html +++ b/lib/GruenbeckPlugin/web/GruenbeckPlugin.html @@ -37,11 +37,11 @@

                                        GruenbeckPlugin

                                        The plugin shows the remaining system capacity (parameter = D_Y_10_1 ) of the Gruenbeck softliQ SC18 via the system's RESTful webservice.

                                        REST API

                                        Get ip-address

                                        -
                                        GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/ipAddress
                                        -
                                        GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/ipAddress
                                        +
                                        GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/ipAddress
                                        +
                                        GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/ipAddress

                                        Set ip-address

                                        -
                                        POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/ipAddress?gruenbeckIP=<IPADDRESS>
                                        -
                                        POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/ipAddress?gruenbeckIP=<IPADDRESS>
                                        +
                                        POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/ipAddress?gruenbeckIP=<IPADDRESS>
                                        +
                                        POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/ipAddress?gruenbeckIP=<IPADDRESS>
                                        • PLUGIN-UID: The plugin unique id.
                                        • PLUGIN-ALIAS: The plugin alias name.
                                        • @@ -139,7 +139,7 @@

                                          IP-address

                                          disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/" + pluginUid + "/ipAddress", + url: "/rest/api/v1/display/uid/" + pluginUid + "/ipAddress", isJsonResponse: true }).then(function(rsp) { $("#gruenbeckIP").val(rsp.data.gruenbeckIP); @@ -154,7 +154,7 @@

                                          IP-address

                                          disableUI(); return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/ipAddress", + url: "/rest/api/v1/display/uid/" + pluginUid + "/ipAddress", isJsonResponse: true, parameter: { gruenbeckIP: $("#gruenbeckIP").val() diff --git a/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp b/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp index 17a34c74..08fc1a78 100644 --- a/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp +++ b/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp @@ -94,11 +94,12 @@ void IconTextLampPlugin::getTopics(JsonArray& topics) const jsonText["name"] = TOPIC_TEXT; - /* Home Assistant support of MQTT discovery */ - jsonText["ha"]["component"] = "text"; - jsonText["ha"]["commandTemplate"] = "{\"text\": \"{{ value }}\" }"; - jsonText["ha"]["valueTemplate"] = "{{ value_json.text }}"; - jsonText["ha"]["icon"] = "mdi:form-textbox"; + /* Home Assistant support of MQTT discovery (https://www.home-assistant.io/integrations/mqtt) */ + jsonText["ha"]["component"] = "text"; /* MQTT integration */ + jsonText["ha"]["discovery"]["name"] = "MQTT text"; /* Application that is the origin the discovered MQTT. */ + jsonText["ha"]["discovery"]["cmd_tpl"] = "{\"text\": \"{{ value }}\" }"; /* Command template */ + jsonText["ha"]["discovery"]["val_tpl"] = "{{ value_json.text }}"; /* Value template */ + jsonText["ha"]["discovery"]["ic"] = "mdi:form-textbox"; /* Icon (MaterialDesignIcons.com) */ jsonIcon["name"] = TOPIC_ICON; jsonIcon["access"] = "w"; /* Only icon upload is supported. */ diff --git a/lib/IconTextLampPlugin/web/IconTextLampPlugin.html b/lib/IconTextLampPlugin/web/IconTextLampPlugin.html index e03a3819..58a95383 100644 --- a/lib/IconTextLampPlugin/web/IconTextLampPlugin.html +++ b/lib/IconTextLampPlugin/web/IconTextLampPlugin.html @@ -50,15 +50,15 @@

                                          IconTextLampPlugin

                                          If MQTT is built in and enabled, it will support Home Assistant MQTT discovery.

                                          REST API

                                          Get text

                                          -
                                          GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/text
                                          -
                                          GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/text
                                          +
                                          GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/text
                                          +
                                          GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/text
                                          • PLUGIN-UID: The plugin unique id.
                                          • PLUGIN-ALIAS: The plugin alias name.

                                          Set text

                                          -
                                          POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                                          -
                                          POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                                          +
                                          POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                                          +
                                          POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                                          • PLUGIN-UID: The plugin unique id.
                                          • PLUGIN-ALIAS: The plugin alias name.
                                          • @@ -67,29 +67,29 @@

                                            Set text

                                          • SPRITESHEET-FULL-PATH: Full path to the sprite sheet.

                                          Upload icon

                                          -
                                          POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/bitmap
                                          -
                                          POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/bitmap
                                          +
                                          POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/bitmap
                                          +
                                          POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/bitmap
                                          • PLUGIN-UID: The plugin unique id.
                                          • PLUGIN-ALIAS: The plugin alias name.

                                          Upload sprite sheet

                                          -
                                          POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/spritesheet
                                          -
                                          POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/spritesheet
                                          +
                                          POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/spritesheet
                                          +
                                          POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/spritesheet
                                          • PLUGIN-UID: The plugin unique id.
                                          • PLUGIN-ALIAS: The plugin alias name.

                                          Get all lamp states

                                          -
                                          GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/lamps
                                          -
                                          GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/lamps
                                          +
                                          GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/lamps
                                          +
                                          GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/lamps
                                          • PLUGIN-UID: The plugin unique id.
                                          • PLUGIN-ALIAS: The plugin alias name.

                                          Set lamp state

                                          -
                                          POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/lamp/<LAMP-ID>?state=<LAMP-STATE>
                                          -
                                          POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/lamp/<LAMP-ID>?state=<LAMP-STATE>
                                          +
                                          POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/lamp/<LAMP-ID>?state=<LAMP-STATE>
                                          +
                                          POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/lamp/<LAMP-ID>?state=<LAMP-STATE>
                                          • PLUGIN-UID: The plugin unique id.
                                          • PLUGIN-ALIAS: The plugin alias name.
                                          • @@ -259,7 +259,7 @@

                                            Lamp

                                            return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/bitmap", + url: "/rest/api/v1/display/uid/" + pluginUid + "/bitmap", isJsonResponse: true, parameter: { file: file @@ -282,7 +282,7 @@

                                            Lamp

                                            return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/spritesheet", + url: "/rest/api/v1/display/uid/" + pluginUid + "/spritesheet", isJsonResponse: true, parameter: { file: file @@ -304,7 +304,7 @@

                                            Lamp

                                            disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/" + pluginUid + "/text", + url: "/rest/api/v1/display/uid/" + pluginUid + "/text", isJsonResponse: true }).then(function(rsp) { var justTextInput = document.getElementById(justTextId); @@ -327,7 +327,7 @@

                                            Lamp

                                            return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/text", + url: "/rest/api/v1/display/uid/" + pluginUid + "/text", isJsonResponse: true, parameter: { text: justText, @@ -347,7 +347,7 @@

                                            Lamp

                                            disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/" + pluginUid + "/lamps", + url: "/rest/api/v1/display/uid/" + pluginUid + "/lamps", isJsonResponse: true }).then(function(rsp) { var selectLampState = document.getElementById(lampStateId); @@ -370,7 +370,7 @@

                                            Lamp

                                            return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/lamp/" + lampId, + url: "/rest/api/v1/display/uid/" + pluginUid + "/lamp/" + lampId, isJsonResponse: true, parameter: { state: lampState diff --git a/lib/IconTextPlugin/src/IconTextPlugin.cpp b/lib/IconTextPlugin/src/IconTextPlugin.cpp index a2fa62dc..8eda4b5a 100644 --- a/lib/IconTextPlugin/src/IconTextPlugin.cpp +++ b/lib/IconTextPlugin/src/IconTextPlugin.cpp @@ -99,11 +99,12 @@ void IconTextPlugin::getTopics(JsonArray& topics) const jsonText["name"] = TOPIC_TEXT; - /* Home Assistant support of MQTT discovery */ - jsonText["ha"]["component"] = "text"; - jsonText["ha"]["commandTemplate"] = "{\"text\": \"{{ value }}\" }"; - jsonText["ha"]["valueTemplate"] = "{{ value_json.text }}"; - jsonText["ha"]["icon"] = "mdi:form-textbox"; + /* Home Assistant support of MQTT discovery (https://www.home-assistant.io/integrations/mqtt) */ + jsonText["ha"]["component"] = "text"; /* MQTT integration */ + jsonText["ha"]["discovery"]["name"] = "MQTT text"; /* Application that is the origin the discovered MQTT. */ + jsonText["ha"]["discovery"]["cmd_tpl"] = "{\"text\": \"{{ value }}\" }"; /* Command template */ + jsonText["ha"]["discovery"]["val_tpl"] = "{{ value_json.text }}"; /* Value template */ + jsonText["ha"]["discovery"]["ic"] = "mdi:form-textbox"; /* Icon (MaterialDesignIcons.com) */ jsonIcon["name"] = TOPIC_ICON; jsonIcon["access"] = "w"; /* Only icon upload is supported. */ diff --git a/lib/IconTextPlugin/web/IconTextPlugin.html b/lib/IconTextPlugin/web/IconTextPlugin.html index 8f963b0c..a51835eb 100644 --- a/lib/IconTextPlugin/web/IconTextPlugin.html +++ b/lib/IconTextPlugin/web/IconTextPlugin.html @@ -50,15 +50,15 @@

                                            IconTextPlugin

                                            If MQTT is built in and enabled, it will support Home Assistant MQTT discovery.

                                            REST API

                                            Get text

                                            -
                                            GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/text
                                            -
                                            GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/text
                                            +
                                            GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/text
                                            +
                                            GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/text
                                            • PLUGIN-UID: The plugin unique id.
                                            • PLUGIN-ALIAS: The plugin alias name.

                                            Set text

                                            -
                                            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                                            -
                                            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                                            +
                                            POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                                            +
                                            POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/text?text=<TEXT>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                                            • PLUGIN-UID: The plugin unique id.
                                            • PLUGIN-ALIAS: The plugin alias name.
                                            • @@ -67,15 +67,15 @@

                                              Set text

                                            • SPRITESHEET-FULL-PATH: Full path to the sprite sheet.

                                            Upload icon

                                            -
                                            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/bitmap
                                            -
                                            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/bitmap
                                            +
                                            POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/bitmap
                                            +
                                            POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/bitmap
                                            • PLUGIN-UID: The plugin unique id.
                                            • PLUGIN-ALIAS: The plugin alias name.

                                            Upload sprite sheet

                                            -
                                            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/spritesheet
                                            -
                                            POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/spritesheet
                                            +
                                            POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/spritesheet
                                            +
                                            POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/spritesheet
                                            • PLUGIN-UID: The plugin unique id.
                                            • PLUGIN-ALIAS: The plugin alias name.
                                            • @@ -216,7 +216,7 @@

                                              Text

                                              return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/bitmap", + url: "/rest/api/v1/display/uid/" + pluginUid + "/bitmap", isJsonResponse: true, parameter: { file: file @@ -239,7 +239,7 @@

                                              Text

                                              return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/spritesheet", + url: "/rest/api/v1/display/uid/" + pluginUid + "/spritesheet", isJsonResponse: true, parameter: { file: file @@ -261,7 +261,7 @@

                                              Text

                                              disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/" + pluginUid + "/text", + url: "/rest/api/v1/display/uid/" + pluginUid + "/text", isJsonResponse: true }).then(function(rsp) { var justTextInput = document.getElementById(justTextId); @@ -284,7 +284,7 @@

                                              Text

                                              return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/text", + url: "/rest/api/v1/display/uid/" + pluginUid + "/text", isJsonResponse: true, parameter: { text: justText, diff --git a/lib/JustTextPlugin/src/JustTextPlugin.cpp b/lib/JustTextPlugin/src/JustTextPlugin.cpp index 630ee2f6..62a8cb42 100644 --- a/lib/JustTextPlugin/src/JustTextPlugin.cpp +++ b/lib/JustTextPlugin/src/JustTextPlugin.cpp @@ -83,13 +83,14 @@ void JustTextPlugin::getTopics(JsonArray& topics) const { JsonObject jsonText = topics.createNestedObject(); - jsonText["name"] = TOPIC_TEXT; - - /* Home Assistant support of MQTT discovery */ - jsonText["ha"]["component"] = "text"; - jsonText["ha"]["commandTemplate"] = "{\"text\": \"{{ value }}\" }"; - jsonText["ha"]["valueTemplate"] = "{{ value_json.text }}"; - jsonText["ha"]["icon"] = "mdi:form-textbox"; + jsonText["name"] = TOPIC_TEXT; + + /* Home Assistant support of MQTT discovery (https://www.home-assistant.io/integrations/mqtt) */ + jsonText["ha"]["component"] = "text"; /* MQTT integration */ + jsonText["ha"]["discovery"]["name"] = "MQTT text"; /* Application that is the origin the discovered MQTT. */ + jsonText["ha"]["discovery"]["cmd_tpl"] = "{\"text\": \"{{ value }}\" }"; /* Command template */ + jsonText["ha"]["discovery"]["val_tpl"] = "{{ value_json.text }}"; /* Value template */ + jsonText["ha"]["discovery"]["ic"] = "mdi:form-textbox"; /* Icon (MaterialDesignIcons.com) */ } bool JustTextPlugin::getTopic(const String& topic, JsonObject& value) const diff --git a/lib/JustTextPlugin/web/JustTextPlugin.html b/lib/JustTextPlugin/web/JustTextPlugin.html index 377eef4d..8b00d233 100644 --- a/lib/JustTextPlugin/web/JustTextPlugin.html +++ b/lib/JustTextPlugin/web/JustTextPlugin.html @@ -38,15 +38,15 @@

                                              JustTextPlugin

                                              If MQTT is built in and enabled, it will support Home Assistant MQTT discovery.

                                              REST API

                                              Get text

                                              -
                                              GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/text
                                              -
                                              GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/text
                                              +
                                              GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/text
                                              +
                                              GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/text
                                              • PLUGIN-UID: The plugin unique id.
                                              • PLUGIN-ALIAS: The plugin alias name.

                                              Set text

                                              -
                                              POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/text?text=<TEXT>
                                              -
                                              POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/text?text=<TEXT>
                                              +
                                              POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/text?text=<TEXT>
                                              +
                                              POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/text?text=<TEXT>
                                              • PLUGIN-UID: The plugin unique id.
                                              • PLUGIN-ALIAS: The plugin alias name.
                                              • @@ -149,7 +149,7 @@

                                                Text

                                                disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/" + pluginUid + "/text", + url: "/rest/api/v1/display/uid/" + pluginUid + "/text", isJsonResponse: true }).then(function(rsp) { var justTextInput = document.getElementById(justTextId); @@ -168,7 +168,7 @@

                                                Text

                                                return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/text", + url: "/rest/api/v1/display/uid/" + pluginUid + "/text", isJsonResponse: true, parameter: { text: justText diff --git a/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp b/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp index fee3b18e..b5a1eb6b 100644 --- a/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp +++ b/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp @@ -143,7 +143,7 @@ void HomeAssistantMqtt::process(bool isConnected) m_isConnected = isConnected; } -void HomeAssistantMqtt::registerMqttDiscovery(const String& nodeId, const String& objectId, const String& stateTopic, const String& cmdTopic, const String& availabilityTopic, JsonObjectConst& extra) +void HomeAssistantMqtt::registerMqttDiscovery(const String& deviceId, const String& entityId, const String& stateTopic, const String& cmdTopic, const String& availabilityTopic, JsonObjectConst& extra) { /* The Home Assistant discovery must be enabled and the prefix must be available, otherwise this * feature is disabled. @@ -156,10 +156,7 @@ void HomeAssistantMqtt::registerMqttDiscovery(const String& nodeId, const String /* Configuration available? */ if (false == jsonHomeAssistant.isNull()) { - JsonVariantConst jsonComponent = jsonHomeAssistant["component"]; - JsonVariantConst jsonCommandTemplate = jsonHomeAssistant["commandTemplate"]; - JsonVariantConst jsonValueTemplate = jsonHomeAssistant["valueTemplate"]; - JsonVariantConst jsonIcon = jsonHomeAssistant["icon"]; + JsonVariantConst jsonComponent = jsonHomeAssistant["component"]; if (true == jsonComponent.is()) { @@ -167,29 +164,27 @@ void HomeAssistantMqtt::registerMqttDiscovery(const String& nodeId, const String if (nullptr != mqttDiscoveryInfo) { - mqttDiscoveryInfo->component = jsonComponent.as(); - mqttDiscoveryInfo->nodeId = nodeId; - mqttDiscoveryInfo->objectId = objectId; - mqttDiscoveryInfo->stateTopic = stateTopic; - mqttDiscoveryInfo->commandTopic = cmdTopic; - mqttDiscoveryInfo->availabilityTopic = availabilityTopic; - - /* Command template is optional */ - if (true == jsonCommandTemplate.is()) + mqttDiscoveryInfo->component = jsonComponent.as(); + mqttDiscoveryInfo->nodeId = deviceId; + mqttDiscoveryInfo->objectId = getObjectId(entityId); + mqttDiscoveryInfo->discoveryDetails = jsonHomeAssistant["discovery"]; + + /* Readable topic? */ + if (false == stateTopic.isEmpty()) { - mqttDiscoveryInfo->commandTemplate = jsonCommandTemplate.as(); + mqttDiscoveryInfo->discoveryDetails["stat_t"] = stateTopic; } - /* Value template is optional */ - if (true == jsonValueTemplate.is()) + /* Writeable topic? */ + if (false == cmdTopic.isEmpty()) { - mqttDiscoveryInfo->valueTemplate = jsonValueTemplate.as(); + mqttDiscoveryInfo->discoveryDetails["cmd_t"] = cmdTopic; } - /* Icon is optional */ - if (true == jsonIcon.is()) + /* Availability? */ + if (false == availabilityTopic.isEmpty()) { - mqttDiscoveryInfo->icon = jsonIcon.as(); + mqttDiscoveryInfo->discoveryDetails["avty_t"] = availabilityTopic; } m_mqttDiscoveryInfoList.push_back(mqttDiscoveryInfo); @@ -199,7 +194,7 @@ void HomeAssistantMqtt::registerMqttDiscovery(const String& nodeId, const String } } -void HomeAssistantMqtt::unregisterMqttDiscovery(const String& nodeId, const String& objectId, const String& stateTopic, const String& cmdTopic) +void HomeAssistantMqtt::unregisterMqttDiscovery(const String& deviceId, const String& entityId, const String& stateTopic, const String& cmdTopic) { /* The Home Assistant discovery must be enabled and the prefix must be available, otherwise this * feature is disabled. @@ -207,17 +202,18 @@ void HomeAssistantMqtt::unregisterMqttDiscovery(const String& nodeId, const Stri if ((true == m_haDiscoveryEnabled) && (false == m_haDiscoveryPrefix.isEmpty())) { - ListOfMqttDiscoveryInfo::iterator listOfMqttDiscoveryInfoIt = m_mqttDiscoveryInfoList.begin(); + ListOfMqttDiscoveryInfo::iterator listOfMqttDiscoveryInfoIt = m_mqttDiscoveryInfoList.begin(); + String objectId = getObjectId(entityId); while(m_mqttDiscoveryInfoList.end() != listOfMqttDiscoveryInfoIt) { MqttDiscoveryInfo* mqttDiscoveryInfo = *listOfMqttDiscoveryInfoIt; - + if ((nullptr != mqttDiscoveryInfo) && - (nodeId == mqttDiscoveryInfo->nodeId) && + (deviceId == mqttDiscoveryInfo->nodeId) && (objectId == mqttDiscoveryInfo->objectId) && - (stateTopic == mqttDiscoveryInfo->stateTopic) && - (cmdTopic == mqttDiscoveryInfo->commandTopic)) + (stateTopic == mqttDiscoveryInfo->discoveryDetails["stat_t"].as()) && + (cmdTopic == mqttDiscoveryInfo->discoveryDetails["cmd_t"].as())) { MqttService& mqttService = MqttService::getInstance(); String mqttTopic; @@ -227,11 +223,11 @@ void HomeAssistantMqtt::unregisterMqttDiscovery(const String& nodeId, const Stri /* Purge discovery info. */ if (false == mqttService.publish(mqttTopic, "")) { - LOG_WARNING("[%s] Failed to purge HA discovery info.", mqttDiscoveryInfo->objectId.c_str()); + LOG_WARNING("Failed to purge HA discovery info of %s.", mqttDiscoveryInfo->objectId.c_str()); } else { - LOG_INFO("[%s] HA discovery info purged.", mqttDiscoveryInfo->objectId.c_str()); + LOG_INFO("HA discovery info of %s purged.", mqttDiscoveryInfo->objectId.c_str()); } listOfMqttDiscoveryInfoIt = m_mqttDiscoveryInfoList.erase(listOfMqttDiscoveryInfoIt); @@ -255,6 +251,17 @@ void HomeAssistantMqtt::unregisterMqttDiscovery(const String& nodeId, const Stri * Private Methods *****************************************************************************/ +String HomeAssistantMqtt::getObjectId(const String& entityId) +{ + String objectId = entityId; + + /* Home Assistant MQTT discovery doesn't allow '/' and '.' in the object id. */ + objectId.replace('/', '_'); + objectId.replace('.', '_'); + + return objectId; +} + void HomeAssistantMqtt::clearMqttDiscoveryInfoList() { ListOfMqttDiscoveryInfo::iterator listOfMqttDiscoveryInfoIt = m_mqttDiscoveryInfoList.begin(); @@ -287,16 +294,15 @@ void HomeAssistantMqtt::getConfigTopic(String& haConfigTopic, const String& comp void HomeAssistantMqtt::publishAutoDiscoveryInfo(MqttDiscoveryInfo& mqttDiscoveryInfo) { - const size_t JSON_DOC_SIZE = 1024U; - DynamicJsonDocument jsonDoc(JSON_DOC_SIZE); - MqttService& mqttService = MqttService::getInstance(); - String mqttTopic; - String discoveryInfo; + const size_t JSON_DOC_SIZE = 1024U; + DynamicJsonDocument jsonDoc(JSON_DOC_SIZE); + MqttService& mqttService = MqttService::getInstance(); + String mqttTopic; + String discoveryInfo; + JsonObjectConstIterator discoveryDetailsIt = mqttDiscoveryInfo.discoveryDetails.as().begin(); getConfigTopic(mqttTopic, mqttDiscoveryInfo.component, mqttDiscoveryInfo.nodeId, mqttDiscoveryInfo.objectId); - /* Entity name */ - jsonDoc["name"] = "MQTT text"; /* The object id (object_id) is used to generate the entity id. */ jsonDoc["obj_id"] = mqttDiscoveryInfo.objectId; /* The unique id (unique_id) identifies the device and its entity. */ @@ -314,47 +320,23 @@ void HomeAssistantMqtt::publishAutoDiscoveryInfo(MqttDiscoveryInfo& mqttDiscover /* SW version of the device (sw_version) */ jsonDoc["dev"]["sw"] = QUOTE(SW_VERSION); - /* Entity icon available? */ - if (false == mqttDiscoveryInfo.icon.isEmpty()) - { - jsonDoc["icon"] = mqttDiscoveryInfo.icon; - } - - /* Readable topic? */ - if (false == mqttDiscoveryInfo.stateTopic.isEmpty()) + while(discoveryDetailsIt != mqttDiscoveryInfo.discoveryDetails.as().end()) { - jsonDoc["stat_t"] = mqttDiscoveryInfo.stateTopic; + jsonDoc[discoveryDetailsIt->key()] = discoveryDetailsIt->value(); - if (false == mqttDiscoveryInfo.valueTemplate.isEmpty()) - { - jsonDoc["val_tpl"] = mqttDiscoveryInfo.valueTemplate; - } + ++discoveryDetailsIt; } - /* Writeable topic? */ - if (false == mqttDiscoveryInfo.commandTopic.isEmpty()) - { - jsonDoc["cmd_t"] = mqttDiscoveryInfo.commandTopic; - - if (false == mqttDiscoveryInfo.commandTemplate.isEmpty()) - { - jsonDoc["cmd_tpl"] = mqttDiscoveryInfo.commandTemplate; - } - } - - /* Set availability topic (availability_topic) */ - jsonDoc["avty_t"] = mqttDiscoveryInfo.availabilityTopic; - /* Send the JSON as string. */ if (0U < serializeJson(jsonDoc, discoveryInfo)) { if (false == mqttService.publish(mqttTopic, discoveryInfo)) { - LOG_WARNING("[%s] Failed to provide HA discovery info.", mqttDiscoveryInfo.objectId.c_str()); + LOG_WARNING("Failed to provide HA discovery info of %s.", mqttDiscoveryInfo.objectId.c_str()); } else { - LOG_INFO("[%s] HA discovery info published.", mqttDiscoveryInfo.objectId.c_str()); + LOG_INFO("HA discovery info of %s published.", mqttDiscoveryInfo.objectId.c_str()); } } diff --git a/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.h b/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.h index 766f9546..9ffa15fb 100644 --- a/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.h +++ b/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.h @@ -107,24 +107,24 @@ class HomeAssistantMqtt * It will not publish, just prepare the MQTT discovery information * and hold it internally. * - * @param[in] nodeId ID of the node providing the topic. - * @param[in] objectId ID of the object to divide between topics. + * @param[in] deviceId Device id. + * @param[in] entityId Entity id. * @param[in] stateTopic The MQTT status topic. * @param[in] cmdTopic The MQTT command topic. * @param[in] availabilityTopic The MQTT availability topic. * @param[in] extra Extra parameters used by this extension. */ - void registerMqttDiscovery(const String& nodeId, const String& objectId, const String& stateTopic, const String& cmdTopic, const String& availabilityTopic, JsonObjectConst& extra); + void registerMqttDiscovery(const String& deviceId, const String& entityId, const String& stateTopic, const String& cmdTopic, const String& availabilityTopic, JsonObjectConst& extra); /** * Unregister Home Assistant MQTT discovery. * - * @param[in] nodeId ID of the node providing the topic. - * @param[in] objectId ID of the object to divide between topics. + * @param[in] deviceId Device id. + * @param[in] entityId Entity id. * @param[in] stateTopic The MQTT status topic. * @param[in] cmdTopic The MQTT command topic. */ - void unregisterMqttDiscovery(const String& nodeId, const String& objectId, const String& stateTopic, const String& cmdTopic); + void unregisterMqttDiscovery(const String& deviceId, const String& entityId, const String& stateTopic, const String& cmdTopic); private: @@ -155,28 +155,18 @@ class HomeAssistantMqtt /** Information necessary for Home Assistant MQTT discovery. */ struct MqttDiscoveryInfo { - String component; /**< Home Assistant component */ - String nodeId; /**< Home Assistant node id */ - String objectId; /**< Home Assistant object id */ - String stateTopic; /**< Status topic */ - String valueTemplate; /**< Value template to extract the text state value */ - String commandTopic; /**< Command topic */ - String commandTemplate; /**< Command template to generate payload to send to command topic */ - String availabilityTopic; /**< Availability topic */ - String icon; /**< Icon name from MaterialDesignIcons.com */ - bool isReqToPublish; /**< Is requested to publish this discovery info? */ + String component; /**< Home Assistant component */ + String nodeId; /**< Home Assistant node id */ + String objectId; /**< Home Assistant object id */ + DynamicJsonDocument discoveryDetails; /**< Additional discovery information. */ + bool isReqToPublish; /**< Is requested to publish this discovery info? */ /** Construct Home Assistant MQTT discovery information. */ MqttDiscoveryInfo() : component(), nodeId(), objectId(), - stateTopic(), - valueTemplate(), - commandTopic(), - commandTemplate(), - availabilityTopic(), - icon(), + discoveryDetails(368U), isReqToPublish(true) { } @@ -195,6 +185,15 @@ class HomeAssistantMqtt HomeAssistantMqtt(const HomeAssistantMqtt& ext); HomeAssistantMqtt& operator=(const HomeAssistantMqtt& ext); + /** + * Get the object id from the entity id. + * + * @param[in] entityId The entity id. + * + * @return Object id + */ + String getObjectId(const String& entityId); + /** * Clear MQTT discovery info list. */ diff --git a/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp b/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp index dc2e7714..bb9a0f28 100644 --- a/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp +++ b/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp @@ -88,7 +88,7 @@ void MqttApiTopicHandler::registerTopic(const String& deviceId, const String& en String mqttTopicNameBase = deviceId + "/" + entityId + topic; TopicState* topicState = new(std::nothrow) TopicState(); - LOG_INFO("[%s] Register: %s", entityId.c_str(), mqttTopicNameBase.c_str()); + LOG_INFO("Register: %s", mqttTopicNameBase.c_str()); if (nullptr != topicState) { @@ -129,11 +129,11 @@ void MqttApiTopicHandler::registerTopic(const String& deviceId, const String& en if (false == mqttService.subscribe(topicUriWriteable, setCallback)) { - LOG_WARNING("[%s] Couldn't subscribe %s.", entityId.c_str(), topicUriWriteable.c_str()); + LOG_WARNING("Couldn't subscribe %s.", topicUriWriteable.c_str()); } else { - LOG_INFO("[%s] Subscribed: %s", entityId.c_str(), topicUriWriteable.c_str()); + LOG_INFO("Subscribed: %s", topicUriWriteable.c_str()); } } @@ -159,7 +159,7 @@ void MqttApiTopicHandler::unregisterTopic(const String& deviceId, const String& MqttService& mqttService = MqttService::getInstance(); ListOfTopicStates::iterator topicStateIt = m_listOfTopicStates.begin(); - LOG_INFO("[%s] Unregister: %s", entityId.c_str(), mqttTopicNameBase.c_str()); + LOG_INFO("Unregister: %s", mqttTopicNameBase.c_str()); while(m_listOfTopicStates.end() != topicStateIt) { @@ -180,11 +180,11 @@ void MqttApiTopicHandler::unregisterTopic(const String& deviceId, const String& /* Purge topic */ if (false == mqttService.publish(topicUriReadable, "")) { - LOG_WARNING("[%s] Failed to purge: %s", entityId.c_str(), topicUriReadable.c_str()); + LOG_WARNING("Failed to purge: %s", topicUriReadable.c_str()); } else { - LOG_INFO("[%s] Purged: %s", entityId.c_str(), topicUriReadable.c_str()); + LOG_INFO("Purged: %s", topicUriReadable.c_str()); } } @@ -192,9 +192,9 @@ void MqttApiTopicHandler::unregisterTopic(const String& deviceId, const String& { topicUriWriteable = mqttTopicNameBase + MQTT_ENDPOINT_WRITE_ACCESS; - mqttService.unsubscribe(topicUriWriteable); + LOG_INFO("Unsubscribe: %s", topicUriWriteable.c_str()); - LOG_INFO("[%s] Unsubscribed: %s", entityId.c_str(), topicUriWriteable.c_str()); + mqttService.unsubscribe(topicUriWriteable); } /* Handle Home Assistant extension */ @@ -326,13 +326,10 @@ void MqttApiTopicHandler::write(const String& deviceId, const String& entityId, String dstFullPath; /* Ask plugin, whether the upload is allowed or not. */ - if (nullptr == uploadReqFunc) - { - LOG_WARNING("[%s] Upload not supported.", entityId.c_str()); - } - else if (false == uploadReqFunc(topic, jsonFileName.as(), dstFullPath)) + if ((nullptr == uploadReqFunc) || + (false == uploadReqFunc(topic, jsonFileName.as(), dstFullPath))) { - LOG_WARNING("[%s] Upload not supported.", entityId.c_str()); + LOG_WARNING("Upload not supported by %s.", entityId.c_str()); } else { @@ -342,12 +339,12 @@ void MqttApiTopicHandler::write(const String& deviceId, const String& entityId, if (MBEDTLS_ERR_BASE64_INVALID_CHARACTER == decodeRet) { - LOG_WARNING("[%s] File encoding contains invalid character.", entityId.c_str(), fileSize); + LOG_WARNING("File encoding contains invalid character."); } else if ((MAX_FILE_SIZE < fileSize) || (0U == fileSize)) { - LOG_WARNING("[%s] File size %u not supported.", entityId.c_str(), fileSize); + LOG_WARNING("File size %u not supported.", fileSize); } else { @@ -361,7 +358,7 @@ void MqttApiTopicHandler::write(const String& deviceId, const String& entityId, if (0U != decodeRet) { - LOG_WARNING("[%s] File decode error: %d", entityId.c_str(), decodeRet); + LOG_WARNING("File decode error: %d", decodeRet); } else { @@ -370,7 +367,7 @@ void MqttApiTopicHandler::write(const String& deviceId, const String& entityId, if (false == fd) { - LOG_ERROR("[%s] Couldn't create file: %s", entityId.c_str(), dstFullPath.c_str()); + LOG_ERROR("Couldn't create file: %s", dstFullPath.c_str()); } else { @@ -393,7 +390,7 @@ void MqttApiTopicHandler::write(const String& deviceId, const String& entityId, jsonValue = jsonDoc.as(); if (false == setTopicFunc(topic, jsonValue)) { - LOG_WARNING("[%s] Payload rejected.", entityId.c_str()); + LOG_WARNING("Payload rejected by %s.", entityId.c_str()); } } } @@ -418,11 +415,11 @@ void MqttApiTopicHandler::publish(const String& deviceId, const String& entityId if (false == mqttService.publish(topicStateUri, topicContent)) { - LOG_WARNING("[%s] Couldn't publish %s.", entityId.c_str(), topicStateUri.c_str()); + LOG_WARNING("Couldn't publish %s.", topicStateUri.c_str()); } else { - LOG_INFO("[%s] Published: %s", entityId.c_str(), topicStateUri.c_str()); + LOG_INFO("Published: %s", topicStateUri.c_str()); } } } diff --git a/lib/OpenWeatherPlugin/web/OpenWeatherPlugin.html b/lib/OpenWeatherPlugin/web/OpenWeatherPlugin.html index e675a355..7cf62a51 100644 --- a/lib/OpenWeatherPlugin/web/OpenWeatherPlugin.html +++ b/lib/OpenWeatherPlugin/web/OpenWeatherPlugin.html @@ -37,15 +37,15 @@

                                                OpenWeatherPlugin

                                                The plugin shows the current weather condition (icon and temperature) and one aditional information (see configuration below) provided by https://openweathermap.org/.

                                                REST API

                                                Get the OpenWeather related configuration .

                                                -
                                                GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/weather
                                                -
                                                GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/weather
                                                +
                                                GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/weather
                                                +
                                                GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/weather
                                                • PLUGIN-UID: The plugin unique id.
                                                • PLUGIN-ALIAS: The plugin alias name.

                                                Set OpenWeather related configuration.

                                                -
                                                POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/weather?apiKey=<API-KEY>&latitude=<LATITUDE>&longitude=<LONGITUDE>&other=<OTHER>&units=<UNITS>
                                                -
                                                POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/weather?apiKey=<API-KEY>&latitude=<LATITUDE>&longitude=<LONGITUDE>&other=<OTHER>&units=<UNITS>
                                                +
                                                POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/weather?apiKey=<API-KEY>&latitude=<LATITUDE>&longitude=<LONGITUDE>&other=<OTHER>&units=<UNITS>
                                                +
                                                POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/weather?apiKey=<API-KEY>&latitude=<LATITUDE>&longitude=<LONGITUDE>&other=<OTHER>&units=<UNITS>
                                                • PLUGIN-UID: The plugin unique id.
                                                • PLUGIN-ALIAS: The plugin alias name.
                                                • @@ -171,7 +171,7 @@

                                                  Configuration

                                                  disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/" + pluginUid + "/weather", + url: "/rest/api/v1/display/uid/" + pluginUid + "/weather", isJsonResponse: true }).then(function(rsp) { $("#apiKey").val(rsp.data.apiKey); @@ -191,7 +191,7 @@

                                                  Configuration

                                                  return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/weather", + url: "/rest/api/v1/display/uid/" + pluginUid + "/weather", isJsonResponse: true, parameter: { apiKey: $("#apiKey").val(), diff --git a/lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp b/lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp index 4b212953..b25c1135 100644 --- a/lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp +++ b/lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp @@ -99,9 +99,9 @@ void RestApiTopicHandler::registerTopic(const String& deviceId, const String& en UTIL_NOT_USED(extra); - m_listOfTopicMetaData.push_back(topicMetaData); + LOG_INFO("Register: %s", topicMetaData->uri.c_str()); - LOG_INFO("[%s] Registered: %s", entityId.c_str(), topicMetaData->uri.c_str()); + m_listOfTopicMetaData.push_back(topicMetaData); } } } @@ -125,11 +125,11 @@ void RestApiTopicHandler::unregisterTopic(const String& deviceId, const String& { if (false == MyWebServer::getInstance().removeHandler(topicMetaData->webHandler)) { - LOG_WARNING("[%s] Failed to unregister: %s", topicMetaData->entityId.c_str(), topicMetaData->uri.c_str()); + LOG_WARNING("Failed to unregister: %s", topicMetaData->uri.c_str()); } else { - LOG_INFO("[%s] Unregistered: %s", topicMetaData->entityId.c_str(), topicMetaData->uri.c_str()); + LOG_INFO("Unregister: %s", topicMetaData->uri.c_str()); } topicMetaDataIt = m_listOfTopicMetaData.erase(topicMetaDataIt); @@ -156,7 +156,7 @@ void RestApiTopicHandler::unregisterTopic(const String& deviceId, const String& String RestApiTopicHandler::getBaseUri(const String& entityId) { String baseUri = RestApi::BASE_URI; - baseUri += "/display/"; + baseUri += "/"; baseUri += entityId; return baseUri; @@ -182,7 +182,7 @@ void RestApiTopicHandler::webReqHandler(AsyncWebServerRequest *request, TopicMet /* Topic data will be transported in the HTTP body as JSON. */ if (false == topicMetaData->getTopicFunc(topicMetaData->topic, dataObj)) { - LOG_WARNING("[%s] Topic \"%s\" not supported.", topicMetaData->entityId, topicMetaData->topic); + LOG_WARNING("Topic \"%s\" not supported by %s.", topicMetaData->topic.c_str(), topicMetaData->entityId.c_str()); RestUtil::prepareRspError(jsonDoc, "Requested topic not supported."); @@ -215,7 +215,7 @@ void RestApiTopicHandler::webReqHandler(AsyncWebServerRequest *request, TopicMet jsonValue = jsonDocPar.as(); /* Assign after par2Json conversion! Otherwise there will be a empty object. */ if (false == topicMetaData->setTopicFunc(topicMetaData->topic, jsonValue)) { - LOG_WARNING("[%s] Topic \"%s\" not supported or invalid data.", topicMetaData->entityId, topicMetaData->topic); + LOG_WARNING("Topic \"%s\" not supported by %s or invalid data.", topicMetaData->topic.c_str(), topicMetaData->entityId.c_str()); RestUtil::prepareRspError(jsonDoc, "Requested topic not supported or invalid data."); @@ -283,7 +283,7 @@ void RestApiTopicHandler::uploadHandler(AsyncWebServerRequest *request, const St if ((nullptr == topicMetaData->uploadReqFunc) || (false == topicMetaData->uploadReqFunc(topicMetaData->topic, filename, topicMetaData->fullPath))) { - LOG_WARNING("[%s] Upload not supported.", topicMetaData->entityId.c_str()); + LOG_WARNING("Upload not supported by %s.", topicMetaData->entityId.c_str()); topicMetaData->isUploadError = true; topicMetaData->fullPath.clear(); } diff --git a/lib/SensorPlugin/web/SensorPlugin.html b/lib/SensorPlugin/web/SensorPlugin.html index 01852bce..36767c27 100644 --- a/lib/SensorPlugin/web/SensorPlugin.html +++ b/lib/SensorPlugin/web/SensorPlugin.html @@ -39,9 +39,9 @@

                                                  REST API

                                                  Get all installed sensor drivers and their provided channels.

                                                  GET {{ORIGIN}}/rest/api/v1/sensors

                                                  Get current selected sensor and channel.

                                                  -
                                                  GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/channel
                                                  +
                                                  GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/channel

                                                  Set sensor and channel, which values to show.

                                                  -
                                                  POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/channel?sensorIndex=<SENSOR-IDX>&channelIndex=<CHANNEL-IDX>
                                                  +
                                                  POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/channel?sensorIndex=<SENSOR-IDX>&channelIndex=<CHANNEL-IDX>
                                                  • SENSOR-IDX: The sensor index.
                                                  • CHANNEL-IDX: The channel index of the sensor.
                                                  • @@ -176,7 +176,7 @@

                                                    Sensor and Channel

                                                    disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/" + pluginUid + "/channel", + url: "/rest/api/v1/display/uid/" + pluginUid + "/channel", isJsonResponse: true }).then(function(rsp) { @@ -215,7 +215,7 @@

                                                    Sensor and Channel

                                                    return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/channel", + url: "/rest/api/v1/display/uid/" + pluginUid + "/channel", isJsonResponse: true, parameter: { sensorIndex: $("#sensorList").val(), diff --git a/lib/SignalDetectorPlugin/web/SignalDetectorPlugin.html b/lib/SignalDetectorPlugin/web/SignalDetectorPlugin.html index 76776147..6bc981a8 100644 --- a/lib/SignalDetectorPlugin/web/SignalDetectorPlugin.html +++ b/lib/SignalDetectorPlugin/web/SignalDetectorPlugin.html @@ -41,15 +41,15 @@

                                                    SignalDetectorPlugin

                                                    Additional a push notification can be configured. By default a GET is triggered. Using "GET" or "POST" as prefix its configureable. Example: "POST http://..."

                                                    REST API

                                                    Get configuration

                                                    -
                                                    GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/signalDetector
                                                    -
                                                    GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/signalDetector
                                                    +
                                                    GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/signalDetector
                                                    +
                                                    GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/signalDetector
                                                    • PLUGIN-UID: The plugin unique id.
                                                    • PLUGIN-ALIAS: The plugin alias name.

                                                    Set configuration

                                                    -
                                                    POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/signalDetector?text=<TEXT>&pushUrl=<PUSH-URL>&frequency=<FREQUENCY>&minDuration=<MIN-DURATON>&threshold=<THRESHOLD>
                                                    -
                                                    POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/signalDetector?text=<TEXT>&pushUrl=<PUSH-URL>&frequency=<FREQUENCY>&minDuration=<MIN-DURATON>&threshold=<THRESHOLD>
                                                    +
                                                    POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/signalDetector?text=<TEXT>&pushUrl=<PUSH-URL>&frequency=<FREQUENCY>&minDuration=<MIN-DURATON>&threshold=<THRESHOLD>
                                                    +
                                                    POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/signalDetector?text=<TEXT>&pushUrl=<PUSH-URL>&frequency=<FREQUENCY>&minDuration=<MIN-DURATON>&threshold=<THRESHOLD>
                                                    • PLUGIN-UID: The plugin unique id.
                                                    • PLUGIN-ALIAS: The plugin alias name.
                                                    • @@ -190,7 +190,7 @@

                                                      Configuration

                                                      disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/" + pluginUid + "/signalDetector", + url: "/rest/api/v1/display/uid/" + pluginUid + "/signalDetector", isJsonResponse: true }).then(function(rsp) { var index = 0; @@ -215,7 +215,7 @@

                                                      Configuration

                                                      disableUI(); return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/signalDetector", + url: "/rest/api/v1/display/uid/" + pluginUid + "/signalDetector", isJsonResponse: true, parameter: { text: $("#text").val(), diff --git a/lib/SoundReactivePlugin/web/SoundReactivePlugin.html b/lib/SoundReactivePlugin/web/SoundReactivePlugin.html index 3267e48a..3992bbed 100644 --- a/lib/SoundReactivePlugin/web/SoundReactivePlugin.html +++ b/lib/SoundReactivePlugin/web/SoundReactivePlugin.html @@ -38,11 +38,11 @@

                                                      SoundReactivePlugin

                                                      Required: A digital microphone (INMP441) is required, connected to the I2S port.

                                                      REST API

                                                      Get configuration about how many frequency bands are shown.

                                                      -
                                                      GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/config
                                                      -
                                                      GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/config
                                                      +
                                                      GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/config
                                                      +
                                                      GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/config

                                                      Set configuration about how many frequency bands shall be shown.

                                                      -
                                                      POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/config?freqBandLen=<FREQ-BAND-LEN>
                                                      -
                                                      POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/config?freqBandLen=<FREQ-BAND-LEN>
                                                      +
                                                      POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/config?freqBandLen=<FREQ-BAND-LEN>
                                                      +
                                                      POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/config?freqBandLen=<FREQ-BAND-LEN>
                                                      • PLUGIN-UID: The plugin unique id.
                                                      • PLUGIN-ALIAS: The plugin alias name.
                                                      • @@ -147,7 +147,7 @@

                                                        Number of frequency bands

                                                        disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/" + pluginUid + "/config", + url: "/rest/api/v1/display/uid/" + pluginUid + "/config", isJsonResponse: true }).then(function(rsp) { $("#freqBandLen").val(rsp.data.freqBandLen); @@ -162,7 +162,7 @@

                                                        Number of frequency bands

                                                        disableUI(); return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/config", + url: "/rest/api/v1/display/uid/" + pluginUid + "/config", isJsonResponse: true, parameter: { freqBandLen: $("#freqBandLen").val() diff --git a/lib/SunrisePlugin/web/SunrisePlugin.html b/lib/SunrisePlugin/web/SunrisePlugin.html index a7ef65a5..813d0199 100644 --- a/lib/SunrisePlugin/web/SunrisePlugin.html +++ b/lib/SunrisePlugin/web/SunrisePlugin.html @@ -38,15 +38,15 @@

                                                        SunrisePlugin

                                                        Configure the time format in the plugin configuration JSON file. The format itself is according to strftime(). For colorization text properties can be added.

                                                        REST API

                                                        Get location

                                                        -
                                                        GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/location
                                                        -
                                                        GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/location
                                                        +
                                                        GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/location
                                                        +
                                                        GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/location
                                                        • PLUGIN-UID: The plugin unique id.
                                                        • PLUGIN-ALIAS: The plugin alias name.

                                                        Set location

                                                        -
                                                        POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/location?longitude=<LONGITUDE>&latitude=<LATITUDE>
                                                        -
                                                        POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/location?longitude=<LONGITUDE>&latitude=<LATITUDE>
                                                        +
                                                        POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/location?longitude=<LONGITUDE>&latitude=<LATITUDE>
                                                        +
                                                        POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/location?longitude=<LONGITUDE>&latitude=<LATITUDE>
                                                        • PLUGIN-UID: The plugin unique id.
                                                        • PLUGIN-ALIAS: The plugin alias name.
                                                        • @@ -149,7 +149,7 @@

                                                          Location

                                                          disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/" + pluginUid + "/location", + url: "/rest/api/v1/display/uid/" + pluginUid + "/location", isJsonResponse: true }).then(function(rsp) { $("#longitude").val(rsp.data.longitude); @@ -165,7 +165,7 @@

                                                          Location

                                                          disableUI(); return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/location", + url: "/rest/api/v1/display/uid/" + pluginUid + "/location", isJsonResponse: true, parameter: { latitude: $("#latitude").val(), diff --git a/lib/ThreeIconPlugin/web/ThreeIconPlugin.html b/lib/ThreeIconPlugin/web/ThreeIconPlugin.html index 2d3acd0e..4c7044ca 100644 --- a/lib/ThreeIconPlugin/web/ThreeIconPlugin.html +++ b/lib/ThreeIconPlugin/web/ThreeIconPlugin.html @@ -50,16 +50,16 @@

                                                          ThreeIconPlugin

                                                        REST API

                                                        Set icon

                                                        -
                                                        POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/bitmap/<ICON-ID>
                                                        -
                                                        POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/bitmap/<ICON-ID>
                                                        +
                                                        POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/bitmap/<ICON-ID>
                                                        +
                                                        POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/bitmap/<ICON-ID>
                                                        • PLUGIN-UID: The plugin unique id.
                                                        • PLUGIN-ALIAS: The plugin alias name.
                                                        • ICON-ID: The id of the icon (starting with 0 from left).

                                                        Clear icon

                                                        -
                                                        POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/bitmap/<ICON-ID>?clear=<STATE>
                                                        -
                                                        POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/bitmap/<ICON-ID>?clear=<STATE>
                                                        +
                                                        POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/bitmap/<ICON-ID>?clear=<STATE>
                                                        +
                                                        POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/bitmap/<ICON-ID>?clear=<STATE>
                                                        • PLUGIN-UID: The plugin unique id.
                                                        • PLUGIN-ALIAS: The plugin alias name.
                                                        • @@ -67,10 +67,10 @@

                                                          Clear icon

                                                        • STATE: true or false

                                                        Control animation (Sprite Sheet required)

                                                        -
                                                        POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/animation/<ICON-ID>?forward=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                                                        -
                                                        POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/animation/<ICON-ID>?forward=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                                                        -
                                                        POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/animation/<ICON-ID>?repeat=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                                                        -
                                                        POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/animation/<ICON-ID>?repeat=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                                                        +
                                                        POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/animation/<ICON-ID>?forward=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                                                        +
                                                        POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/animation/<ICON-ID>?forward=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                                                        +
                                                        POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/animation/<ICON-ID>?repeat=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                                                        +
                                                        POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/animation/<ICON-ID>?repeat=<STATE>&ICONFULLPATH=<ICON-FULL-PATH>&SPRITESHEETFULLPATH=<SPRITESHEET-FULL-PATH>
                                                        • PLUGIN-UID: The plugin unique id.
                                                        • PLUGIN-ALIAS: The plugin alias name.
                                                        • @@ -304,7 +304,7 @@

                                                          Animation configuration

                                                          return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/bitmap/" + index, + url: "/rest/api/v1/display/uid/" + pluginUid + "/bitmap/" + index, isJsonResponse: true, parameter: { file: file @@ -327,7 +327,7 @@

                                                          Animation configuration

                                                          return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/spritesheet/" + index, + url: "/rest/api/v1/display/uid/" + pluginUid + "/spritesheet/" + index, isJsonResponse: true, parameter: { file: file @@ -368,7 +368,7 @@

                                                          Animation configuration

                                                          return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/animation/" + index, + url: "/rest/api/v1/display/uid/" + pluginUid + "/animation/" + index, isJsonResponse: true, parameter: { iconFullPath: "", @@ -406,7 +406,7 @@

                                                          Animation configuration

                                                          disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/" + pluginUid + "/animation/" + index, + url: "/rest/api/v1/display/uid/" + pluginUid + "/animation/" + index, isJsonResponse: true }).then(function(rsp) { var forwardElement = document.getElementById("forwardAnimation"); @@ -471,7 +471,7 @@

                                                          Animation configuration

                                                          return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/animation/" + index, + url: "/rest/api/v1/display/uid/" + pluginUid + "/animation/" + index, isJsonResponse: true, parameter: { forward: isForward, diff --git a/lib/TopicHandlerService/src/TopicHandlerService.cpp b/lib/TopicHandlerService/src/TopicHandlerService.cpp index ee050b03..d3d4227d 100644 --- a/lib/TopicHandlerService/src/TopicHandlerService.cpp +++ b/lib/TopicHandlerService/src/TopicHandlerService.cpp @@ -122,7 +122,6 @@ void TopicHandlerService::registerTopics(const String& deviceId, IPluginMaintena for (JsonVariantConst jsonTopic : jsonTopics) { String topicName; - String entityId; JsonObjectConst extra; String topicAccess = DEFAULT_ACCESS; ITopicHandler::GetTopicFunc getTopicFunc = nullptr; @@ -163,14 +162,12 @@ void TopicHandlerService::registerTopics(const String& deviceId, IPluginMaintena strToAccess(plugin, topicAccess, getTopicFunc, setTopicFunc, uploadReqFunc); /* Register plugin topic with plugin UID as entity id. */ - entityId = plugin->getUID(); - registerTopic(deviceId, entityId, topicName, extra, getTopicFunc, nullptr, setTopicFunc, uploadReqFunc); + registerTopic(deviceId, getEntityIdByPluginUid(plugin->getUID()), topicName, extra, getTopicFunc, nullptr, setTopicFunc, uploadReqFunc); /* Register plugin topic with plugin alias as entity id (if possible). */ - entityId = plugin->getAlias(); - if (false == entityId.isEmpty()) + if (false == plugin->getAlias().isEmpty()) { - registerTopic(deviceId, entityId, topicName, extra, getTopicFunc, nullptr, setTopicFunc, uploadReqFunc); + registerTopic(deviceId, getEntityIdByPluginAlias(plugin->getAlias()), topicName, extra, getTopicFunc, nullptr, setTopicFunc, uploadReqFunc); } addToPluginMetaDataList(deviceId, plugin, topicName); @@ -198,7 +195,6 @@ void TopicHandlerService::unregisterTopics(const String& deviceId, IPluginMainte for (JsonVariantConst jsonTopic : jsonTopics) { String topicName; - String entityId; /* Topic specific parameter available? */ if (true == jsonTopic.is()) @@ -224,14 +220,12 @@ void TopicHandlerService::unregisterTopics(const String& deviceId, IPluginMainte if (false == topicName.isEmpty()) { /* Unregister plugin topic with plugin UID as entity id. */ - entityId = plugin->getUID(); - unregisterTopic(deviceId, entityId, topicName); + unregisterTopic(deviceId, getEntityIdByPluginUid(plugin->getUID()), topicName); /* Unregister plugin topic with plugin UID as entity id (if possible). */ - entityId = plugin->getAlias(); - if (false == entityId.isEmpty()) + if (false == plugin->getAlias().isEmpty()) { - unregisterTopic(deviceId, entityId, topicName); + unregisterTopic(deviceId, getEntityIdByPluginAlias(plugin->getAlias()), topicName); } removeFromPluginMetaDataList(deviceId, plugin); @@ -330,6 +324,16 @@ void TopicHandlerService::unregisterTopic(const String& deviceId, const String& * Private Methods *****************************************************************************/ +String TopicHandlerService::getEntityIdByPluginUid(uint16_t uid) +{ + return String("display/uid/") + uid; +} + +String TopicHandlerService::getEntityIdByPluginAlias(const String& alias) +{ + return String("display/alias/") + alias; +} + void TopicHandlerService::strToAccess(IPluginMaintenance* plugin, const String& strAccess, ITopicHandler::GetTopicFunc& getTopicFunc, ITopicHandler::SetTopicFunc& setTopicFunc, ITopicHandler::UploadReqFunc& uploadReqFunc) const { if (nullptr != plugin) diff --git a/lib/TopicHandlerService/src/TopicHandlerService.h b/lib/TopicHandlerService/src/TopicHandlerService.h index 24500271..528b2928 100644 --- a/lib/TopicHandlerService/src/TopicHandlerService.h +++ b/lib/TopicHandlerService/src/TopicHandlerService.h @@ -221,6 +221,24 @@ class TopicHandlerService : public IService TopicHandlerService(const TopicHandlerService& service); TopicHandlerService& operator=(const TopicHandlerService& service); + /** + * Get the entity id by plugin UID. + * + * @param[in] uid Plugin UID + * + * @return Entity id + */ + String getEntityIdByPluginUid(uint16_t uid); + + /** + * Get the entity id by plugin alias. + * + * @param[in] alias Plugin alias + * + * @return Entity id + */ + String getEntityIdByPluginAlias(const String& alias); + /** * Generates the access functions depended on the plugin accessibility. * diff --git a/lib/VolumioPlugin/web/VolumioPlugin.html b/lib/VolumioPlugin/web/VolumioPlugin.html index a57a3f4d..d8c6c73d 100644 --- a/lib/VolumioPlugin/web/VolumioPlugin.html +++ b/lib/VolumioPlugin/web/VolumioPlugin.html @@ -38,11 +38,11 @@

                                                          VolumioPlugin

                                                          If the VOLUMIO server is offline, the plugin gets automatically disabled, otherwise enabled.

                                                          REST API

                                                          Get VOLUMIO host address

                                                          -
                                                          GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/host
                                                          -
                                                          GET {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/host
                                                          +
                                                          GET {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/host
                                                          +
                                                          GET {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/host

                                                          Set VOLUMIO host address

                                                          -
                                                          POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-UID>/host?host=<HOST-ADDRESS>
                                                          -
                                                          POST {{ORIGIN}}/rest/api/v1/display/<PLUGIN-ALIAS>/host?host=<HOST-ADDRESS>
                                                          +
                                                          POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/host?host=<HOST-ADDRESS>
                                                          +
                                                          POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/host?host=<HOST-ADDRESS>
                                                          • PLUGIN-UID: The plugin unique id.
                                                          • PLUGIN-ALIAS: The plugin alias name.
                                                          • @@ -140,7 +140,7 @@

                                                            Host Address

                                                            disableUI(); return utils.makeRequest({ method: "GET", - url: "/rest/api/v1/display/" + pluginUid + "/host", + url: "/rest/api/v1/display/uid/" + pluginUid + "/host", isJsonResponse: true }).then(function(rsp) { $("#host").val(rsp.data.host); @@ -155,7 +155,7 @@

                                                            Host Address

                                                            disableUI(); return utils.makeRequest({ method: "POST", - url: "/rest/api/v1/display/" + pluginUid + "/host", + url: "/rest/api/v1/display/uid/" + pluginUid + "/host", isJsonResponse: true, parameter: { host: $("#host").val() diff --git a/src/Hal/SensorDataProvider.cpp b/src/Hal/SensorDataProvider.cpp index 261f53e3..ada9971e 100644 --- a/src/Hal/SensorDataProvider.cpp +++ b/src/Hal/SensorDataProvider.cpp @@ -38,6 +38,9 @@ #include #include #include +#include +#include +#include /****************************************************************************** * Compiler Switches @@ -47,10 +50,29 @@ * Macros *****************************************************************************/ +#define SENSOR_TOPICS_COUNT (4U) + /****************************************************************************** * Types and classes *****************************************************************************/ +/** This type defines the required data to publish sensor values as topics. */ +typedef struct +{ + ISensorChannel::Type sensorChannelType; /**< Sensor channel type. */ + const char* extra; /**< Extra data as JSON string, e.g. for homeassistant extension. */ + uint32_t updatePeriod; /**< Max. sensor data update period in ms regarding publishing. */ + +} SensorTopic; + +/** This type defines the runtime data for a sensor topic, required for publishing. */ +typedef struct +{ + String lastValue; /**< Last published sensor value. */ + uint32_t lastTimestamp; /**< Last timestamp of publishing, used to limit the update period. */ + +} SensorTopicRunData; + /****************************************************************************** * Prototypes *****************************************************************************/ @@ -62,12 +84,103 @@ /* Initialize file name where to find the sensor calibration values. */ const char* SensorDataProvider::SENSOR_CALIB_FILE_NAME = "/configuration/sensors.json"; +/** The provided sensor topics. */ +static const SensorTopic gSensorTopics[SENSOR_TOPICS_COUNT] = +{ + { + ISensorChannel::TYPE_TEMPERATURE_DEGREE_CELSIUS, + "{" \ + "\"ha\": {" \ + "\"component\": \"sensor\"," \ + "\"discovery\": {" \ + "\"name\": \"Temperature\"," \ + "\"unit_of_meas\": \"°C\"," \ + "\"ic\": \"mdi:thermometer\"," \ + "\"dev_cla\": \"temperature\"," \ + "\"val_tpl\": \"{{ value_json.value }}\"" \ + "}" \ + "}" \ + "}", + 30000U + }, + { + ISensorChannel::TYPE_HUMIDITY_PERCENT, + "{" \ + "\"ha\": {" \ + "\"component\": \"sensor\"," \ + "\"discovery\": {" \ + "\"name\": \"Humidity\"," \ + "\"unit_of_meas\": \"%\"," \ + "\"ic\": \"mdi:water-percent\"," \ + "\"dev_cla\": \"humidity\"," \ + "\"val_tpl\": \"{{ value_json.value }}\"" \ + "}" \ + "}" \ + "}", + 30000U + }, + { + ISensorChannel::TYPE_ILLUMINANCE_LUX, + "{" \ + "\"ha\": {" \ + "\"component\": \"sensor\"," \ + "\"discovery\": {" \ + "\"name\": \"Illuminance\"," \ + "\"unit_of_meas\": \"lx\"," \ + "\"ic\": \"mdi:sun-wireless\"," \ + "\"dev_cla\": \"illuminance\"," \ + "\"val_tpl\": \"{{ value_json.value }}\"" \ + "}" \ + "}" \ + "}", + 10000U + }, + { + ISensorChannel::TYPE_STATE_OF_CHARGE_PERCENT, + "{" \ + "\"ha\": {" \ + "\"component\": \"sensor\"," \ + "\"discovery\": {" \ + "\"name\": \"Battery\"," \ + "\"unit_of_meas\": \"%\"," \ + "\"ic\": \"mdi:battery-90\"," \ + "\"dev_cla\": \"battery\"," \ + "\"val_tpl\": \"{{ value_json.value }}\"" \ + "}" \ + "}" \ + "}", + 10000U + } +}; + +/** The runtime sensor topic data. */ +static SensorTopicRunData gSensorLastValue[SENSOR_TOPICS_COUNT] = +{ + { String(), 0U }, + { String(), 0U }, + { String(), 0U }, + { String(), 0U } +}; + /****************************************************************************** * Public Methods *****************************************************************************/ void SensorDataProvider::begin() { + SettingsService& settings = SettingsService::getInstance(); + + if (false == settings.open(true)) + { + m_deviceId = settings.getHostname().getDefault(); + } + else + { + m_deviceId = settings.getHostname().getValue(); + + settings.close(); + } + /* Initialize all sensor drivers. */ m_impl->begin(); @@ -78,6 +191,12 @@ void SensorDataProvider::begin() } logSensorAvailability(); + registerSensorTopics(); +} + +void SensorDataProvider::end() +{ + unregisterSensorTopics(); } uint8_t SensorDataProvider::getNumSensors() const @@ -259,7 +378,8 @@ bool SensorDataProvider::save() *****************************************************************************/ SensorDataProvider::SensorDataProvider() : - m_impl(Sensors::getSensorDataProviderImpl()) + m_impl(Sensors::getSensorDataProviderImpl()), + m_deviceId() { } @@ -447,6 +567,99 @@ void SensorDataProvider::createCalibrationFile() (void)save(); } +void SensorDataProvider::registerSensorTopics() +{ + uint8_t index = 0U; + TopicHandlerService& topicHandlerService = TopicHandlerService::getInstance(); + + for(index = 0U; index < UTIL_ARRAY_NUM(gSensorTopics); ++index) + { + const SensorTopic* sensorTopic = &gSensorTopics[index]; + SensorTopicRunData* sensorTopicRunData = &gSensorLastValue[index]; + DynamicJsonDocument jsonDoc(512U); + JsonObjectConst extra; + uint8_t sensorIndex = 0U; + uint8_t channelIndex = 0U; + + if (DeserializationError::Ok != deserializeJson(jsonDoc, sensorTopic->extra)) + { + LOG_ERROR("Sensor/Channel %u discovery details error.", index); + } + + extra = jsonDoc.as(); + + /* Try to find a sensor channel which provides the required information. */ + if (true == find(sensorIndex, channelIndex, sensorTopic->sensorChannelType)) + { + ISensor* sensor = this->getSensor(sensorIndex); + ISensorChannel* sensorChannel = sensor->getChannel(channelIndex); + String channelName = "/" + ISensorChannel::channelTypeToName(sensorTopic->sensorChannelType); + String entityId = "sensors/"; + + entityId += index; + + ITopicHandler::GetTopicFunc getTopicFunc = + [sensorTopic, sensorChannel](const String &topic, ArduinoJson::V6213PB2::JsonObject &value) -> bool + { + const uint32_t VALUE_PRECISION = 2U; /* 2 digits after the . */ + + UTIL_NOT_USED(topic); + + value["value"] = sensorChannel->getValueAsString(VALUE_PRECISION); + + return true; + }; + TopicHandlerService::HasChangedFunc hasChangedFunc = + [sensorTopic, sensorChannel, sensorTopicRunData](const String &topic) -> bool + { + bool hasChanged = false; + + UTIL_NOT_USED(topic); + + if ((nullptr != sensorTopic) && + (nullptr != sensorChannel) && + (nullptr != sensorTopicRunData)) + { + String value = sensorChannel->getValueAsString(2U); + uint32_t timestamp = millis(); + uint32_t delta = timestamp - sensorTopicRunData->lastTimestamp; + + if ((sensorTopicRunData->lastValue != value) && + (sensorTopic->updatePeriod <= delta)) + { + sensorTopicRunData->lastValue = value; + sensorTopicRunData->lastTimestamp = timestamp; + + hasChanged = true; + } + } + + return hasChanged; + }; + + topicHandlerService.registerTopic(m_deviceId, entityId, channelName, extra, getTopicFunc, hasChangedFunc, nullptr, nullptr); + } + } +} + + +void SensorDataProvider::unregisterSensorTopics() +{ + uint8_t index = 0U; + TopicHandlerService& topicHandlerService = TopicHandlerService::getInstance(); + + for(index = 0U; index < UTIL_ARRAY_NUM(gSensorTopics); ++index) + { + const SensorTopic* sensorTopic = &gSensorTopics[index]; + String channelName = "/" + ISensorChannel::channelTypeToName(sensorTopic->sensorChannelType); + String entityId = "sensors/"; + + entityId += index; + + topicHandlerService.unregisterTopic(m_deviceId, entityId, channelName); + } +} + /****************************************************************************** * External Functions *****************************************************************************/ diff --git a/src/Hal/SensorDataProvider.h b/src/Hal/SensorDataProvider.h index 0cdffb29..130c68a5 100644 --- a/src/Hal/SensorDataProvider.h +++ b/src/Hal/SensorDataProvider.h @@ -90,6 +90,11 @@ class SensorDataProvider */ void begin(); + /** + * Stop the sensor data provider. + */ + void end(); + /** * Get number of installed sensor drivers, independed of the physical * sensor availability. @@ -158,6 +163,11 @@ class SensorDataProvider */ SensorDataProviderImpl* m_impl; + /** + * Device id, used for topic registration. + */ + String m_deviceId; + /** * Constructs the sensor data provder. */ @@ -192,6 +202,16 @@ class SensorDataProvider * Create file with the default calibration values. */ void createCalibrationFile(); + + /** + * Register sensor topics. + */ + void registerSensorTopics(); + + /** + * Unregister sensor topics. + */ + void unregisterSensorTopics(); }; /****************************************************************************** diff --git a/src/StateMachine/RestartState.cpp b/src/StateMachine/RestartState.cpp index 704b162d..6b2625c0 100644 --- a/src/StateMachine/RestartState.cpp +++ b/src/StateMachine/RestartState.cpp @@ -38,6 +38,7 @@ #include "UpdateMgr.h" #include "FileSystem.h" #include "Services.h" +#include "SensorDataProvider.h" #include #include @@ -77,6 +78,7 @@ void RestartState::entry(StateMachine& sm) m_timer.start(WAIT_TILL_STOP_SVC); + SensorDataProvider::getInstance().end(); /* Stop before all services will be stopped. */ Services::stopAll(); } From 89d17448432474992881baa0e06cd0888953de32 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 29 Oct 2023 17:29:17 +0100 Subject: [PATCH 038/105] Fixed the MQTT on change publishing. #132 --- .../src/MqttApiTopicHandler.cpp | 9 +---- .../src/TopicHandlerService.cpp | 39 ++++--------------- 2 files changed, 10 insertions(+), 38 deletions(-) diff --git a/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp b/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp index bb9a0f28..6e5a3f4c 100644 --- a/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp +++ b/lib/MqttApiTopicHandler/src/MqttApiTopicHandler.cpp @@ -127,14 +127,11 @@ void MqttApiTopicHandler::registerTopic(const String& deviceId, const String& en topicUriWriteable = mqttTopicNameBase + MQTT_ENDPOINT_WRITE_ACCESS; + LOG_INFO("Subscribe: %s", topicUriWriteable.c_str()); if (false == mqttService.subscribe(topicUriWriteable, setCallback)) { LOG_WARNING("Couldn't subscribe %s.", topicUriWriteable.c_str()); } - else - { - LOG_INFO("Subscribed: %s", topicUriWriteable.c_str()); - } } /* Handle Home Assistant extension */ @@ -317,7 +314,6 @@ void MqttApiTopicHandler::write(const String& deviceId, const String& entityId, { JsonVariantConst jsonFileName = jsonDoc["fileName"]; JsonVariantConst jsonFileBase64 = jsonDoc["file"]; - JsonObjectConst jsonValue; /* File transfer? */ if ((true == jsonFileName.is()) && @@ -387,8 +383,7 @@ void MqttApiTopicHandler::write(const String& deviceId, const String& entityId, } } - jsonValue = jsonDoc.as(); - if (false == setTopicFunc(topic, jsonValue)) + if (false == setTopicFunc(topic, jsonDoc.as())) { LOG_WARNING("Payload rejected by %s.", entityId.c_str()); } diff --git a/lib/TopicHandlerService/src/TopicHandlerService.cpp b/lib/TopicHandlerService/src/TopicHandlerService.cpp index d3d4227d..4c714bf9 100644 --- a/lib/TopicHandlerService/src/TopicHandlerService.cpp +++ b/lib/TopicHandlerService/src/TopicHandlerService.cpp @@ -362,14 +362,8 @@ void TopicHandlerService::strToAccess(IPluginMaintenance* plugin, const String& { getTopicFunc = [plugin](const String& topic, JsonObject& value) -> bool { - bool status = false; - - if (nullptr != plugin) - { - status = plugin->getTopic(topic, value); - } - - return status; + LOG_INFO("Get %s of plugin %u.", topic.c_str(), plugin->getUID()); + return plugin->getTopic(topic, value); }; } @@ -377,26 +371,13 @@ void TopicHandlerService::strToAccess(IPluginMaintenance* plugin, const String& { setTopicFunc = [plugin](const String& topic, const JsonObjectConst& value) -> bool { - bool status = false; - - if (nullptr != plugin) - { - status = plugin->setTopic(topic, value); - } - - return status; + LOG_INFO("Set %s of plugin %u.", topic.c_str(), plugin->getUID()); + return plugin->setTopic(topic, value); }; uploadReqFunc = [plugin](const String& topic, const String& srcFilename, String& dstFilename) -> bool { - bool status = false; - - if (nullptr != plugin) - { - status = plugin->isUploadAccepted(topic, srcFilename, dstFilename); - } - - return status; + return plugin->isUploadAccepted(topic, srcFilename, dstFilename); }; } } @@ -505,15 +486,11 @@ void TopicHandlerService::processOnChange() (nullptr != pluginMetaData->plugin) && (true == pluginMetaData->plugin->hasTopicChanged(pluginMetaData->topic))) { - String entityId; - - entityId = pluginMetaData->plugin->getUID(); - notifyAllHandlers(pluginMetaData->deviceId, entityId, pluginMetaData->topic); + notifyAllHandlers(pluginMetaData->deviceId, getEntityIdByPluginUid(pluginMetaData->plugin->getUID()), pluginMetaData->topic); - entityId = pluginMetaData->plugin->getAlias(); - if (false == entityId.isEmpty()) + if (false == pluginMetaData->plugin->getAlias().isEmpty()) { - notifyAllHandlers(pluginMetaData->deviceId, entityId, pluginMetaData->topic); + notifyAllHandlers(pluginMetaData->deviceId, getEntityIdByPluginAlias(pluginMetaData->plugin->getAlias()), pluginMetaData->topic); } } From 55f936ff4c5673e2bd016da71087c60c2ca51aa6 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Mon, 30 Oct 2023 22:15:30 +0100 Subject: [PATCH 039/105] Fixed purge of sensor topics in case of user requested to disconnect. #132 --- src/StateMachine/ConnectedState.cpp | 17 ++++++++++++++--- src/StateMachine/RestartState.cpp | 2 -- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/StateMachine/ConnectedState.cpp b/src/StateMachine/ConnectedState.cpp index dbd38965..6bce0f1d 100644 --- a/src/StateMachine/ConnectedState.cpp +++ b/src/StateMachine/ConnectedState.cpp @@ -38,6 +38,7 @@ #include "MyWebServer.h" #include "DisplayMgr.h" #include "Services.h" +#include "SensorDataProvider.h" #include "ConnectingState.h" #include "RestartState.h" @@ -182,10 +183,20 @@ void ConnectedState::exit(StateMachine& sm) { UTIL_NOT_USED(sm); - /* Disconnect all connections */ - (void)WiFi.disconnect(); + /* User requested (power off / restart after update) to disconnect? */ + if (true == WiFi.isConnected()) + { + /* Purge sensor topics (MQTT) */ + SensorDataProvider::getInstance().end(); + + /* Purge plugin topics (MQTT) */ + /* TODO */ + + /* Disconnect all connections */ + (void)WiFi.disconnect(); + } - /* Notify about lost network connection. */ + /* Notify about no network connection. */ DisplayMgr::getInstance().setNetworkStatus(false); } diff --git a/src/StateMachine/RestartState.cpp b/src/StateMachine/RestartState.cpp index 6b2625c0..704b162d 100644 --- a/src/StateMachine/RestartState.cpp +++ b/src/StateMachine/RestartState.cpp @@ -38,7 +38,6 @@ #include "UpdateMgr.h" #include "FileSystem.h" #include "Services.h" -#include "SensorDataProvider.h" #include #include @@ -78,7 +77,6 @@ void RestartState::entry(StateMachine& sm) m_timer.start(WAIT_TILL_STOP_SVC); - SensorDataProvider::getInstance().end(); /* Stop before all services will be stopped. */ Services::stopAll(); } From a424507ac1527e3ede03d2ddaf2c88ff65b1aeba Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Wed, 1 Nov 2023 20:27:29 +0100 Subject: [PATCH 040/105] If user requests going to off state, the plugin and sensor topics will be unregistered. #147 --- .../src/HomeAssistantMqtt.cpp | 54 ++++++++++++++++--- lib/SettingsService/src/SettingsService.cpp | 2 +- src/Plugin/PluginMgr.cpp | 13 +++++ src/Plugin/PluginMgr.h | 6 +++ src/StateMachine/ConnectedState.cpp | 48 +++++++++-------- 5 files changed, 93 insertions(+), 30 deletions(-) diff --git a/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp b/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp index b5a1eb6b..b9541202 100644 --- a/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp +++ b/lib/MqttApiTopicHandler/src/HomeAssistantMqtt.cpp @@ -207,13 +207,55 @@ void HomeAssistantMqtt::unregisterMqttDiscovery(const String& deviceId, const St while(m_mqttDiscoveryInfoList.end() != listOfMqttDiscoveryInfoIt) { - MqttDiscoveryInfo* mqttDiscoveryInfo = *listOfMqttDiscoveryInfoIt; + MqttDiscoveryInfo* mqttDiscoveryInfo = *listOfMqttDiscoveryInfoIt; + bool found = false; if ((nullptr != mqttDiscoveryInfo) && (deviceId == mqttDiscoveryInfo->nodeId) && - (objectId == mqttDiscoveryInfo->objectId) && - (stateTopic == mqttDiscoveryInfo->discoveryDetails["stat_t"].as()) && - (cmdTopic == mqttDiscoveryInfo->discoveryDetails["cmd_t"].as())) + (objectId == mqttDiscoveryInfo->objectId)) + { + JsonVariantConst jsonStateTopic = mqttDiscoveryInfo->discoveryDetails["stat_t"]; + JsonVariantConst jsonCmdTopic = mqttDiscoveryInfo->discoveryDetails["cmd_t"]; + + /* Topic only readable? */ + if ((false == jsonStateTopic.isNull()) && + (true == jsonCmdTopic.isNull())) + { + if (stateTopic == jsonStateTopic.as()) + { + found = true; + } + } + /* Topic only writeable? */ + else if ((true == jsonStateTopic.isNull()) && + (false == jsonCmdTopic.isNull())) + { + if (cmdTopic == jsonCmdTopic.as()) + { + found = true; + } + } + /* Topic is read- and writeable? */ + else if ((false == jsonStateTopic.isNull()) && + (false == jsonCmdTopic.isNull())) + { + if ((stateTopic == jsonStateTopic.as()) && + (cmdTopic == jsonCmdTopic.as())) + { + found = true; + } + } + else + { + ; + } + } + + if (false == found) + { + ++listOfMqttDiscoveryInfoIt; + } + else { MqttService& mqttService = MqttService::getInstance(); String mqttTopic; @@ -235,10 +277,6 @@ void HomeAssistantMqtt::unregisterMqttDiscovery(const String& deviceId, const St delete mqttDiscoveryInfo; mqttDiscoveryInfo = nullptr; } - else - { - ++listOfMqttDiscoveryInfoIt; - } } } } diff --git a/lib/SettingsService/src/SettingsService.cpp b/lib/SettingsService/src/SettingsService.cpp index 20d44f68..628076ea 100644 --- a/lib/SettingsService/src/SettingsService.cpp +++ b/lib/SettingsService/src/SettingsService.cpp @@ -332,7 +332,7 @@ bool SettingsService::start() void SettingsService::stop() { - /* Nothing to do. */ + LOG_INFO("Settings service stopped."); } void SettingsService::process() diff --git a/src/Plugin/PluginMgr.cpp b/src/Plugin/PluginMgr.cpp index 5ffa694d..33ab8b48 100644 --- a/src/Plugin/PluginMgr.cpp +++ b/src/Plugin/PluginMgr.cpp @@ -163,6 +163,19 @@ bool PluginMgr::setPluginAliasName(IPluginMaintenance* plugin, const String& ali return isSuccessful; } +void PluginMgr::unregisterAllPluginTopics() +{ + uint8_t maxSlots = DisplayMgr::getInstance().getMaxSlots(); + uint8_t slotId = 0U; + + for(slotId = 0U; slotId < maxSlots; ++slotId) + { + IPluginMaintenance* plugin = DisplayMgr::getInstance().getPluginInSlot(slotId); + + TopicHandlerService::getInstance().unregisterTopics(m_deviceId, plugin); + } +} + bool PluginMgr::load() { bool isSuccessful = true; diff --git a/src/Plugin/PluginMgr.h b/src/Plugin/PluginMgr.h index 603f397f..3763a808 100644 --- a/src/Plugin/PluginMgr.h +++ b/src/Plugin/PluginMgr.h @@ -135,6 +135,12 @@ class PluginMgr */ bool setPluginAliasName(IPluginMaintenance* plugin, const String& alias); + /** + * Unregister all plugin topics from the topic handler service. + * The plugins will still be installed, but won't get any update from outside. + */ + void unregisterAllPluginTopics(); + /** * Load plugin installation from persistent memory. * It will automatically enable the installed plugins. diff --git a/src/StateMachine/ConnectedState.cpp b/src/StateMachine/ConnectedState.cpp index 6bce0f1d..8cebadfc 100644 --- a/src/StateMachine/ConnectedState.cpp +++ b/src/StateMachine/ConnectedState.cpp @@ -39,6 +39,7 @@ #include "DisplayMgr.h" #include "Services.h" #include "SensorDataProvider.h" +#include "PluginMgr.h" #include "ConnectingState.h" #include "RestartState.h" @@ -86,7 +87,7 @@ void ConnectedState::entry(StateMachine& sm) LOG_INFO("Connected."); - /* Get hostname and notifyURL. */ + /* Get some settings. */ if (false == settings.open(true)) { LOG_WARNING("Use default hostname."); @@ -139,23 +140,6 @@ void ConnectedState::entry(StateMachine& sm) } } -void ConnectedState::initHttpClient() -{ - m_client.regOnResponse([](const HttpResponse& rsp){ - uint16_t statusCode = rsp.getStatusCode(); - - if (HttpStatus::STATUS_CODE_OK == statusCode) - { - LOG_INFO("Online state reported."); - } - - }); - - m_client.regOnError([]() { - LOG_WARNING("Connection error happened."); - }); -} - void ConnectedState::process(StateMachine& sm) { /* Handle update, there may be one in the background. */ @@ -189,10 +173,15 @@ void ConnectedState::exit(StateMachine& sm) /* Purge sensor topics (MQTT) */ SensorDataProvider::getInstance().end(); - /* Purge plugin topics (MQTT) */ - /* TODO */ + /* Unregister all plugins, which will purge all of their topics (MQTT). */ + PluginMgr::getInstance().unregisterAllPluginTopics(); + + /* Stop all services now to allow them having graceful disconnection from + * servers until the wifi will be disconnected. + */ + Services::stopAll(); - /* Disconnect all connections */ + /* Disconnect wifi connection. */ (void)WiFi.disconnect(); } @@ -208,6 +197,23 @@ void ConnectedState::exit(StateMachine& sm) * Private Methods *****************************************************************************/ +void ConnectedState::initHttpClient() +{ + m_client.regOnResponse([](const HttpResponse& rsp){ + uint16_t statusCode = rsp.getStatusCode(); + + if (HttpStatus::STATUS_CODE_OK == statusCode) + { + LOG_INFO("Online state reported."); + } + + }); + + m_client.regOnError([]() { + LOG_WARNING("Connection error happened."); + }); +} + void ConnectedState::pushUrl(const String& pushUrl) { /* If a push URL is set, notify about the online status. */ From 8cbf7994caf09df7b156ffdf81e5d5f00e5ce199 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Wed, 1 Nov 2023 20:49:05 +0100 Subject: [PATCH 041/105] Bugfix: Sprite sheet and bitmap filenames switched. --- lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp | 4 ++-- lib/IconTextPlugin/src/IconTextPlugin.cpp | 4 ++-- lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp b/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp index 08fc1a78..7ea183ce 100644 --- a/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp +++ b/lib/IconTextLampPlugin/src/IconTextLampPlugin.cpp @@ -506,7 +506,7 @@ bool IconTextLampPlugin::loadBitmap(const String& filename) if (false == m_spriteSheetPath.isEmpty()) { - status = m_bitmapWidget.loadSpriteSheet(FILESYSTEM, m_iconPath, m_spriteSheetPath); + status = m_bitmapWidget.loadSpriteSheet(FILESYSTEM, m_spriteSheetPath, m_iconPath); } if (false == status) @@ -531,7 +531,7 @@ bool IconTextLampPlugin::loadSpriteSheet(const String& filename) if (false == m_iconPath.isEmpty()) { - status = m_bitmapWidget.loadSpriteSheet(FILESYSTEM, m_iconPath, m_spriteSheetPath); + status = m_bitmapWidget.loadSpriteSheet(FILESYSTEM, m_spriteSheetPath, m_iconPath); } return status; diff --git a/lib/IconTextPlugin/src/IconTextPlugin.cpp b/lib/IconTextPlugin/src/IconTextPlugin.cpp index 8eda4b5a..0a72427f 100644 --- a/lib/IconTextPlugin/src/IconTextPlugin.cpp +++ b/lib/IconTextPlugin/src/IconTextPlugin.cpp @@ -382,7 +382,7 @@ bool IconTextPlugin::loadBitmap(const String& filename) if (false == m_spriteSheetPath.isEmpty()) { - status = m_bitmapWidget.loadSpriteSheet(FILESYSTEM, m_iconPath, m_spriteSheetPath); + status = m_bitmapWidget.loadSpriteSheet(FILESYSTEM, m_spriteSheetPath, m_iconPath); } if (false == status) @@ -407,7 +407,7 @@ bool IconTextPlugin::loadSpriteSheet(const String& filename) if (false == m_iconPath.isEmpty()) { - status = m_bitmapWidget.loadSpriteSheet(FILESYSTEM, m_iconPath, m_spriteSheetPath); + status = m_bitmapWidget.loadSpriteSheet(FILESYSTEM, m_spriteSheetPath, m_iconPath); } return status; diff --git a/lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp b/lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp index 050b5a2d..f9474af8 100644 --- a/lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp +++ b/lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp @@ -422,7 +422,7 @@ bool ThreeIconPlugin::loadBitmap(uint8_t iconId, const String& filename) if (false == m_spriteSheetPaths->isEmpty()) { - status = m_bitmapWidgets[iconId].loadSpriteSheet(FILESYSTEM, m_iconPaths[iconId], m_spriteSheetPaths[iconId]); + status = m_bitmapWidgets[iconId].loadSpriteSheet(FILESYSTEM, m_spriteSheetPaths[iconId], m_iconPaths[iconId]); } if (false == status) @@ -450,7 +450,7 @@ bool ThreeIconPlugin::loadSpriteSheet(uint8_t iconId, const String& filename) if (false == m_iconPaths[iconId].isEmpty()) { - status = m_bitmapWidgets[iconId].loadSpriteSheet(FILESYSTEM, m_iconPaths[iconId], m_spriteSheetPaths[iconId]); + status = m_bitmapWidgets[iconId].loadSpriteSheet(FILESYSTEM, m_spriteSheetPaths[iconId], m_iconPaths[iconId]); } } From d4493e5e7d1b3b33c001feae1a535f297fc36f5c Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Thu, 2 Nov 2023 01:00:11 +0100 Subject: [PATCH 042/105] Sensor access added to MQTT readme. #132 --- doc/MQTT.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/doc/MQTT.md b/doc/MQTT.md index 4d928163..e35b269f 100644 --- a/doc/MQTT.md +++ b/doc/MQTT.md @@ -9,9 +9,10 @@ * [Overview Mindmap](#overview-mindmap) * [MQTT Topics](#mqtt-topics) * [Birth and last will](#birth-and-last-will) - * [Plugin base URI](#plugin-base-uri) + * [Plugin topic path](#plugin-topic-path) * [Topic name](#topic-name) * [Sending a bitmap](#sending-a-bitmap) + * [Sensors](#sensors) * [Issues, Ideas And Bugs](#issues-ideas-and-bugs) * [License](#license) * [Contribution](#contribution) @@ -40,10 +41,10 @@ Pixelix supports birth and last will messages. After the successful connection establishment to the MQTT broker, Pixelix will send "online" to the <HOSTNAME>/status topic. In any disconnect case, "offline" will be sent to the <HOSTNAME>/status topic. -## Plugin base URI -The base URI to access plugin related topics can be setup with the plugin UID or the plugin alias: -* <HOSTNAME>/uid/<PLUGIN-UID>/... -* <HOSTNAME>/alias/<PLUGIN-ALIAS>/... +## Plugin topic path +The base topic path to access plugin related topics can be setup with the plugin UID or the plugin alias: +* <HOSTNAME>/display/uid/<PLUGIN-UID>/... +* <HOSTNAME>/display/alias/<PLUGIN-ALIAS>/... ## Topic name The complete topic name can be derived from the REST API documentation. @@ -51,7 +52,7 @@ The complete topic name can be derived from the REST API documentation. Example: JustTextPlugin The REST API URL looks like the following: http://<HOSTNAME>/rest/api/v1/display/uid/<PLUGIN-UID>/text?text=<TEXT> -1. Replace the http://<HOSTNAME>/rest/api/v1/display part with <HOSTNAME> --> <HOSTNAME>/uid/<PLUGIN-UID>/text?text=<TEXT> +1. Replace the http://<HOSTNAME>/rest/api/v1/ part with <HOSTNAME> --> <HOSTNAME>/display/uid/<PLUGIN-UID>/text?text=<TEXT> 2. Every URL parameter, which is in this case show=<TEXT> must be sent in JSON format. ```json @@ -75,6 +76,14 @@ Example: IconTextPlugin } ``` +## Sensors +The sensor topic path is valid if the sensor is available! + +* Temperature in °C: <HOSTNAME>/sensors/0/temperature/state +* Humidity in %: <HOSTNAME>/sensors/1/humidity/state +* Illuminance in lx: <HOSTNAME>/sensors/2/illuminance/state +* Battery SOC in %: <HOSTNAME>/sensors/3/soc/state + # Issues, Ideas And Bugs If you have further ideas or you found some bugs, great! Create a [issue](https://github.com/BlueAndi/esp-rgb-led-matrix/issues) or if you are able and willing to fix it by yourself, clone the repository and create a pull request. From 95866f12670550f68f1dc8f45ec6db28f0422ecd Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Thu, 2 Nov 2023 01:00:39 +0100 Subject: [PATCH 043/105] Obsolete requirements removed. --- doc/README.md | 5 --- doc/REQUIREMENTS.md | 81 --------------------------------------------- 2 files changed, 86 deletions(-) delete mode 100644 doc/REQUIREMENTS.md diff --git a/doc/README.md b/doc/README.md index 681c4a3d..e8a1aa54 100644 --- a/doc/README.md +++ b/doc/README.md @@ -5,17 +5,12 @@ # Documentation -* [Requirements](#requirements) * [Hardware](#hardware) * [Software](#software) * [Issues, Ideas And Bugs](#issues-ideas-and-bugs) * [License](#license) * [Contribution](#contribution) -# Requirements - -* [Requirements](REQUIREMENTS.md) - # Hardware * [Boards](./boards/README.md) diff --git a/doc/REQUIREMENTS.md b/doc/REQUIREMENTS.md deleted file mode 100644 index ca824b04..00000000 --- a/doc/REQUIREMENTS.md +++ /dev/null @@ -1,81 +0,0 @@ -# PIXELIX -![PIXELIX](./images/LogoBlack.png) - -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](http://choosealicense.com/licenses/mit/) - -# Requirements - -* [General](#general) -* [Communication](#communication) -* [User Interaction](#user-interaction) - * [Wifi](#wifi) - * [REST](#rest) - * [Websocket](#websocket) -* [Firmware update](#firmware-update) -* [TCP/IP Server](#tcpip-server) -* [Brightness Control](#brightness-control) -* [Real Time Clock](#real-time-clock) -* [Issues, Ideas And Bugs](#issues-ideas-and-bugs) -* [License](#license) -* [Contribution](#contribution) - -Next available REQ id: 21 - -# General -* REQ-01: The display shall be able to show bitmaps and text. -* REQ-02: The display shall be able to show animations. - -# Communication -* REQ-03: The communication with the display shall be via wifi. -* REQ-04: The display shall provide a [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API to exchange [JSON](https://en.wikipedia.org/wiki/JSON). -* REQ-05: The display shall provide a [websocket](https://en.wikipedia.org/wiki/WebSocket) interface. - -# User Interaction -* REQ-10: The display shall provide a user button for user interaction. -* REQ-17: The display shall provide a reset button. - -## Wifi - -* REQ-07: The display shall setting up an wifi access point on user demand (via user button) after a power-cycle. -* REQ-08: The wifi access point shall have the following default configuration: - * SSID: pixelix - * Security: WPA - * Password: "Luke, I am your father." -* REQ-09: If a remote wifi network is configured, the display shall automatically connect after a power-cycle. - -## REST - -* REQ-19: A REST interface shall be provided to control the display or retrieve information. - -## Websocket - -* REQ-20: A websocket interface shall be provided to control the display or retrieve information. - -# Firmware update -* REQ-06: The firmware shall be updated over the air. -* REQ-14: The firmware update shall be protected against malicious access via authorization. - -# TCP/IP Server - -* REQ-11: The display shall provide a [HTTP](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol)/[HTTPS](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol_Secure) webserver. -* REQ-12: The user shall be able to configure a remote wifi network, if a access point is spawned. -* REQ-12: The webserver shall be protected against malicious access via authorization. -* REQ-13: The webserver shall not run during an firmware update. - -# Brightness Control -* REQ-15: The display brightness shall be automatically adapted to the environment. -* REQ-16: The max. display brightness shall be limited to avoid a high LED matrix current, which destroys electronic parts. - -# Real Time Clock -* REQ-18: The display shall contain a real time clock, used for power modes or a clock. - -# Issues, Ideas And Bugs -If you have further ideas or you found some bugs, great! Create a [issue](https://github.com/BlueAndi/esp-rgb-led-matrix/issues) or if you are able and willing to fix it by yourself, clone the repository and create a pull request. - -# License -The whole source code is published under the [MIT license](http://choosealicense.com/licenses/mit/). -Consider the different licenses of the used third party libraries too! - -# Contribution -Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, shall be licensed as above, without any -additional terms or conditions. From 309795a68259fd0570e6f08ce0982f1de47efddb Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Thu, 2 Nov 2023 01:01:14 +0100 Subject: [PATCH 044/105] Make bold whats important. #145 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 01b6baa5..dcc3594b 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ If the device starts the very first time, the wifi station SSID and passphrase s 2. Using a terminal connecting via usb. ## Variant 1: Configure wifi station SSID and passphrase with the browser -Restart the device and keep the button pressed until it shows the SSID of the wifi access point, spawned by PIXELIX. Search for it with your mobile device and connect. +Restart the device and **keep the button pressed** until it shows the SSID of the wifi access point, spawned by PIXELIX. Search for it with your mobile device and connect. * SSID: **pixelix-<DEVICE-ID>** * Passphrase: **Luke, I am your father.** From 2ca3b3514fce44f3a6266371b9936b1f9dcaff34 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Thu, 2 Nov 2023 20:18:25 +0100 Subject: [PATCH 045/105] Websocket commands optimized in sense of memory consumption. --- src/Web/WsCommand/WsCmd.cpp | 20 +++++++++--- src/Web/WsCommand/WsCmd.h | 40 +++++++++++++++++++++-- src/Web/WsCommand/WsCmdAlias.cpp | 6 ++-- src/Web/WsCommand/WsCmdBrightness.cpp | 6 ++-- src/Web/WsCommand/WsCmdEffect.cpp | 6 ++-- src/Web/WsCommand/WsCmdGetDisp.cpp | 43 ++++++++++++++++--------- src/Web/WsCommand/WsCmdInstall.cpp | 8 +++-- src/Web/WsCommand/WsCmdLog.cpp | 8 +++-- src/Web/WsCommand/WsCmdPlugins.cpp | 4 ++- src/Web/WsCommand/WsCmdSlotDuration.cpp | 6 ++-- src/Web/WsCommand/WsCmdSlots.cpp | 6 ++-- 11 files changed, 114 insertions(+), 39 deletions(-) diff --git a/src/Web/WsCommand/WsCmd.cpp b/src/Web/WsCommand/WsCmd.cpp index 27a84777..f284a564 100644 --- a/src/Web/WsCommand/WsCmd.cpp +++ b/src/Web/WsCommand/WsCmd.cpp @@ -71,14 +71,15 @@ const char* WsCmd::NACK = "NACK"; * Protected Methods *****************************************************************************/ -void WsCmd::sendPositiveResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const String& msg) +void WsCmd::sendPositiveResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const char* msg) { if ((nullptr != server) && (nullptr != client)) { String rsp = ACK; - if (false == msg.isEmpty()) + if ((nullptr != msg) && + ('\0' != msg[0U])) { rsp += DELIMITER; rsp += msg; @@ -90,10 +91,10 @@ void WsCmd::sendPositiveResponse(AsyncWebSocket* server, AsyncWebSocketClient* c void WsCmd::sendPositiveResponse(AsyncWebSocket* server, AsyncWebSocketClient* client) { - sendPositiveResponse(server, client, ""); + sendPositiveResponse(server, client, nullptr); } -void WsCmd::sendNegativeResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const String& msg) +void WsCmd::sendNegativeResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const char* msg) { if ((nullptr != server) && (nullptr != client)) @@ -101,7 +102,16 @@ void WsCmd::sendNegativeResponse(AsyncWebSocket* server, AsyncWebSocketClient* c String rsp = NACK; rsp += DELIMITER; - rsp += msg; + + if ((nullptr != msg) && + ('\0' != msg[0U])) + { + rsp += msg; + } + else + { + rsp += "\"Unknown.\""; + } server->text(client->id(), rsp); } diff --git a/src/Web/WsCommand/WsCmd.h b/src/Web/WsCommand/WsCmd.h index 352a7d42..11b5dbc1 100644 --- a/src/Web/WsCommand/WsCmd.h +++ b/src/Web/WsCommand/WsCmd.h @@ -113,6 +113,42 @@ class WsCmd /** Negative response code. */ static const char* NACK; + /** + * Prepare a positive response message. + * The last added element will always be a delimiter. + * + * @param[out] msg Message to prepare + */ + void preparePositiveResponse(String& msg) + { + msg = ACK; + msg += DELIMITER; + } + + /** + * Prepare a negative response message. + * The last added element will always be a delimiter. + * + * @param[out] msg Message to prepare + */ + void prepareNegativeResponse(String& msg) + { + msg = NACK; + msg += DELIMITER; + } + + /** + * Send a response to the client. + * + * @param[in] server Websocket server which is used to send a message to the client. + * @param[in] client The client the message belongs to. + * @param[in] msg The response messsage. + */ + void sendResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const String& msg) + { + server->text(client->id(), msg); + } + /** * Send positive response to the client. * @@ -120,7 +156,7 @@ class WsCmd * @param[in] client The client the message belongs to. * @param[in] msg The negative response messsage. */ - void sendPositiveResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const String& msg); + void sendPositiveResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const char* msg); /** * Send negative response to the client. @@ -138,7 +174,7 @@ class WsCmd * @param[in] client The client the message belongs to. * @param[in] msg The negative response messsage. */ - void sendNegativeResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const String& msg); + void sendNegativeResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const char* msg); private: diff --git a/src/Web/WsCommand/WsCmdAlias.cpp b/src/Web/WsCommand/WsCmdAlias.cpp index 68be4e4a..2dc85bf0 100644 --- a/src/Web/WsCommand/WsCmdAlias.cpp +++ b/src/Web/WsCommand/WsCmdAlias.cpp @@ -84,11 +84,13 @@ void WsCmdAlias::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) (void)DisplayMgr::getInstance().setPluginAliasName(m_pluginUid, m_alias); } - msg = "\""; + preparePositiveResponse(msg); + + msg += "\""; msg += DisplayMgr::getInstance().getPluginAliasName(m_pluginUid); msg += "\""; - sendPositiveResponse(server, client, msg); + sendResponse(server, client, msg); } m_isError = false; diff --git a/src/Web/WsCommand/WsCmdBrightness.cpp b/src/Web/WsCommand/WsCmdBrightness.cpp index bc67c46b..c427afad 100644 --- a/src/Web/WsCommand/WsCmdBrightness.cpp +++ b/src/Web/WsCommand/WsCmdBrightness.cpp @@ -93,11 +93,13 @@ void WsCmdBrightness::execute(AsyncWebSocket* server, AsyncWebSocketClient* clie ; } - msg = DisplayMgr::getInstance().getBrightness(); + preparePositiveResponse(msg); + + msg += DisplayMgr::getInstance().getBrightness(); msg += DELIMITER; msg += (true == DisplayMgr::getInstance().getAutoBrightnessAdjustment()) ? 1 : 0; - sendPositiveResponse(server, client, msg); + sendResponse(server, client, msg); } m_isError = false; diff --git a/src/Web/WsCommand/WsCmdEffect.cpp b/src/Web/WsCommand/WsCmdEffect.cpp index 7b222d41..8e6871bb 100644 --- a/src/Web/WsCommand/WsCmdEffect.cpp +++ b/src/Web/WsCommand/WsCmdEffect.cpp @@ -83,9 +83,11 @@ void WsCmdEffect::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) DisplayMgr::getInstance().activateNextFadeEffect(static_cast(m_fadeEffect)); } - msg = DisplayMgr::getInstance().getFadeEffect(); + preparePositiveResponse(msg); - sendPositiveResponse(server, client, msg); + msg += DisplayMgr::getInstance().getFadeEffect(); + + sendResponse(server, client, msg); } m_isError = false; diff --git a/src/Web/WsCommand/WsCmdGetDisp.cpp b/src/Web/WsCommand/WsCmdGetDisp.cpp index db442885..8b743a48 100644 --- a/src/Web/WsCommand/WsCmdGetDisp.cpp +++ b/src/Web/WsCommand/WsCmdGetDisp.cpp @@ -78,27 +78,40 @@ void WsCmdGetDisp::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) } else { - uint32_t index = 0U; - String msg; IDisplay& display = Display::getInstance(); - uint32_t framebuffer[display.getWidth() * display.getHeight()]; - uint8_t slotId = SlotList::SLOT_ID_INVALID; + size_t fbLength = display.getWidth() * display.getHeight(); + uint32_t* framebuffer = new(std::nothrow) uint32_t[fbLength]; - DisplayMgr::getInstance().getFBCopy(framebuffer, UTIL_ARRAY_NUM(framebuffer), &slotId); + if (nullptr == framebuffer) + { + m_isError = true; + } + else + { + uint32_t index = 0U; + String msg; + uint8_t slotId = SlotList::SLOT_ID_INVALID; - msg = slotId; - msg += DELIMITER; - msg += display.getWidth(); - msg += DELIMITER; - msg += display.getHeight(); + DisplayMgr::getInstance().getFBCopy(framebuffer, fbLength, &slotId); - for(index = 0U; index < UTIL_ARRAY_NUM(framebuffer); ++index) - { + preparePositiveResponse(msg); + + msg += slotId; + msg += DELIMITER; + msg += display.getWidth(); msg += DELIMITER; - msg += Util::uint32ToHex(framebuffer[index]); + msg += display.getHeight(); + + for(index = 0U; index < fbLength; ++index) + { + msg += DELIMITER; + msg += Util::uint32ToHex(framebuffer[index]); + } + + delete[] framebuffer; + + sendResponse(server, client, msg); } - - sendPositiveResponse(server, client, msg); } m_isError = false; diff --git a/src/Web/WsCommand/WsCmdInstall.cpp b/src/Web/WsCommand/WsCmdInstall.cpp index 65a44506..136a2801 100644 --- a/src/Web/WsCommand/WsCmdInstall.cpp +++ b/src/Web/WsCommand/WsCmdInstall.cpp @@ -86,7 +86,9 @@ void WsCmdInstall::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) } else { - msg = DisplayMgr::getInstance().getSlotIdByPluginUID(plugin->getUID()); + preparePositiveResponse(msg); + + msg += DisplayMgr::getInstance().getSlotIdByPluginUID(plugin->getUID()); msg += DELIMITER; msg += plugin->getUID(); @@ -94,9 +96,9 @@ void WsCmdInstall::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) /* Save current installed plugins to persistent memory. */ PluginMgr::getInstance().save(); - } - sendPositiveResponse(server, client, msg); + sendResponse(server, client, msg); + } } m_isError = false; diff --git a/src/Web/WsCommand/WsCmdLog.cpp b/src/Web/WsCommand/WsCmdLog.cpp index 1d9816e5..c0f64f6e 100644 --- a/src/Web/WsCommand/WsCmdLog.cpp +++ b/src/Web/WsCommand/WsCmdLog.cpp @@ -94,17 +94,19 @@ void WsCmdLog::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) selectedSink = Logging::getInstance().getSelectedSink(); + preparePositiveResponse(msg); + if ((nullptr == selectedSink) || (selectedSink->getName() != "Websocket")) { - msg = "0"; + msg += "0"; } else { - msg = "1"; + msg += "1"; } - sendPositiveResponse(server, client, msg); + sendResponse(server, client, msg); } m_cnt = 0U; diff --git a/src/Web/WsCommand/WsCmdPlugins.cpp b/src/Web/WsCommand/WsCmdPlugins.cpp index a80395f5..e36f8bd1 100644 --- a/src/Web/WsCommand/WsCmdPlugins.cpp +++ b/src/Web/WsCommand/WsCmdPlugins.cpp @@ -80,6 +80,8 @@ void WsCmdPlugins::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) uint8_t cnt = 0U; const char* pluginName = PluginMgr::getInstance().findFirst(); + preparePositiveResponse(msg); + while(nullptr != pluginName) { if (0 < cnt) @@ -96,7 +98,7 @@ void WsCmdPlugins::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) ++cnt; } - sendPositiveResponse(server, client, msg); + sendResponse(server, client, msg); } m_isError = false; diff --git a/src/Web/WsCommand/WsCmdSlotDuration.cpp b/src/Web/WsCommand/WsCmdSlotDuration.cpp index a1ccc46b..57b8bdf6 100644 --- a/src/Web/WsCommand/WsCmdSlotDuration.cpp +++ b/src/Web/WsCommand/WsCmdSlotDuration.cpp @@ -89,9 +89,11 @@ void WsCmdSlotDuration::execute(AsyncWebSocket* server, AsyncWebSocketClient* cl } } - msg = DisplayMgr::getInstance().getSlotDuration(m_slotId); + preparePositiveResponse(msg); - sendPositiveResponse(server, client, msg); + msg += DisplayMgr::getInstance().getSlotDuration(m_slotId); + + sendResponse(server, client, msg); } m_isError = false; diff --git a/src/Web/WsCommand/WsCmdSlots.cpp b/src/Web/WsCommand/WsCmdSlots.cpp index bd3d5888..744816ac 100644 --- a/src/Web/WsCommand/WsCmdSlots.cpp +++ b/src/Web/WsCommand/WsCmdSlots.cpp @@ -82,7 +82,9 @@ void WsCmdSlots::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) uint8_t slotId = SlotList::SLOT_ID_INVALID; uint8_t stickySlot = displayMgr.getStickySlot(); - msg = displayMgr.getMaxSlots(); + preparePositiveResponse(msg); + + msg += displayMgr.getMaxSlots(); /* Provides for every slot: * - Name of plugin. @@ -120,7 +122,7 @@ void WsCmdSlots::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) msg += duration; } - sendPositiveResponse(server, client, msg); + sendResponse(server, client, msg); } m_isError = false; From 8cff24b54cf8d0ec2f8913ef45aadbb766df0698 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Thu, 2 Nov 2023 20:18:25 +0100 Subject: [PATCH 046/105] Websocket commands optimized in sense of memory consumption. --- src/Web/WsCommand/WsCmd.cpp | 20 +++++++++--- src/Web/WsCommand/WsCmd.h | 40 +++++++++++++++++++++-- src/Web/WsCommand/WsCmdAlias.cpp | 6 ++-- src/Web/WsCommand/WsCmdBrightness.cpp | 6 ++-- src/Web/WsCommand/WsCmdEffect.cpp | 6 ++-- src/Web/WsCommand/WsCmdGetDisp.cpp | 43 ++++++++++++++++--------- src/Web/WsCommand/WsCmdInstall.cpp | 8 +++-- src/Web/WsCommand/WsCmdIperf.cpp | 24 ++++++++++---- src/Web/WsCommand/WsCmdLog.cpp | 8 +++-- src/Web/WsCommand/WsCmdPlugins.cpp | 4 ++- src/Web/WsCommand/WsCmdSlotDuration.cpp | 6 ++-- src/Web/WsCommand/WsCmdSlots.cpp | 6 ++-- 12 files changed, 131 insertions(+), 46 deletions(-) diff --git a/src/Web/WsCommand/WsCmd.cpp b/src/Web/WsCommand/WsCmd.cpp index 27a84777..f284a564 100644 --- a/src/Web/WsCommand/WsCmd.cpp +++ b/src/Web/WsCommand/WsCmd.cpp @@ -71,14 +71,15 @@ const char* WsCmd::NACK = "NACK"; * Protected Methods *****************************************************************************/ -void WsCmd::sendPositiveResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const String& msg) +void WsCmd::sendPositiveResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const char* msg) { if ((nullptr != server) && (nullptr != client)) { String rsp = ACK; - if (false == msg.isEmpty()) + if ((nullptr != msg) && + ('\0' != msg[0U])) { rsp += DELIMITER; rsp += msg; @@ -90,10 +91,10 @@ void WsCmd::sendPositiveResponse(AsyncWebSocket* server, AsyncWebSocketClient* c void WsCmd::sendPositiveResponse(AsyncWebSocket* server, AsyncWebSocketClient* client) { - sendPositiveResponse(server, client, ""); + sendPositiveResponse(server, client, nullptr); } -void WsCmd::sendNegativeResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const String& msg) +void WsCmd::sendNegativeResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const char* msg) { if ((nullptr != server) && (nullptr != client)) @@ -101,7 +102,16 @@ void WsCmd::sendNegativeResponse(AsyncWebSocket* server, AsyncWebSocketClient* c String rsp = NACK; rsp += DELIMITER; - rsp += msg; + + if ((nullptr != msg) && + ('\0' != msg[0U])) + { + rsp += msg; + } + else + { + rsp += "\"Unknown.\""; + } server->text(client->id(), rsp); } diff --git a/src/Web/WsCommand/WsCmd.h b/src/Web/WsCommand/WsCmd.h index 352a7d42..11b5dbc1 100644 --- a/src/Web/WsCommand/WsCmd.h +++ b/src/Web/WsCommand/WsCmd.h @@ -113,6 +113,42 @@ class WsCmd /** Negative response code. */ static const char* NACK; + /** + * Prepare a positive response message. + * The last added element will always be a delimiter. + * + * @param[out] msg Message to prepare + */ + void preparePositiveResponse(String& msg) + { + msg = ACK; + msg += DELIMITER; + } + + /** + * Prepare a negative response message. + * The last added element will always be a delimiter. + * + * @param[out] msg Message to prepare + */ + void prepareNegativeResponse(String& msg) + { + msg = NACK; + msg += DELIMITER; + } + + /** + * Send a response to the client. + * + * @param[in] server Websocket server which is used to send a message to the client. + * @param[in] client The client the message belongs to. + * @param[in] msg The response messsage. + */ + void sendResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const String& msg) + { + server->text(client->id(), msg); + } + /** * Send positive response to the client. * @@ -120,7 +156,7 @@ class WsCmd * @param[in] client The client the message belongs to. * @param[in] msg The negative response messsage. */ - void sendPositiveResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const String& msg); + void sendPositiveResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const char* msg); /** * Send negative response to the client. @@ -138,7 +174,7 @@ class WsCmd * @param[in] client The client the message belongs to. * @param[in] msg The negative response messsage. */ - void sendNegativeResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const String& msg); + void sendNegativeResponse(AsyncWebSocket* server, AsyncWebSocketClient* client, const char* msg); private: diff --git a/src/Web/WsCommand/WsCmdAlias.cpp b/src/Web/WsCommand/WsCmdAlias.cpp index 68be4e4a..2dc85bf0 100644 --- a/src/Web/WsCommand/WsCmdAlias.cpp +++ b/src/Web/WsCommand/WsCmdAlias.cpp @@ -84,11 +84,13 @@ void WsCmdAlias::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) (void)DisplayMgr::getInstance().setPluginAliasName(m_pluginUid, m_alias); } - msg = "\""; + preparePositiveResponse(msg); + + msg += "\""; msg += DisplayMgr::getInstance().getPluginAliasName(m_pluginUid); msg += "\""; - sendPositiveResponse(server, client, msg); + sendResponse(server, client, msg); } m_isError = false; diff --git a/src/Web/WsCommand/WsCmdBrightness.cpp b/src/Web/WsCommand/WsCmdBrightness.cpp index bc67c46b..c427afad 100644 --- a/src/Web/WsCommand/WsCmdBrightness.cpp +++ b/src/Web/WsCommand/WsCmdBrightness.cpp @@ -93,11 +93,13 @@ void WsCmdBrightness::execute(AsyncWebSocket* server, AsyncWebSocketClient* clie ; } - msg = DisplayMgr::getInstance().getBrightness(); + preparePositiveResponse(msg); + + msg += DisplayMgr::getInstance().getBrightness(); msg += DELIMITER; msg += (true == DisplayMgr::getInstance().getAutoBrightnessAdjustment()) ? 1 : 0; - sendPositiveResponse(server, client, msg); + sendResponse(server, client, msg); } m_isError = false; diff --git a/src/Web/WsCommand/WsCmdEffect.cpp b/src/Web/WsCommand/WsCmdEffect.cpp index 7b222d41..8e6871bb 100644 --- a/src/Web/WsCommand/WsCmdEffect.cpp +++ b/src/Web/WsCommand/WsCmdEffect.cpp @@ -83,9 +83,11 @@ void WsCmdEffect::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) DisplayMgr::getInstance().activateNextFadeEffect(static_cast(m_fadeEffect)); } - msg = DisplayMgr::getInstance().getFadeEffect(); + preparePositiveResponse(msg); - sendPositiveResponse(server, client, msg); + msg += DisplayMgr::getInstance().getFadeEffect(); + + sendResponse(server, client, msg); } m_isError = false; diff --git a/src/Web/WsCommand/WsCmdGetDisp.cpp b/src/Web/WsCommand/WsCmdGetDisp.cpp index db442885..8b743a48 100644 --- a/src/Web/WsCommand/WsCmdGetDisp.cpp +++ b/src/Web/WsCommand/WsCmdGetDisp.cpp @@ -78,27 +78,40 @@ void WsCmdGetDisp::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) } else { - uint32_t index = 0U; - String msg; IDisplay& display = Display::getInstance(); - uint32_t framebuffer[display.getWidth() * display.getHeight()]; - uint8_t slotId = SlotList::SLOT_ID_INVALID; + size_t fbLength = display.getWidth() * display.getHeight(); + uint32_t* framebuffer = new(std::nothrow) uint32_t[fbLength]; - DisplayMgr::getInstance().getFBCopy(framebuffer, UTIL_ARRAY_NUM(framebuffer), &slotId); + if (nullptr == framebuffer) + { + m_isError = true; + } + else + { + uint32_t index = 0U; + String msg; + uint8_t slotId = SlotList::SLOT_ID_INVALID; - msg = slotId; - msg += DELIMITER; - msg += display.getWidth(); - msg += DELIMITER; - msg += display.getHeight(); + DisplayMgr::getInstance().getFBCopy(framebuffer, fbLength, &slotId); - for(index = 0U; index < UTIL_ARRAY_NUM(framebuffer); ++index) - { + preparePositiveResponse(msg); + + msg += slotId; + msg += DELIMITER; + msg += display.getWidth(); msg += DELIMITER; - msg += Util::uint32ToHex(framebuffer[index]); + msg += display.getHeight(); + + for(index = 0U; index < fbLength; ++index) + { + msg += DELIMITER; + msg += Util::uint32ToHex(framebuffer[index]); + } + + delete[] framebuffer; + + sendResponse(server, client, msg); } - - sendPositiveResponse(server, client, msg); } m_isError = false; diff --git a/src/Web/WsCommand/WsCmdInstall.cpp b/src/Web/WsCommand/WsCmdInstall.cpp index 65a44506..136a2801 100644 --- a/src/Web/WsCommand/WsCmdInstall.cpp +++ b/src/Web/WsCommand/WsCmdInstall.cpp @@ -86,7 +86,9 @@ void WsCmdInstall::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) } else { - msg = DisplayMgr::getInstance().getSlotIdByPluginUID(plugin->getUID()); + preparePositiveResponse(msg); + + msg += DisplayMgr::getInstance().getSlotIdByPluginUID(plugin->getUID()); msg += DELIMITER; msg += plugin->getUID(); @@ -94,9 +96,9 @@ void WsCmdInstall::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) /* Save current installed plugins to persistent memory. */ PluginMgr::getInstance().save(); - } - sendPositiveResponse(server, client, msg); + sendResponse(server, client, msg); + } } m_isError = false; diff --git a/src/Web/WsCommand/WsCmdIperf.cpp b/src/Web/WsCommand/WsCmdIperf.cpp index 261964bd..f5c08698 100644 --- a/src/Web/WsCommand/WsCmdIperf.cpp +++ b/src/Web/WsCommand/WsCmdIperf.cpp @@ -81,16 +81,18 @@ void WsCmdIperf::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) { String msg; + preparePositiveResponse(msg); + if (false == m_isIperfRunning) { - msg = "0"; + msg += "0"; } else { - msg = "1"; + msg += "1"; } - sendPositiveResponse(server, client, msg); + sendResponse(server, client, msg); } /* Start iperf? */ else if (CMD_START == m_cmd) @@ -101,7 +103,11 @@ void WsCmdIperf::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) } else { - String msg = "1"; + String msg; + + preparePositiveResponse(msg); + + msg += "1"; LOG_INFO("iperf started: mode = %s-%s sip = %u.%u.%u.%u:%u, interval = %us, time = %us", (m_cfg.flag & IPERF_FLAG_TCP) ? "tcp" : "udp", @@ -109,7 +115,7 @@ void WsCmdIperf::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) m_cfg.sip & 0xffU, (m_cfg.sip >> 8) & 0xffU, (m_cfg.sip >> 16) & 0xffU, (m_cfg.sip >>24) & 0xffU, m_cfg.sport, m_cfg.interval, m_cfg.time); - sendPositiveResponse(server, client, msg); + sendResponse(server, client, msg); } } /* Stop iperf? */ @@ -121,11 +127,15 @@ void WsCmdIperf::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) } else { - String msg = "0"; + String msg; + + preparePositiveResponse(msg); + + msg += "0"; LOG_INFO("iperf stopped."); - sendPositiveResponse(server, client, msg); + sendResponse(server, client, msg); } } else diff --git a/src/Web/WsCommand/WsCmdLog.cpp b/src/Web/WsCommand/WsCmdLog.cpp index 1d9816e5..c0f64f6e 100644 --- a/src/Web/WsCommand/WsCmdLog.cpp +++ b/src/Web/WsCommand/WsCmdLog.cpp @@ -94,17 +94,19 @@ void WsCmdLog::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) selectedSink = Logging::getInstance().getSelectedSink(); + preparePositiveResponse(msg); + if ((nullptr == selectedSink) || (selectedSink->getName() != "Websocket")) { - msg = "0"; + msg += "0"; } else { - msg = "1"; + msg += "1"; } - sendPositiveResponse(server, client, msg); + sendResponse(server, client, msg); } m_cnt = 0U; diff --git a/src/Web/WsCommand/WsCmdPlugins.cpp b/src/Web/WsCommand/WsCmdPlugins.cpp index a80395f5..e36f8bd1 100644 --- a/src/Web/WsCommand/WsCmdPlugins.cpp +++ b/src/Web/WsCommand/WsCmdPlugins.cpp @@ -80,6 +80,8 @@ void WsCmdPlugins::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) uint8_t cnt = 0U; const char* pluginName = PluginMgr::getInstance().findFirst(); + preparePositiveResponse(msg); + while(nullptr != pluginName) { if (0 < cnt) @@ -96,7 +98,7 @@ void WsCmdPlugins::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) ++cnt; } - sendPositiveResponse(server, client, msg); + sendResponse(server, client, msg); } m_isError = false; diff --git a/src/Web/WsCommand/WsCmdSlotDuration.cpp b/src/Web/WsCommand/WsCmdSlotDuration.cpp index a1ccc46b..57b8bdf6 100644 --- a/src/Web/WsCommand/WsCmdSlotDuration.cpp +++ b/src/Web/WsCommand/WsCmdSlotDuration.cpp @@ -89,9 +89,11 @@ void WsCmdSlotDuration::execute(AsyncWebSocket* server, AsyncWebSocketClient* cl } } - msg = DisplayMgr::getInstance().getSlotDuration(m_slotId); + preparePositiveResponse(msg); - sendPositiveResponse(server, client, msg); + msg += DisplayMgr::getInstance().getSlotDuration(m_slotId); + + sendResponse(server, client, msg); } m_isError = false; diff --git a/src/Web/WsCommand/WsCmdSlots.cpp b/src/Web/WsCommand/WsCmdSlots.cpp index bd3d5888..744816ac 100644 --- a/src/Web/WsCommand/WsCmdSlots.cpp +++ b/src/Web/WsCommand/WsCmdSlots.cpp @@ -82,7 +82,9 @@ void WsCmdSlots::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) uint8_t slotId = SlotList::SLOT_ID_INVALID; uint8_t stickySlot = displayMgr.getStickySlot(); - msg = displayMgr.getMaxSlots(); + preparePositiveResponse(msg); + + msg += displayMgr.getMaxSlots(); /* Provides for every slot: * - Name of plugin. @@ -120,7 +122,7 @@ void WsCmdSlots::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) msg += duration; } - sendPositiveResponse(server, client, msg); + sendResponse(server, client, msg); } m_isError = false; From aeedcedb528e9e76e8c184c65a4d7770b6d2efdd Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Thu, 2 Nov 2023 21:00:33 +0100 Subject: [PATCH 047/105] Doxygen configuration file updated to doxygen v1.9.5 --- doc/doxygen/Doxyfile | 177 +++++++++++++++++++++++++++++-------------- 1 file changed, 122 insertions(+), 55 deletions(-) diff --git a/doc/doxygen/Doxyfile b/doc/doxygen/Doxyfile index 154edbc9..53d944ea 100644 --- a/doc/doxygen/Doxyfile +++ b/doc/doxygen/Doxyfile @@ -1,4 +1,4 @@ -# Doxyfile 1.9.3 +# Doxyfile 1.9.5 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project. @@ -12,6 +12,16 @@ # For lists, items can also be appended using: # TAG += value [value, ...] # Values that contain spaces should be placed between quotes (\" \"). +# +# Note: +# +# Use doxygen to compare the used configuration file with the template +# configuration file: +# doxygen -x [configFile] +# Use doxygen to compare the used configuration file with the template +# configuration file without replacing the environment variables or CMake type +# replacement variables: +# doxygen -x_noenv [configFile] #--------------------------------------------------------------------------- # Project related configuration options @@ -60,16 +70,28 @@ PROJECT_LOGO = OUTPUT_DIRECTORY = . -# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- -# directories (in 2 levels) under the output directory of each output format and -# will distribute the generated files over these directories. Enabling this +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096 +# sub-directories (in 2 levels) under the output directory of each output format +# and will distribute the generated files over these directories. Enabling this # option can be useful when feeding doxygen a huge amount of source files, where # putting all generated files in the same directory would otherwise causes -# performance problems for the file system. +# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to +# control the number of sub-directories. # The default value is: NO. CREATE_SUBDIRS = NO +# Controls the number of sub-directories that will be created when +# CREATE_SUBDIRS tag is set to YES. Level 0 represents 16 directories, and every +# level increment doubles the number of directories, resulting in 4096 +# directories at level 8 which is the default and also the maximum value. The +# sub-directories are organized in 2 levels, the first level always has a fixed +# numer of 16 directories. +# Minimum value: 0, maximum value: 8, default value: 8. +# This tag requires that the tag CREATE_SUBDIRS is set to YES. + +CREATE_SUBDIRS_LEVEL = 8 + # If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII # characters to appear in the names of generated files. If set to NO, non-ASCII # characters will be escaped, for example _xE3_x81_x84 will be used for Unicode @@ -81,14 +103,14 @@ ALLOW_UNICODE_NAMES = NO # The OUTPUT_LANGUAGE tag is used to specify the language in which all # documentation generated by doxygen is written. Doxygen will use this # information to generate all constant output in the proper language. -# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, -# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), -# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, -# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), -# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, -# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, -# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, -# Ukrainian and Vietnamese. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian, +# Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English +# (United States), Esperanto, Farsi (Persian), Finnish, French, German, Greek, +# Hindi, Hungarian, Indonesian, Italian, Japanese, Japanese-en (Japanese with +# English messages), Korean, Korean-en (Korean with English messages), Latvian, +# Lithuanian, Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, +# Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, +# Swedish, Turkish, Ukrainian and Vietnamese. # The default value is: English. OUTPUT_LANGUAGE = English @@ -452,7 +474,7 @@ TYPEDEF_HIDES_STRUCT = NO LOOKUP_CACHE_SIZE = 0 -# The NUM_PROC_THREADS specifies the number threads doxygen is allowed to use +# The NUM_PROC_THREADS specifies the number of threads doxygen is allowed to use # during processing. When set to 0 doxygen will based this on the number of # cores available in the system. You can set it explicitly to a value larger # than 0 to get more control over the balance between CPU load and processing @@ -577,14 +599,15 @@ INTERNAL_DOCS = NO # filesystem is case sensitive (i.e. it supports files in the same directory # whose names only differ in casing), the option must be set to YES to properly # deal with such files in case they appear in the input. For filesystems that -# are not case sensitive the option should be be set to NO to properly deal with +# are not case sensitive the option should be set to NO to properly deal with # output files written for symbols that only differ in casing, such as for two # classes, one named CLASS and the other named Class, and to also support # references to files without having to specify the exact matching casing. On # Windows (including Cygwin) and MacOS, users should typically set this option # to NO, whereas on Linux or other Unix flavors it should typically be set to # YES. -# The default value is: system dependent. +# Possible values are: SYSTEM, NO and YES. +# The default value is: SYSTEM. CASE_SENSE_NAMES = NO @@ -851,10 +874,21 @@ WARN_AS_ERROR = NO # and the warning text. Optionally the format may contain $version, which will # be replaced by the version of the file (if it could be obtained via # FILE_VERSION_FILTER) +# See also: WARN_LINE_FORMAT # The default value is: $file:$line: $text. WARN_FORMAT = "$file:$line: $text" +# In the $text part of the WARN_FORMAT command it is possible that a reference +# to a more specific place is given. To make it easier to jump to this place +# (outside of doxygen) the user can define a custom "cut" / "paste" string. +# Example: +# WARN_LINE_FORMAT = "'vi $file +$line'" +# See also: WARN_FORMAT +# The default value is: at line $line of file $file. + +WARN_LINE_FORMAT = "at line $line of file $file" + # The WARN_LOGFILE tag can be used to specify a file to which warning and error # messages should be written. If left blank the output is written to standard # error (stderr). In case the file specified cannot be opened for writing the @@ -895,10 +929,21 @@ INPUT = . \ # libiconv (or the iconv built into libc) for the transcoding. See the libiconv # documentation (see: # https://www.gnu.org/software/libiconv/) for the list of possible encodings. +# See also: INPUT_FILE_ENCODING # The default value is: UTF-8. INPUT_ENCODING = UTF-8 +# This tag can be used to specify the character encoding of the source files +# that doxygen parses The INPUT_FILE_ENCODING tag can be used to specify +# character encoding on a per file pattern basis. Doxygen will compare the file +# name with each pattern and apply the encoding instead of the default +# INPUT_ENCODING) if there is a match. The character encodings are a list of the +# form: pattern=encoding (like *.php=ISO-8859-1). See cfg_input_encoding +# "INPUT_ENCODING" for further information on supported encodings. + +INPUT_FILE_ENCODING = + # If the value of the INPUT tag contains directories, you can use the # FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and # *.h) to filter out the source-files in the directories. @@ -1045,6 +1090,11 @@ IMAGE_PATH = # code is scanned, but not when the output code is generated. If lines are added # or removed, the anchors will not be placed correctly. # +# Note that doxygen will use the data processed and written to standard output +# for further processing, therefore nothing else, like debug statements or used +# commands (so in case of a Windows batch file always use @echo OFF), should be +# written to standard output. +# # Note that for custom extensions or not directly supported extensions you also # need to set EXTENSION_MAPPING for the extension otherwise the files are not # properly processed by doxygen. @@ -1086,6 +1136,15 @@ FILTER_SOURCE_PATTERNS = USE_MDFILE_AS_MAINPAGE = +# The Fortran standard specifies that for fixed formatted Fortran code all +# characters from position 72 are to be considered as comment. A common +# extension is to allow longer lines before the automatic comment starts. The +# setting FORTRAN_COMMENT_AFTER will also make it possible that longer lines can +# be processed before the automatic comment starts. +# Minimum value: 7, maximum value: 10000, default value: 72. + +FORTRAN_COMMENT_AFTER = 72 + #--------------------------------------------------------------------------- # Configuration options related to source browsing #--------------------------------------------------------------------------- @@ -1320,6 +1379,23 @@ HTML_EXTRA_STYLESHEET = HTML_EXTRA_FILES = +# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output +# should be rendered with a dark or light theme. Default setting AUTO_LIGHT +# enables light output unless the user preference is dark output. Other options +# are DARK to always use dark mode, LIGHT to always use light mode, AUTO_DARK to +# default to dark mode unless the user prefers light mode, and TOGGLE to let the +# user toggle between dark and light mode via a button. +# Possible values are: LIGHT Always generate light output., DARK Always generate +# dark output., AUTO_LIGHT Automatically set the mode according to the user +# preference, use light mode if no preference is set (the default)., AUTO_DARK +# Automatically set the mode according to the user preference, use dark mode if +# no preference is set. and TOGGLE Allow to user to switch between light and +# dark mode via a button.. +# The default value is: AUTO_LIGHT. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE = AUTO_LIGHT + # The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen # will adjust the colors in the style sheet and background images according to # this color. Hue is specified as an angle on a color-wheel, see @@ -1683,17 +1759,6 @@ HTML_FORMULA_FORMAT = png FORMULA_FONTSIZE = 10 -# Use the FORMULA_TRANSPARENT tag to determine whether or not the images -# generated for formulas are transparent PNGs. Transparent PNGs are not -# supported properly for IE 6.0, but are supported on all modern browsers. -# -# Note that when changing this option you need to delete any form_*.png files in -# the HTML output directory before the changes have effect. -# The default value is: YES. -# This tag requires that the tag GENERATE_HTML is set to YES. - -FORMULA_TRANSPARENT = YES - # The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands # to create new LaTeX commands to be used in formulas as building blocks. See # the section "Including formulas" for details. @@ -2284,7 +2349,8 @@ SEARCH_INCLUDES = YES # The INCLUDE_PATH tag can be used to specify one or more directories that # contain include files that are not input files but should be processed by the -# preprocessor. +# preprocessor. Note that the INCLUDE_PATH is not recursive, so the setting of +# RECURSIVE has no effect here. # This tag requires that the tag SEARCH_INCLUDES is set to YES. INCLUDE_PATH = @@ -2408,26 +2474,38 @@ HAVE_DOT = YES DOT_NUM_THREADS = 0 -# When you want a differently looking font in the dot files that doxygen -# generates you can specify the font name using DOT_FONTNAME. You need to make -# sure dot is able to find the font, which can be done by putting it in a -# standard location or by setting the DOTFONTPATH environment variable or by -# setting DOT_FONTPATH to the directory containing the font. -# The default value is: Helvetica. +# DOT_COMMON_ATTR is common attributes for nodes, edges and labels of +# subgraphs. When you want a differently looking font in the dot files that +# doxygen generates you can specify fontname, fontcolor and fontsize attributes. +# For details please see Node, +# Edge and Graph Attributes specification You need to make sure dot is able +# to find the font, which can be done by putting it in a standard location or by +# setting the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the +# directory containing the font. Default graphviz fontsize is 14. +# The default value is: fontname=Helvetica,fontsize=10. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_COMMON_ATTR = "fontname=Helvetica,fontsize=10" + +# DOT_EDGE_ATTR is concatenated with DOT_COMMON_ATTR. For elegant style you can +# add 'arrowhead=open, arrowtail=open, arrowsize=0.5'. Complete documentation about +# arrows shapes. +# The default value is: labelfontname=Helvetica,labelfontsize=10. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_FONTNAME = Helvetica +DOT_EDGE_ATTR = "labelfontname=Helvetica,labelfontsize=10" -# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of -# dot graphs. -# Minimum value: 4, maximum value: 24, default value: 10. +# DOT_NODE_ATTR is concatenated with DOT_COMMON_ATTR. For view without boxes +# around nodes set 'shape=plain' or 'shape=plaintext' Shapes specification +# The default value is: shape=box,height=0.2,width=0.4. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_FONTSIZE = 10 +DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4" -# By default doxygen will tell dot to use the default font as specified with -# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set -# the path where dot can find it using this tag. +# You can set the path where dot can find font specified with fontname in +# DOT_COMMON_ATTR and others dot attributes. # This tag requires that the tag HAVE_DOT is set to YES. DOT_FONTPATH = @@ -2453,7 +2531,8 @@ CLASS_GRAPH = YES COLLABORATION_GRAPH = YES # If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for -# groups, showing the direct groups dependencies. +# groups, showing the direct groups dependencies. See also the chapter Grouping +# in the manual. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2669,18 +2748,6 @@ DOT_GRAPH_MAX_NODES = 50 MAX_DOT_GRAPH_DEPTH = 0 -# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent -# background. This is disabled by default, because dot on Windows does not seem -# to support this out of the box. -# -# Warning: Depending on the platform used, enabling this option may lead to -# badly anti-aliased labels on the edges of a graph (i.e. they become hard to -# read). -# The default value is: NO. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_TRANSPARENT = NO - # Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output # files in one run (i.e. multiple -o and -T options on the command line). This # makes dot run faster, but since only newer versions of dot (>1.8.10) support From 68c29c5f438586802a15fe296ea38ac0cf2325ed Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Fri, 3 Nov 2023 19:07:55 +0100 Subject: [PATCH 048/105] To switch the device off was replaced with switching the display on/off. #149 --- README.md | 6 +- config/display.ini | 1 + doc/architecture/uml/system_state_machine.wsd | 11 -- lib/Common/src/IDisplay.hpp | 17 ++ lib/HalLedMatrix/src/Display.cpp | 43 ++++- lib/HalLedMatrix/src/Display.h | 42 ++--- lib/HalTftDisplay/src/Board.cpp | 6 +- lib/HalTftDisplay/src/Board.h | 6 + lib/HalTftDisplay/src/Display.cpp | 72 +++++++- lib/HalTftDisplay/src/Display.h | 46 +++-- src/Common/ButtonActions.cpp | 17 +- src/Common/ButtonActions.h | 6 +- src/Common/ButtonHandler.hpp | 10 -- src/Common/OneButtonCtrl.hpp | 2 +- src/Common/ThreeButtonCtrl.hpp | 2 +- src/Common/TwoButtonCtrl.hpp | 2 +- src/Gfx/DisplayMgr.cpp | 27 +++ src/Gfx/DisplayMgr.h | 19 ++- src/StateMachine/OffState.cpp | 161 ------------------ src/StateMachine/OffState.h | 125 -------------- src/main.cpp | 8 - 21 files changed, 252 insertions(+), 377 deletions(-) delete mode 100644 src/StateMachine/OffState.cpp delete mode 100644 src/StateMachine/OffState.h diff --git a/README.md b/README.md index dcc3594b..22220826 100644 --- a/README.md +++ b/README.md @@ -143,12 +143,12 @@ For changing whats displayed, go to its web interface. Use the same credentials * 2 short pulses: Activates the previous slot. * 3 short pulses: Activates next fade effect. * 4 short pulses: IP address is shown. - * 5 short pulses: Switch device off. + * 5 short pulses: Toggle display power on/off. * Long pressed: Increases the display brightness until maximum and then decreases until minimum. After that it will again increases it and so on. * Two button control (LILYGO® T-Display ESP32-S3): * Left button: * 1 short pulses: Activates the previous slot. - * 2 short pulses: Switch device off. + * 2 short pulses: Toggle display power on/off. * Long pressed: Decreases the display brightness until minimum. * Right button * 1 short pulse: Activates the next slot. @@ -162,7 +162,7 @@ For changing whats displayed, go to its web interface. Use the same credentials * Ok button: * 1 short pulses: Activates next fade effect. * 2 short pulses: IP address is shown. - * Long pressed: Switch device off. + * Long pressed: Toggle display power on/off. * Right button * 1 short pulse: Activates the next slot. * Long pressed: Increases the display brightness until maximum. diff --git a/config/display.ini b/config/display.ini index e7294a25..b7578261 100644 --- a/config/display.ini +++ b/config/display.ini @@ -88,6 +88,7 @@ build_flags = -D TFT_RD=9 -D TFT_DATA_PIN_OFFSET_EN -D TFT_BL=38 + -D TFT_BACKLIGHT_ON=HIGH -D TFT_D0=39 -D TFT_D1=40 -D TFT_D2=41 diff --git a/doc/architecture/uml/system_state_machine.wsd b/doc/architecture/uml/system_state_machine.wsd index 2187da21..ba04fb1b 100644 --- a/doc/architecture/uml/system_state_machine.wsd +++ b/doc/architecture/uml/system_state_machine.wsd @@ -29,34 +29,23 @@ ErrorState: /do: Wait for manual reset. RestartState: /entry: Unmount filesystem RestartState: /do: Restart -OffState: /entry: Set user button as wakeup source -OffState: /do: Clear display -OffState: /do: Enter sleep mode - [*] --> InitState: Power up InitState --> APState: [Button pressed]\nSet wifi access point mode InitState -> ConnectingState: [Button released]\nSet wifi station mode APState --> ErrorState: [Failed to start\nwifi access point] APState --> RestartState: [Update received] -APState --> OffState: [Switch off requested] ConnectingState --> IdleState: [Remote wifi SSID or\npassword missing] ConnectingState --> ConnectedState: [Connection successful established] -ConnectingState --> OffState: [Switch off requested] ConnectedState --> ConnectingState: [Connection lost] ConnectedState --> RestartState: [Update finished] -ConnectedState --> OffState: [Switch off requested] ErrorState --> [*]: [Restart] -ErrorState --> OffState: [Switch off requested] IdleState --> [*]: [Restart] -IdleState --> OffState: [Switch off requested] RestartState --> [*]: [Restart] -OffState --> RestartState: [Wakeup] - @enduml \ No newline at end of file diff --git a/lib/Common/src/IDisplay.hpp b/lib/Common/src/IDisplay.hpp index 6e829634..9cd97168 100644 --- a/lib/Common/src/IDisplay.hpp +++ b/lib/Common/src/IDisplay.hpp @@ -101,6 +101,23 @@ class IDisplay : public YAGfx */ virtual void clear() = 0; + /** + * Power display off. + */ + virtual void off() = 0; + + /** + * Power display on. + */ + virtual void on() = 0; + + /** + * Is display powered on? + * + * @return If display is powered on, it will return true otherwise false. + */ + virtual bool isOn() const = 0; + protected: /** diff --git a/lib/HalLedMatrix/src/Display.cpp b/lib/HalLedMatrix/src/Display.cpp index 6293454d..8c7957fe 100644 --- a/lib/HalLedMatrix/src/Display.cpp +++ b/lib/HalLedMatrix/src/Display.cpp @@ -70,7 +70,8 @@ Display::Display() : IDisplay(), m_strip(Board::LedMatrix::width * Board::LedMatrix::height, Board::Pin::ledMatrixDataOutPinNo), m_topo(Board::LedMatrix::width, Board::LedMatrix::height), - m_ledMatrix() + m_ledMatrix(), + m_isOn(true) { } @@ -78,6 +79,46 @@ Display::~Display() { } +void Display::show() +{ + if (true == m_isOn) + { + int16_t x = 0; + int16_t y = 0; + + for(y = 0; y < m_ledMatrix.getHeight(); ++y) + { + for(x = 0; x < m_ledMatrix.getWidth(); ++x) + { + HtmlColor htmlColor = static_cast(m_ledMatrix.getColor(x, y)); + + m_strip.SetPixelColor(m_topo.Map(x, y), htmlColor); + } + } + + m_strip.Show(); + } +} + +void Display::off() +{ + m_isOn = false; + + /* Simulate powered off display. */ + m_strip.ClearTo(ColorDef::BLACK); + m_strip.Show(); +} + +void Display::on() +{ + m_isOn = true; +} + +bool Display::isOn() const +{ + return m_isOn; +} + /****************************************************************************** * External Functions *****************************************************************************/ diff --git a/lib/HalLedMatrix/src/Display.h b/lib/HalLedMatrix/src/Display.h index eadae48b..ae9b336b 100644 --- a/lib/HalLedMatrix/src/Display.h +++ b/lib/HalLedMatrix/src/Display.h @@ -95,24 +95,7 @@ class Display : public IDisplay * Show framebuffer on physical display. This may be synchronous * or asynchronous. */ - void show() final - { - int16_t x = 0; - int16_t y = 0; - - for(y = 0; y < m_ledMatrix.getHeight(); ++y) - { - for(x = 0; x < m_ledMatrix.getWidth(); ++x) - { - HtmlColor htmlColor = static_cast(m_ledMatrix.getColor(x, y)); - - m_strip.SetPixelColor(m_topo.Map(x, y), htmlColor); - } - } - - m_strip.Show(); - return; - } + void show() final; /** * The display is ready, when the last physical pixel update is finished. @@ -140,7 +123,6 @@ class Display : public IDisplay (Board::LedMatrix::maxCurrentPerLed * Board::LedMatrix::width *Board::LedMatrix::height); m_strip.SetLuminance(SAFE_LUMINANCE); - return; } /** @@ -198,6 +180,23 @@ class Display : public IDisplay return m_ledMatrix.getColor(x, y); } + /** + * Power display off. + */ + void off() final; + + /** + * Power display on. + */ + void on() final; + + /** + * Is display powered on? + * + * @return If display is powered on, it will return true otherwise false. + */ + bool isOn() const final; + private: /** @@ -214,6 +213,11 @@ class Display : public IDisplay */ YAGfxStaticBitmap m_ledMatrix; + /** + * Is display on? + */ + bool m_isOn; + /** * Construct display. */ diff --git a/lib/HalTftDisplay/src/Board.cpp b/lib/HalTftDisplay/src/Board.cpp index 6b510e8d..2e35be68 100644 --- a/lib/HalTftDisplay/src/Board.cpp +++ b/lib/HalTftDisplay/src/Board.cpp @@ -83,6 +83,9 @@ const AnalogPin Board::batteryVoltageIn; /** Digital output pin: Buzzer */ const DOutPin Board::buzzerOut; +/** Digital output pin: TFT display backlight switch */ +const DOutPin Board::tftBackLightOut; + /****************************************************************************** * Local Variables *****************************************************************************/ @@ -97,7 +100,8 @@ static const IoPin* ioPinList[] = &ldrIn, &dhtIn, &batteryVoltageIn, - &buzzerOut + &buzzerOut, + &tftBackLightOut }; /****************************************************************************** diff --git a/lib/HalTftDisplay/src/Board.h b/lib/HalTftDisplay/src/Board.h index 7a01d571..2c916ba3 100644 --- a/lib/HalTftDisplay/src/Board.h +++ b/lib/HalTftDisplay/src/Board.h @@ -105,6 +105,9 @@ namespace Pin /** Pin number of buzzer out */ constexpr uint8_t buzzerOutPinNo = CONFIG_PIN_BUZZER_OUT; + + /** Pin number of TFT display backlight LED switch. */ + constexpr uint8_t tftBackLightPinNo = TFT_BL; }; /* Digital output pin: Onboard LED */ @@ -134,6 +137,9 @@ extern const AnalogPin batteryVoltageIn; /* Digital output pin: Buzzer */ extern const DOutPin buzzerOut; +/* Digital output pin: TFT display backlight switch */ +extern const DOutPin tftBackLightOut; + /** ADC resolution in digits */ constexpr uint16_t adcResolution = 4096U; diff --git a/lib/HalTftDisplay/src/Display.cpp b/lib/HalTftDisplay/src/Display.cpp index 6a91ca0d..39e5bdb7 100644 --- a/lib/HalTftDisplay/src/Display.cpp +++ b/lib/HalTftDisplay/src/Display.cpp @@ -70,7 +70,8 @@ Display::Display() : IDisplay(), m_tft(), m_ledMatrix(), - m_brightness(DEFAULT_BRIGHTNESS) + m_brightness(DEFAULT_BRIGHTNESS), + m_isOn(false) { } @@ -78,6 +79,75 @@ Display::~Display() { } +void Display::show() +{ + int32_t x = 0; + int32_t y = 0; + + for(y = 0; y < MATRIX_HEIGHT; ++y) + { + for(x = 0; x < MATRIX_WIDTH; ++x) + { + Color brightnessAdjustedColor = m_ledMatrix.getColor(x, y); + uint16_t intensity = brightnessAdjustedColor.getIntensity(); + int32_t xNative = y * (PIXEL_HEIGHT + PiXEL_DISTANCE) + BORDER_Y; + int32_t yNative = TFT_HEIGHT - (x * (PIXEL_WIDTH + PiXEL_DISTANCE) + BORDER_X) - 1; + + intensity *= (static_cast(m_brightness) + 1U); + intensity /= 256U; + brightnessAdjustedColor.setIntensity(static_cast(intensity)); + + m_tft.fillRect( xNative, + yNative, + PIXEL_HEIGHT, + PIXEL_WIDTH, + brightnessAdjustedColor.to565()); + } + } +} + +void Display::off() +{ + m_tft.writecommand(TFT_DISPOFF); + +#if defined (TFT_BL) && defined (TFT_BACKLIGHT_ON) + +#if (LOW == TFT_BACKLIGHT_ON) + + /* Turn off the back-light LED */ + Board::tftBackLightOut.write(HIGH); + +#else /* (LOW == TFT_BACKLIGHT_ON) */ + + /* Turn off the back-light LED */ + Board::tftBackLightOut.write(LOW); + +#endif /* (LOW == TFT_BACKLIGHT_ON) */ + +#endif /* defined (TFT_BL) && defined (TFT_BACKLIGHT_ON) */ + + m_isOn = false; +} + +void Display::on() +{ + m_tft.writecommand(TFT_DISPON); + +#if defined (TFT_BL) && defined (TFT_BACKLIGHT_ON) + + /* Turn off the back-light LED */ + Board::tftBackLightOut.write(TFT_BACKLIGHT_ON); + +#endif /* defined (TFT_BL) && defined (TFT_BACKLIGHT_ON) */ + + m_isOn = true; +} + +bool Display::isOn() const +{ + return m_isOn; +} + /****************************************************************************** * External Functions *****************************************************************************/ diff --git a/lib/HalTftDisplay/src/Display.h b/lib/HalTftDisplay/src/Display.h index 2fc19885..a01b9f45 100644 --- a/lib/HalTftDisplay/src/Display.h +++ b/lib/HalTftDisplay/src/Display.h @@ -87,6 +87,7 @@ class Display : public IDisplay { m_tft.init(); m_tft.fillScreen(TFT_BLACK); + m_isOn = true; return true; } @@ -95,32 +96,7 @@ class Display : public IDisplay * Show framebuffer on physical display. This may be synchronous * or asynchronous. */ - void show() final - { - int32_t x = 0; - int32_t y = 0; - - for(y = 0; y < MATRIX_HEIGHT; ++y) - { - for(x = 0; x < MATRIX_WIDTH; ++x) - { - Color brightnessAdjustedColor = m_ledMatrix.getColor(x, y); - uint16_t intensity = brightnessAdjustedColor.getIntensity(); - int32_t xNative = y * (PIXEL_HEIGHT + PiXEL_DISTANCE) + BORDER_Y; - int32_t yNative = TFT_HEIGHT - (x * (PIXEL_WIDTH + PiXEL_DISTANCE) + BORDER_X) - 1; - - intensity *= (static_cast(m_brightness) + 1U); - intensity /= 256U; - brightnessAdjustedColor.setIntensity(static_cast(intensity)); - - m_tft.fillRect( xNative, - yNative, - PIXEL_HEIGHT, - PIXEL_WIDTH, - brightnessAdjustedColor.to565()); - } - } - } + void show() final; /** * The display is ready, when the last physical pixel update is finished. @@ -199,6 +175,23 @@ class Display : public IDisplay return m_ledMatrix.getColor(x, y); } + /** + * Power display off. + */ + void off() final; + + /** + * Power display on. + */ + void on() final; + + /** + * Is display powered on? + * + * @return If display is powered on, it will return true otherwise false. + */ + bool isOn() const final; + private: /* The below TFT_* definitions are set in platform.ini build_flags */ @@ -230,6 +223,7 @@ class Display : public IDisplay TFT_eSPI m_tft; /**< T-Display driver */ YAGfxStaticBitmap m_ledMatrix; /**< Simulated LED matrix framebuffer */ uint8_t m_brightness; /**< Display brightness [0; 255] value. 255 = max. brightness. */ + bool m_isOn; /**< Is display on? */ /** * Construct display. diff --git a/src/Common/ButtonActions.cpp b/src/Common/ButtonActions.cpp index 89834790..7c01f02c 100644 --- a/src/Common/ButtonActions.cpp +++ b/src/Common/ButtonActions.cpp @@ -90,8 +90,8 @@ void ButtonActions::executeAction(ButtonActionId id) showIpAddress(); break; - case BUTTON_ACTION_ID_SWITCH_OFF: - switchOff(); + case BUTTON_ACTION_ID_TOGGLE_DISPLAY_OFF_ON: + toggleDisplayOffOn(); break; case BUTTON_ACTION_ID_SWEEP_BRIGHTNESS: @@ -234,9 +234,18 @@ void ButtonActions::showIpAddress() const SysMsg::getInstance().show(infoStr, DURATION_NON_SCROLLING, SCROLLING_REPEAT_NUM); } -void ButtonActions::switchOff() +void ButtonActions::toggleDisplayOffOn() { - m_isSwitchOffRequested = true; + DisplayMgr& displayMgr = DisplayMgr::getInstance(); + + if (false == displayMgr.isDisplayOn()) + { + displayMgr.displayOn(); + } + else + { + displayMgr.displayOff(); + } } /****************************************************************************** diff --git a/src/Common/ButtonActions.h b/src/Common/ButtonActions.h index 3158c75a..71e7efe7 100644 --- a/src/Common/ButtonActions.h +++ b/src/Common/ButtonActions.h @@ -63,7 +63,7 @@ enum ButtonActionId BUTTON_ACTION_ID_ACTIVATE_PREV_SLOT, /**< Activate previous slot */ BUTTON_ACTION_ID_NEXT_FADE_EFFECT, /**< Select next fade effect */ BUTTON_ACTION_ID_SHOW_IP_ADDRESS, /**< Show IP address on display */ - BUTTON_ACTION_ID_SWITCH_OFF, /**< Switch device off */ + BUTTON_ACTION_ID_TOGGLE_DISPLAY_OFF_ON, /**< Toggle the display off/on */ BUTTON_ACTION_ID_SWEEP_BRIGHTNESS, /**< Sweep brightness from dark to bright and reverse */ BUTTON_ACTION_ID_INC_BRIGHTNESS, /**< Increase display brightness till maximum. */ BUTTON_ACTION_ID_DEC_BRIGHTNESS /**< Decrease display brightness till minimum. */ @@ -160,9 +160,9 @@ class ButtonActions void showIpAddress() const; /** - * Trigger action: Switch device off. + * Trigger action: Toggle display on/off. */ - void switchOff(); + void toggleDisplayOffOn(); }; /****************************************************************************** diff --git a/src/Common/ButtonHandler.hpp b/src/Common/ButtonHandler.hpp index 1948df87..b3d39c33 100644 --- a/src/Common/ButtonHandler.hpp +++ b/src/Common/ButtonHandler.hpp @@ -191,16 +191,6 @@ class ButtonHandler : public IButtonObserver, private ButtonCtrl, private Button } } - /** - * Is switch off requested? - * - * @return If switch off is requested, it will return true otherwise false. - */ - bool isSwitchOffRequested() - { - return ButtonActions::isSwitchOffRequested(); - } - private: /** Length of the button info queue. */ diff --git a/src/Common/OneButtonCtrl.hpp b/src/Common/OneButtonCtrl.hpp index 42ae6247..93e9e97e 100644 --- a/src/Common/OneButtonCtrl.hpp +++ b/src/Common/OneButtonCtrl.hpp @@ -119,7 +119,7 @@ class OneButtonCtrl /* 2 */ BUTTON_ACTION_ID_ACTIVATE_PREV_SLOT, /* 3 */ BUTTON_ACTION_ID_NEXT_FADE_EFFECT, /* 4 */ BUTTON_ACTION_ID_SHOW_IP_ADDRESS, - /* 5 */ BUTTON_ACTION_ID_SWITCH_OFF + /* 5 */ BUTTON_ACTION_ID_TOGGLE_DISPLAY_OFF_ON }; const size_t TABLE_NUM_ELEMENTS = sizeof(ACTION_TABLE) / sizeof(ACTION_TABLE[0]); diff --git a/src/Common/ThreeButtonCtrl.hpp b/src/Common/ThreeButtonCtrl.hpp index 54250f70..27aa9eea 100644 --- a/src/Common/ThreeButtonCtrl.hpp +++ b/src/Common/ThreeButtonCtrl.hpp @@ -114,7 +114,7 @@ class ThreeButtonCtrl } else if (tButtonOk == buttonId) { - action = BUTTON_ACTION_ID_SWITCH_OFF; + action = BUTTON_ACTION_ID_TOGGLE_DISPLAY_OFF_ON; } else if (tButtonRight == buttonId) { diff --git a/src/Common/TwoButtonCtrl.hpp b/src/Common/TwoButtonCtrl.hpp index f8158d2d..2a7b2ca9 100644 --- a/src/Common/TwoButtonCtrl.hpp +++ b/src/Common/TwoButtonCtrl.hpp @@ -133,7 +133,7 @@ class TwoButtonCtrl { /* 0 */ BUTTON_ACTION_ID_NO_ACTION, /* 1 */ BUTTON_ACTION_ID_ACTIVATE_PREV_SLOT, - /* 2 */ BUTTON_ACTION_ID_SWITCH_OFF + /* 2 */ BUTTON_ACTION_ID_TOGGLE_DISPLAY_OFF_ON }; const size_t TABLE_NUM_ELEMENTS = sizeof(ACTION_TABLE) / sizeof(ACTION_TABLE[0]); diff --git a/src/Gfx/DisplayMgr.cpp b/src/Gfx/DisplayMgr.cpp index 645bdd30..e1c148a3 100644 --- a/src/Gfx/DisplayMgr.cpp +++ b/src/Gfx/DisplayMgr.cpp @@ -714,6 +714,33 @@ void DisplayMgr::setNetworkStatus(bool isConnected) m_isNetworkConnected = isConnected; } +void DisplayMgr::displayOff() +{ + MutexGuard guard1(m_mutexInterf); + MutexGuard guard2(m_mutexUpdate); + + Display::getInstance().off(); +} + +void DisplayMgr::displayOn() +{ + MutexGuard guard1(m_mutexInterf); + MutexGuard guard2(m_mutexUpdate); + + Display::getInstance().on(); +} + +bool DisplayMgr::isDisplayOn() const +{ + bool isDisplayOn = false; + MutexGuard guard1(m_mutexInterf); + MutexGuard guard2(m_mutexUpdate); + + isDisplayOn = Display::getInstance().isOn(); + + return isDisplayOn; +} + /****************************************************************************** * Protected Methods *****************************************************************************/ diff --git a/src/Gfx/DisplayMgr.h b/src/Gfx/DisplayMgr.h index 55c45821..aeff8876 100644 --- a/src/Gfx/DisplayMgr.h +++ b/src/Gfx/DisplayMgr.h @@ -329,6 +329,23 @@ class DisplayMgr */ void setNetworkStatus(bool isConnected); + /** + * Power display off. + */ + void displayOff(); + + /** + * Power display on. + */ + void displayOn(); + + /** + * Is the display powered on? + * + * @return If the display is powered on, it will return true otherwise false. + */ + bool isDisplayOn() const; + private: /** The process task stack size in bytes */ @@ -359,7 +376,7 @@ class DisplayMgr mutable MutexRecursive m_mutexInterf; /** Mutex to protect the display update against concurrent access. */ - MutexRecursive m_mutexUpdate; + mutable MutexRecursive m_mutexUpdate; /** Process task handle */ TaskHandle_t m_processTaskHandle; diff --git a/src/StateMachine/OffState.cpp b/src/StateMachine/OffState.cpp deleted file mode 100644 index b0397db2..00000000 --- a/src/StateMachine/OffState.cpp +++ /dev/null @@ -1,161 +0,0 @@ -/* MIT License - * - * Copyright (c) 2019 - 2023 Andreas Merkle - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -/******************************************************************************* - DESCRIPTION -*******************************************************************************/ -/** - * @brief System state: Off - * @author Andreas Merkle - */ - -/****************************************************************************** - * Includes - *****************************************************************************/ -#include "OffState.h" -#include "RestartState.h" -#include "DisplayMgr.h" -#include "Display.h" -#include "ButtonDrv.h" - -#include -#include -#include -#include -#include - -#if 0 -#include -#include -#endif - -/****************************************************************************** - * Compiler Switches - *****************************************************************************/ - -/****************************************************************************** - * Macros - *****************************************************************************/ - -/****************************************************************************** - * Types and classes - *****************************************************************************/ - -/****************************************************************************** - * Prototypes - *****************************************************************************/ - -/****************************************************************************** - * Local Variables - *****************************************************************************/ - -/****************************************************************************** - * Public Methods - *****************************************************************************/ - -void OffState::entry(StateMachine& sm) -{ - UTIL_NOT_USED(sm); - - LOG_INFO("Going in off state."); - - /* Before entering light sleep mode, WiFi and BT must be disabled by using - * appropriate calls (esp_bluedroid_disable(), esp_bt_controller_disable(), esp_wifi_stop()). - * WiFi and BT connections will not be maintained in deep sleep or light sleep, - * even if these functions are not called. - */ - (void)esp_wifi_stop(); -#if 0 - (void)esp_bluedroid_disable(); - (void)esp_bt_controller_disable(); -#endif - - /* Stop display manager and clear the display to minimize power consumption. - * Additional clearing will show the user that he can stop pressing the "off" - * button. - */ - DisplayMgr::getInstance().end(); - Display::getInstance().clear(); - Display::getInstance().show(); - - /* Wait until the LED matrix is updated to avoid artifacts on the - * display. - */ - while(false == Display::getInstance().isReady()) - { - /* Just wait and give other tasks a chance. */ - delay(1U); - } -} - -void OffState::process(StateMachine& sm) -{ - UTIL_NOT_USED(sm); - - /* Prepare wakeup sources. - * Use all available buttons as wakeup sources. - */ - if (true == ButtonDrv::getInstance().enableWakeUpSources()) - { - esp_sleep_wakeup_cause_t wakeupCause = ESP_SLEEP_WAKEUP_UNDEFINED; - - while(ESP_SLEEP_WAKEUP_GPIO != wakeupCause) - { - /* Enter sleep mode. The function will return by wakeup. */ - if (ESP_OK != esp_light_sleep_start()) - { - LOG_ERROR("Enter light sleep mode not possible."); - } - else - { - wakeupCause = esp_sleep_get_wakeup_cause(); - } - } - - /* Restart the device. */ - sm.setState(RestartState::getInstance()); - } -} - -void OffState::exit(StateMachine& sm) -{ - UTIL_NOT_USED(sm); - - /* Nothing to do. */ -} - -/****************************************************************************** - * Protected Methods - *****************************************************************************/ - -/****************************************************************************** - * Private Methods - *****************************************************************************/ - -/****************************************************************************** - * External Functions - *****************************************************************************/ - -/****************************************************************************** - * Local Functions - *****************************************************************************/ diff --git a/src/StateMachine/OffState.h b/src/StateMachine/OffState.h deleted file mode 100644 index 47f667dd..00000000 --- a/src/StateMachine/OffState.h +++ /dev/null @@ -1,125 +0,0 @@ -/* MIT License - * - * Copyright (c) 2019 - 2023 Andreas Merkle - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -/******************************************************************************* - DESCRIPTION -*******************************************************************************/ -/** - * @brief System state: Off - * @author Andreas Merkle - * - * @addtogroup sys_states - * - * @{ - */ - -#ifndef OFFSTATE_H -#define OFFSTATE_H - -/****************************************************************************** - * Compile Switches - *****************************************************************************/ - -/****************************************************************************** - * Includes - *****************************************************************************/ -#include -#include -#include - -/****************************************************************************** - * Macros - *****************************************************************************/ - -/****************************************************************************** - * Types and Classes - *****************************************************************************/ - -/** - * System state: Off - */ -class OffState : public AbstractState -{ -public: - - /** - * Get state instance. - * - * @return State instance - */ - static OffState& getInstance() - { - static OffState instance; /* singleton idiom to force initialization in the first usage. */ - - return instance; - } - - /** - * The entry is called once, a state is entered. - * - * @param[in] sm Responsible state machine - */ - void entry(StateMachine& sm) final; - - /** - * The process routine is called cyclic, as long as the state is active. - * - * @param[in] sm Responsible state machine - */ - void process(StateMachine& sm) final; - - /** - * The exit is called once, a state will be left. - * - * @param[in] sm Responsible state machine - */ - void exit(StateMachine& sm) final; - -private: - - /** - * Constructs the state. - */ - OffState() - { - } - - /** - * Destroys the state. - */ - ~OffState() - { - } - - OffState(const OffState& state); - OffState& operator=(const OffState& state); - -}; - -/****************************************************************************** - * Functions - *****************************************************************************/ - -#endif /* OFFSTATE_H */ - -/** @} */ \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 2f183b43..225cdc79 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -41,7 +41,6 @@ #include "InitState.h" #include "RestartState.h" -#include "OffState.h" #include "TaskMon.h" #include "MemMon.h" #include "ResetMon.h" @@ -222,19 +221,12 @@ void loop() /* Handle button actions only if * - No update is running. - * - Not in OffState. * - Not in RestartState. */ if ((false == UpdateMgr::getInstance().isUpdateRunning()) && - (&OffState::getInstance() != gSysStateMachine.getState()) && (&RestartState::getInstance() != gSysStateMachine.getState())) { gButtonHandler.process(); - - if (true == gButtonHandler.isSwitchOffRequested()) - { - gSysStateMachine.setState(OffState::getInstance()); - } } /* Schedule other tasks with same or lower priority. */ From 91506ad023c628ba3c1f531f688a79c4ab150cee Mon Sep 17 00:00:00 2001 From: Haju Schulz Date: Sun, 5 Nov 2023 13:52:18 +0100 Subject: [PATCH 049/105] Fix issue #148 Add option to rotate display Added common display section to display.ini. Add build option CONFIG_DISPLAY_ROTATE180=0 there. Use the option in Display::show() to rotate bitmap if set --- README.md | 10 ++++++++++ config/display.ini | 12 ++++++++++++ lib/HalLedMatrix/src/Display.cpp | 13 ++++++++----- lib/HalTftDisplay/src/Display.cpp | 6 +++++- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 22220826..38733f08 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,16 @@ To handle there are several .ini files in the ```./config``` folder: Update the one you use for your needs by commenting in or out. +## Is there an easy way to rotate the display by 180° ? I need to turn the display when putting it into a housing. +Change option CONFIG_DISPLAY_ROTATE180 in ```config/display.ini``` to 1 as shown below and rebuild. + +Example: +```ini +[display:common] +build_flags = + -D CONFIG_DISPLAY_ROTATE180=1 +``` + # Used Libraries | Library | Description | License | diff --git a/config/display.ini b/config/display.ini index b7578261..43e76018 100644 --- a/config/display.ini +++ b/config/display.ini @@ -1,8 +1,16 @@ +; ******************************************************************************** +; generic display config settings +; ******************************************************************************** +[display:common] +build_flags = + -D CONFIG_DISPLAY_ROTATE180=0 ; set to 1 to rotate display 180° + ; ******************************************************************************** ; LED matrix based on WS2812B (neopixels) ; ******************************************************************************** [display:led_matrix_column_major_alternating] build_flags = + ${display:common.build_flags} -D CONFIG_LED_MATRIX_WIDTH=32U -D CONFIG_LED_MATRIX_HEIGHT=8U -D CONFIG_LED_TOPO=ColumnMajorAlternatingLayout @@ -19,6 +27,7 @@ lib_ignore_external = ; ******************************************************************************** [display:led_matrix_row_major_alternating] build_flags = + ${display:common.build_flags} -D CONFIG_LED_MATRIX_WIDTH=32U -D CONFIG_LED_MATRIX_HEIGHT=8U -D CONFIG_LED_TOPO=RowMajorAlternatingLayout @@ -37,6 +46,7 @@ lib_ignore_external = ; ******************************************************************************** [display:lilygo_ttgo_tdisplay] build_flags = + ${display:common.build_flags} -D USER_SETUP_LOADED=1 -D ST7789_DRIVER -D TFT_WIDTH=135 @@ -73,6 +83,7 @@ lib_ignore_external = ; ******************************************************************************** [display:lilygo_tdisplay-s3] build_flags = + ${display:common.build_flags} -D USER_SETUP_LOADED=1 -D ST7789_DRIVER -D TFT_PARALLEL_8_BIT @@ -122,6 +133,7 @@ lib_ignore_external = ; ******************************************************************************** [display:m5stack_core] build_flags = + ${display:common.build_flags} -D USER_SETUP_LOADED=1 -D ILI9341_DRIVER -D M5STACK diff --git a/lib/HalLedMatrix/src/Display.cpp b/lib/HalLedMatrix/src/Display.cpp index 8c7957fe..1bbf01ff 100644 --- a/lib/HalLedMatrix/src/Display.cpp +++ b/lib/HalLedMatrix/src/Display.cpp @@ -83,16 +83,19 @@ void Display::show() { if (true == m_isOn) { - int16_t x = 0; - int16_t y = 0; + const int16_t height = m_ledMatrix.getHeight(); + const int16_t width = m_ledMatrix.getWidth(); - for(y = 0; y < m_ledMatrix.getHeight(); ++y) + for (int16_t y = 0; y < height; ++y) { - for(x = 0; x < m_ledMatrix.getWidth(); ++x) + for (int16_t x = 0; x < width; ++x) { HtmlColor htmlColor = static_cast(m_ledMatrix.getColor(x, y)); - +#if CONFIG_DISPLAY_ROTATE180 != 0 + m_strip.SetPixelColor(m_topo.Map(width - x - 1, height - y - 1), htmlColor); +#else m_strip.SetPixelColor(m_topo.Map(x, y), htmlColor); +#endif } } diff --git a/lib/HalTftDisplay/src/Display.cpp b/lib/HalTftDisplay/src/Display.cpp index 39e5bdb7..f27db236 100644 --- a/lib/HalTftDisplay/src/Display.cpp +++ b/lib/HalTftDisplay/src/Display.cpp @@ -88,8 +88,12 @@ void Display::show() { for(x = 0; x < MATRIX_WIDTH; ++x) { +#if CONFIG_DISPLAY_ROTATE180 != 0 + Color brightnessAdjustedColor = m_ledMatrix.getColor(MATRIX_WIDTH - x - 1, MATRIX_HEIGHT - y - 1); +#else Color brightnessAdjustedColor = m_ledMatrix.getColor(x, y); - uint16_t intensity = brightnessAdjustedColor.getIntensity(); +#endif + uint16_t intensity = brightnessAdjustedColor.getIntensity(); int32_t xNative = y * (PIXEL_HEIGHT + PiXEL_DISTANCE) + BORDER_Y; int32_t yNative = TFT_HEIGHT - (x * (PIXEL_WIDTH + PiXEL_DISTANCE) + BORDER_X) - 1; From c50aa2aa44f6938733c45d7b66afcf351f2d99a5 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Wed, 8 Nov 2023 23:08:07 +0100 Subject: [PATCH 050/105] Fixed workflow badge. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22220826..c798d8b6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Full RGB LED matrix, based on an ESP32 and WS2812B LEDs. [![License](https://img.shields.io/badge/license-MIT-blue.svg)](http://choosealicense.com/licenses/mit/) [![Repo Status](https://www.repostatus.org/badges/latest/wip.svg)](https://www.repostatus.org/#wip) [![Release](https://img.shields.io/github/release/BlueAndi/esp-rgb-led-matrix.svg)](https://github.com/BlueAndi/esp-rgb-led-matrix/releases) -[![Build Status](https://github.com/BlueAndi/esp-rgb-led-matrix/workflows/PlatformIO%20CI/badge.svg?branch=master)](https://github.com/BlueAndi/esp-rgb-led-matrix/actions?query=workflow%3A%22PlatformIO+CI%22) +[![Build Status](https://github.com/BlueAndi/esp-rgb-led-matrix/actions/workflows/main.yml/badge.svg)](https://github.com/BlueAndi/esp-rgb-led-matrix/actions/workflows/main.yml) [![pixelix](https://img.youtube.com/vi/dik8Rm6f3o0/0.jpg)](https://www.youtube.com/watch?v=dik8Rm6f3o0 "Pixelix") [![pixelix](https://img.youtube.com/vi/UCjJCI5JShY/0.jpg)](https://www.youtube.com/watch?v=UCjJCI5JShY "Pixelix - Remote Button") From 67d80c6b981df1a308fcaecfbf848fbe00ef7eaa Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Thu, 9 Nov 2023 21:31:12 +0100 Subject: [PATCH 051/105] DDP handles the type 0 (format is undefined and bits per pixel element is undefined) like RGB with 8-bit per pixel element. That means full RGB with 24 bit per pixel. --- lib/DDPPlugin/src/DDPPlugin.cpp | 33 ++++++-- lib/DDPPlugin/src/DDPPlugin.h | 15 ++-- lib/DDPPlugin/src/DDPServer.cpp | 138 ++++++++++++++++---------------- lib/DDPPlugin/src/DDPServer.h | 23 +++--- 4 files changed, 113 insertions(+), 96 deletions(-) diff --git a/lib/DDPPlugin/src/DDPPlugin.cpp b/lib/DDPPlugin/src/DDPPlugin.cpp index 68a360bf..bbe16cef 100644 --- a/lib/DDPPlugin/src/DDPPlugin.cpp +++ b/lib/DDPPlugin/src/DDPPlugin.cpp @@ -131,29 +131,42 @@ void DDPPlugin::update(YAGfx& gfx) * Private Methods *****************************************************************************/ -void DDPPlugin::onData(DDPServer::Format format, uint32_t offset, uint8_t bitsPerPixel, uint8_t* payload, uint16_t payloadSize, bool isFinal) +void DDPPlugin::onData(DDPServer::Format format, uint32_t offset, uint8_t bitsPerPixelElement, uint8_t* payload, uint16_t payloadSize, bool isFinal) { MutexGuard guard(m_mutex); - /* xlights <= v202301 sends FORMAT_UNDEFINED with 1-bit per pixel which is - * necessary to be interpreted as FORMAT_RGB with 24-bit per pixel. + /* xlights <= v202301 sends FORMAT_UNDEFINED with 1-bit per pixel element which is + * necessary to be interpreted as FORMAT_RGB with 8-bit per pixel element. */ if ((DDPServer::FORMAT_UNDEFINED == format) && - (1U == bitsPerPixel)) + (1U == bitsPerPixelElement)) { /* Workaround! */ - format = DDPServer::FORMAT_RGB; - bitsPerPixel = 24U; + format = DDPServer::FORMAT_RGB; + bitsPerPixelElement = 8U; + } + /* If the format is FORMAT_UNDEFINED and the bit per pixel element is undefined, + * we assume its 8-bit per pixel element. + */ + else if ((DDPServer::FORMAT_UNDEFINED == format) && + (0U == bitsPerPixelElement)) + { + format = DDPServer::FORMAT_RGB; + bitsPerPixelElement = 8U; + } + else + { + ; } if ((nullptr != payload) && (DDPServer::FORMAT_RGB == format) && - (24U == bitsPerPixel)) + (8U == bitsPerPixelElement)) { uint16_t srcIdx = 0U; int16_t x = (offset % m_framebuffer.getWidth()); int16_t y = (offset / m_framebuffer.getWidth()); - uint8_t bytePerPixel = bitsPerPixel / 8U; + uint8_t bytePerPixel = (bitsPerPixelElement * 3U) / 8U; /* RGB = 3 base colors */ while((payloadSize > srcIdx) && (m_framebuffer.getHeight() > y)) { @@ -185,6 +198,10 @@ void DDPPlugin::onData(DDPServer::Format format, uint32_t offset, uint8_t bitsPe m_isUpdated = isFinal; } + else + { + LOG_WARNING("Unsupported DDP frame with format %d and bits per pixel element %u.", format, bitsPerPixelElement); + } } /****************************************************************************** diff --git a/lib/DDPPlugin/src/DDPPlugin.h b/lib/DDPPlugin/src/DDPPlugin.h index b041cb46..ee3fed63 100644 --- a/lib/DDPPlugin/src/DDPPlugin.h +++ b/lib/DDPPlugin/src/DDPPlugin.h @@ -149,7 +149,6 @@ class DDPPlugin : public Plugin private: DDPServer m_server; /**< DDP server */ - Mutex m_mutex; /**< Mutex to protect against concurrent access */ YAGfxDynamicBitmap m_framebuffer; /**< Framebuffer used for synchronization */ bool m_isUpdated; /**< Is framebuffer updated and ready to show? */ @@ -157,14 +156,14 @@ class DDPPlugin : public Plugin /** * On data reception, this method will be called from a different context. * - * @param[in] format Format of the payload data - * @param[in] offset Byte offset in display framebuffer where to continue - * @param[in] bitsPerPixel Bits per pixel in payload data - * @param[in] payload Payload data - * @param[in] payloadSize Payload data size in byte - * @param[in] isFinal If final, its the last data and display shall show it. Otherwise more data will come. + * @param[in] format Format of the payload data + * @param[in] offset Byte offset in display framebuffer where to continue + * @param[in] bitsPerPixelElement Bits per pixel in payload data + * @param[in] payload Payload data + * @param[in] payloadSize Payload data size in byte + * @param[in] isFinal If final, its the last data and display shall show it. Otherwise more data will come. */ - void onData(DDPServer::Format format, uint32_t offset, uint8_t bitsPerPixel, uint8_t* payload, uint16_t payloadSize, bool isFinal); + void onData(DDPServer::Format format, uint32_t offset, uint8_t bitsPerPixelElement, uint8_t* payload, uint16_t payloadSize, bool isFinal); }; /****************************************************************************** diff --git a/lib/DDPPlugin/src/DDPServer.cpp b/lib/DDPPlugin/src/DDPServer.cpp index 688b1183..c78173ee 100644 --- a/lib/DDPPlugin/src/DDPServer.cpp +++ b/lib/DDPPlugin/src/DDPServer.cpp @@ -43,127 +43,127 @@ *****************************************************************************/ /** Bit index for the version in the DDP header flags byte. */ -#define DDP_HEADER_FLAGS_VERSION_BIT (6U) +#define DDP_HEADER_FLAGS_VERSION_BIT (6U) /** Bit mask for the version in the DDP header flags byte. */ -#define DDP_HEADER_FLAGS_VERSION_MASK (0x03U) +#define DDP_HEADER_FLAGS_VERSION_MASK (0x03U) /** Bit index for the timecode flag in the DDP header flags byte. */ -#define DDP_HEADER_FLAGS_TIMECODE_BIT (4U) +#define DDP_HEADER_FLAGS_TIMECODE_BIT (4U) /** Bit mask for the timecode flag in the DDP header flags byte. */ -#define DDP_HEADER_FLAGS_TIMECODE_MASK (0x01U) +#define DDP_HEADER_FLAGS_TIMECODE_MASK (0x01U) /** Bit index for the storage flag in the DDP header flags byte. */ -#define DDP_HEADER_FLAGS_STORAGE_BIT (3U) +#define DDP_HEADER_FLAGS_STORAGE_BIT (3U) /** Bit mask for the storage flag in the DDP header flags byte. */ -#define DDP_HEADER_FLAGS_STORAGE_MASK (0x01U) +#define DDP_HEADER_FLAGS_STORAGE_MASK (0x01U) /** Bit index for the reply flag in the DDP header flags byte. */ -#define DDP_HEADER_FLAGS_REPLY_BIT (2U) +#define DDP_HEADER_FLAGS_REPLY_BIT (2U) /** Bit mask for the reply flagn in the DDP header flags byte. */ -#define DDP_HEADER_FLAGS_REPLY_MASK (0x01U) +#define DDP_HEADER_FLAGS_REPLY_MASK (0x01U) /** Bit index for the query flag in the DDP header flags byte. */ -#define DDP_HEADER_FLAGS_QUERY_BIT (1U) +#define DDP_HEADER_FLAGS_QUERY_BIT (1U) /** Bit mask for the query flag in the DDP header flags byte. */ -#define DDP_HEADER_FLAGS_QUERY_MASK (0x01U) +#define DDP_HEADER_FLAGS_QUERY_MASK (0x01U) /** Bit index for the push flag in the DDP header flags byte. */ -#define DDP_HEADER_FLAGS_PUSH_BIT (0U) +#define DDP_HEADER_FLAGS_PUSH_BIT (0U) /** Bit mask for the push flag in the DDP header flags byte. */ -#define DDP_HEADER_FLAGS_PUSH_MASK (0x01U) +#define DDP_HEADER_FLAGS_PUSH_MASK (0x01U) /** Bit index for the sequence number in the DDP header control byte. */ -#define DDP_HEADER_CONTROL_SEQ_NO_BIT (0U) +#define DDP_HEADER_CONTROL_SEQ_NO_BIT (0U) /** Bit mask for the sequence number in the DDP header control byte. */ -#define DDP_HEADER_CONTROL_SEQ_NO_MASK (0x0fU) +#define DDP_HEADER_CONTROL_SEQ_NO_MASK (0x0fU) /** Bit index for the customer bit in the DDP header data type byte. */ -#define DDP_HEADER_DT_CUSTOM_BIT (7U) +#define DDP_HEADER_DT_CUSTOM_BIT (7U) /** Bit mask for the customer bit in the DDP header data type byte. */ -#define DDP_HEADER_DT_CUSTOM_MASK (0x01U) +#define DDP_HEADER_DT_CUSTOM_MASK (0x01U) /** Bit index for the data type in the DDP header data type byte. */ -#define DDP_HEADER_DT_DATA_TYPE_BIT (3U) +#define DDP_HEADER_DT_DATA_TYPE_BIT (3U) /** Bit mask for the data type in the DDP header data type byte. */ -#define DDP_HEADER_DT_DATA_TYPE_MASK (0x07U) +#define DDP_HEADER_DT_DATA_TYPE_MASK (0x07U) /** Bit index for the pixel size in the DDP header data type byte. */ -#define DDP_HEADER_DT_PIXEL_SIZE_BIT (0U) +#define DDP_HEADER_DT_PIXEL_ELEMENT_SIZE_BIT (0U) /** Bit mask for the pixel size in the DDP header data type byte. */ -#define DDP_HEADER_DT_PIXEL_SIZE_MASK (0x07U) +#define DDP_HEADER_DT_PIXEL_ELEMENT_SIZE_MASK (0x07U) /** DDP data type - undefined */ -#define DDP_DATA_TYPE_UNDEFINED (0U) +#define DDP_DATA_TYPE_UNDEFINED (0U) /** DDP data type - RGB order */ -#define DDP_DATA_TYPE_RGB (1U) +#define DDP_DATA_TYPE_RGB (1U) /** DDP data type - HSL order */ -#define DDP_DATA_TYPE_HSL (2U) +#define DDP_DATA_TYPE_HSL (2U) /** DDP data type - RGBW order */ -#define DDP_DATA_TYPE_RGBW (3U) +#define DDP_DATA_TYPE_RGBW (3U) /** DDP data type - Grayscale (shades of gray) */ -#define DDP_DATA_TYPE_GRAYSCALE (4U) +#define DDP_DATA_TYPE_GRAYSCALE (4U) /** DDP pixel size - undefined */ -#define DDP_PIXEL_SIZE_UNDEFINED (0U) +#define DDP_PIXEL_ELEMENT_SIZE_UNDEFINED (0U) -/** DDP pixel size - 1 bit per pixel */ -#define DDP_PIXEL_SIZE_1 (1U) +/** DDP pixel size - 1 bit per pixel element */ +#define DDP_PIXEL_ELEMENT_SIZE_1 (1U) -/** DDP pixel size - 4 bit per pixel */ -#define DDP_PIXEL_SIZE_4 (2U) +/** DDP pixel size - 4 bit per pixel element */ +#define DDP_PIXEL_ELEMENT_SIZE_4 (2U) -/** DDP pixel size - 8 bit per pixel */ -#define DDP_PIXEL_SIZE_8 (3U) +/** DDP pixel size - 8 bit per pixel element */ +#define DDP_PIXEL_ELEMENT_SIZE_8 (3U) -/** DDP pixel size - 16 bit per pixel */ -#define DDP_PIXEL_SIZE_16 (4U) +/** DDP pixel size - 16 bit per pixel element */ +#define DDP_PIXEL_ELEMENT_SIZE_16 (4U) -/** DDP pixel size - 24 bit per pixel */ -#define DDP_PIXEL_SIZE_24 (5U) +/** DDP pixel size - 24 bit per pixel element */ +#define DDP_PIXEL_ELEMENT_SIZE_24 (5U) -/** DDP pixel size - 32 bit per pixel */ -#define DDP_PIXEL_SIZE_32 (6U) +/** DDP pixel size - 32 bit per pixel element */ +#define DDP_PIXEL_ELEMENT_SIZE_32 (6U) /** DDP id - reserved */ -#define DDP_ID_RESERVED (0U) +#define DDP_ID_RESERVED (0U) /** DDP id - default device */ -#define DDP_ID_DEFAULT (1U) +#define DDP_ID_DEFAULT (1U) /** DDP id - custom id defined via JSON config */ -#define DDP_ID_CUSTOM (2U) +#define DDP_ID_CUSTOM (2U) /** DDP id - JSON control */ -#define DDP_ID_JSON_CONTROL (246U) +#define DDP_ID_JSON_CONTROL (246U) /** DDP id - JSON config */ -#define DDP_ID_JSON_CONFIG (250U) +#define DDP_ID_JSON_CONFIG (250U) /** DDP id - JSON status */ -#define DDP_ID_JSON_STATUS (251U) +#define DDP_ID_JSON_STATUS (251U) /** DDP id - DMX legay mode */ -#define DDP_ID_DMX_TRANSIT (254U) +#define DDP_ID_DMX_TRANSIT (254U) /** DDP id - all devices */ -#define DDP_ID_ALL_DEVICES (255U) +#define DDP_ID_ALL_DEVICES (255U) /** DDP timecode field size in byte */ -#define DDP_TIMECODE_SIZE (4U) +#define DDP_TIMECODE_SIZE (4U) /****************************************************************************** * Types and classes @@ -394,46 +394,46 @@ uint8_t DDPServer::getDataType(const DDPHeader& header) return dataType; } -uint8_t DDPServer::getBitsPerPixel(const DDPHeader& header) +uint8_t DDPServer::getBitsPerPixelElement(const DDPHeader& header) { - uint8_t bitsPerPixel = (header.detail.dataType >> DDP_HEADER_DT_PIXEL_SIZE_BIT) & DDP_HEADER_DT_PIXEL_SIZE_MASK; + uint8_t bitsPerPixelElement = (header.detail.dataType >> DDP_HEADER_DT_PIXEL_ELEMENT_SIZE_BIT) & DDP_HEADER_DT_PIXEL_ELEMENT_SIZE_MASK; - switch(bitsPerPixel) + switch(bitsPerPixelElement) { - case DDP_PIXEL_SIZE_UNDEFINED: - bitsPerPixel = 8U; + case DDP_PIXEL_ELEMENT_SIZE_UNDEFINED: + bitsPerPixelElement = 0U; break; - case DDP_PIXEL_SIZE_1: - bitsPerPixel = 1U; + case DDP_PIXEL_ELEMENT_SIZE_1: + bitsPerPixelElement = 1U; break; - case DDP_PIXEL_SIZE_4: - bitsPerPixel = 4U; + case DDP_PIXEL_ELEMENT_SIZE_4: + bitsPerPixelElement = 4U; break; - case DDP_PIXEL_SIZE_8: - bitsPerPixel = 8U; + case DDP_PIXEL_ELEMENT_SIZE_8: + bitsPerPixelElement = 8U; break; - case DDP_PIXEL_SIZE_16: - bitsPerPixel = 16U; + case DDP_PIXEL_ELEMENT_SIZE_16: + bitsPerPixelElement = 16U; break; - case DDP_PIXEL_SIZE_24: - bitsPerPixel = 24U; + case DDP_PIXEL_ELEMENT_SIZE_24: + bitsPerPixelElement = 24U; break; - case DDP_PIXEL_SIZE_32: - bitsPerPixel = 32U; + case DDP_PIXEL_ELEMENT_SIZE_32: + bitsPerPixelElement = 32U; break; default: - bitsPerPixel = 8U; + bitsPerPixelElement = 0U; break; } - return bitsPerPixel; + return bitsPerPixelElement; } uint16_t DDPServer::getOffset(const DDPHeader& header) @@ -586,7 +586,7 @@ void DDPServer::handleData(const DDPHeader& header, uint8_t* payload, uint16_t p else if ((DDP_ID_ALL_DEVICES == header.detail.id) || (DDP_ID_DEFAULT == header.detail.id)) { - ddpNotify(static_cast(getDataType(header)), getOffset(header), getBitsPerPixel(header), payload, payloadSize, isPushFlagSet(header)); + ddpNotify(static_cast(getDataType(header)), getOffset(header), getBitsPerPixelElement(header), payload, payloadSize, isPushFlagSet(header)); } else { @@ -594,7 +594,7 @@ void DDPServer::handleData(const DDPHeader& header, uint8_t* payload, uint16_t p } } -void DDPServer::ddpNotify(Format format, uint32_t offset, uint8_t bitsPerPixel, uint8_t* payload, uint16_t payloadSize, bool isFinal) +void DDPServer::ddpNotify(Format format, uint32_t offset, uint8_t bitsPerPixelElement, uint8_t* payload, uint16_t payloadSize, bool isFinal) { DDPCallback callback = nullptr; @@ -606,7 +606,7 @@ void DDPServer::ddpNotify(Format format, uint32_t offset, uint8_t bitsPerPixel, if (nullptr != callback) { - callback(format, offset, bitsPerPixel, payload, payloadSize, isFinal); + callback(format, offset, bitsPerPixelElement, payload, payloadSize, isFinal); } } diff --git a/lib/DDPPlugin/src/DDPServer.h b/lib/DDPPlugin/src/DDPServer.h index d572e108..b23dc11c 100644 --- a/lib/DDPPlugin/src/DDPServer.h +++ b/lib/DDPPlugin/src/DDPServer.h @@ -81,7 +81,7 @@ class DDPServer * It provides received data to the application. If the final flag is set, the * data is complete and ready for showing it. */ - typedef std::function DDPCallback; + typedef std::function DDPCallback; /** * DDP application callback prototype for DMX legacy mode. @@ -405,13 +405,14 @@ class DDPServer uint8_t getDataType(const DDPHeader& header); /** - * Get the bits per pixel from the DDP header. + * Get the bits per pixel element from the DDP header. + * Pixel element means for just one base color, not for the whole pixel. * * @param[in] header DDP header * - * @return Bits per pixel + * @return Bits per pixel element */ - uint8_t getBitsPerPixel(const DDPHeader& header); + uint8_t getBitsPerPixelElement(const DDPHeader& header); /** * Get the offset from the DDP header. @@ -470,14 +471,14 @@ class DDPServer * Notifys a registered application and provides the DDP received data. * The application needs to copy the data into its own context! * - * @param[in] format Format of the payload data - * @param[in] offset Byte offset in display framebuffer where to continue - * @param[in] bitsPerPixel Bits per pixel in payload data - * @param[in] payload Payload data - * @param[in] payloadSize Payload data size in byte - * @param[in] isFinal If final, its the last data and display shall show it. Otherwise more data will come. + * @param[in] format Format of the payload data + * @param[in] offset Byte offset in display framebuffer where to continue + * @param[in] bitsPerPixelElement Bits per pixel element in payload data + * @param[in] payload Payload data + * @param[in] payloadSize Payload data size in byte + * @param[in] isFinal If final, its the last data and display shall show it. Otherwise more data will come. */ - void ddpNotify(Format format, uint32_t offset, uint8_t bitsPerPixel, uint8_t* payload, uint16_t payloadSize, bool isFinal); + void ddpNotify(Format format, uint32_t offset, uint8_t bitsPerPixelElement, uint8_t* payload, uint16_t payloadSize, bool isFinal); /** * Notifys a registered application and provides the DMX received data. From dd339883912715bddac0bfecec2bb3b5f83921c6 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Fri, 10 Nov 2023 22:20:13 +0100 Subject: [PATCH 052/105] Fixed float handling in GrabViaRestPlugin. --- lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp b/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp index f04e7c3d..8e8f88e9 100644 --- a/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp +++ b/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp @@ -697,7 +697,7 @@ void GrabViaRestPlugin::handleWebResponse(const DynamicJsonDocument& jsonDoc) value *= m_multiplier; value += m_offset; - (void)snprintf(buffer, sizeof(buffer), m_format.c_str(), jsonValue.as().c_str()); + (void)snprintf(buffer, sizeof(buffer), m_format.c_str(), value); m_textWidgetRight.setFormatStr(buffer); m_textWidgetTextOnly.setFormatStr(buffer); From be6e2eb02b50535ea1a77a154b96d224b81a56d2 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Fri, 10 Nov 2023 23:23:52 +0100 Subject: [PATCH 053/105] $/Bitcoin price configuration added. --- doc/grabConfigs/rest/bitcoin.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 doc/grabConfigs/rest/bitcoin.json diff --git a/doc/grabConfigs/rest/bitcoin.json b/doc/grabConfigs/rest/bitcoin.json new file mode 100644 index 00000000..5cdc5556 --- /dev/null +++ b/doc/grabConfigs/rest/bitcoin.json @@ -0,0 +1,15 @@ +{ + "method": "GET", + "url": "http://api.coindesk.com/v1/bpi/currentprice/USD.json", + "filter": { + "bpi": { + "USD": { + "rate_float": true + } + } + }, + "iconPath": "", + "format": "%0.2f $/BTC", + "multiplier": 1, + "offset": 0 +} \ No newline at end of file From 04cfd76bf194977b9106c7f87dfc0424a53ca024 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Fri, 10 Nov 2023 23:49:35 +0100 Subject: [PATCH 054/105] Fixed some doxygen comments. --- lib/BaseGfx/src/BaseGfx.hpp | 2 -- lib/BaseGfx/src/BaseGfxMap.hpp | 2 +- lib/BaseGfx/src/BaseGfxText.hpp | 6 +++--- lib/Utilities/src/LogSinkPrinter.cpp | 4 ++-- lib/Utilities/src/LogSinkPrinter.h | 2 +- lib/YAWidgets/src/BitmapWidget.h | 10 +++++----- lib/YAWidgets/src/TextWidget.h | 30 +++++++++++++++++----------- scripts/Services.cpp | 2 ++ scripts/TopicHandlers.cpp | 2 ++ src/Common/ResetMon.h | 2 +- src/Gfx/SlotList.cpp | 2 +- src/Web/Pages.cpp | 5 ----- src/Web/RestUtil.cpp | 25 ----------------------- 13 files changed, 36 insertions(+), 58 deletions(-) diff --git a/lib/BaseGfx/src/BaseGfx.hpp b/lib/BaseGfx/src/BaseGfx.hpp index 5c4380a9..becb8615 100644 --- a/lib/BaseGfx/src/BaseGfx.hpp +++ b/lib/BaseGfx/src/BaseGfx.hpp @@ -368,8 +368,6 @@ class BaseGfx * @param[in] x x-coordinate of upper left point * @param[in] y y-coordinate of upper left point * @param[in] bitmap Bitmap pixel buffer - * @param[in] width Bitmap width in pixel - * @param[in] height Bitmap height in pixel */ void drawBitmap(int16_t x, int16_t y, const BaseGfxBitmap& bitmap) { diff --git a/lib/BaseGfx/src/BaseGfxMap.hpp b/lib/BaseGfx/src/BaseGfxMap.hpp index 8830d2b5..492d46aa 100644 --- a/lib/BaseGfx/src/BaseGfxMap.hpp +++ b/lib/BaseGfx/src/BaseGfxMap.hpp @@ -146,7 +146,7 @@ class BaseGfxMap : public BaseGfx /** * Set canvas graphic operations. * - * @param[in] bitmap Bitmap which to set + * @param[in] gfx Graphic functions */ void setGfx(BaseGfx& gfx) { diff --git a/lib/BaseGfx/src/BaseGfxText.hpp b/lib/BaseGfx/src/BaseGfxText.hpp index 285ab197..aa3fdd82 100644 --- a/lib/BaseGfx/src/BaseGfxText.hpp +++ b/lib/BaseGfx/src/BaseGfxText.hpp @@ -232,7 +232,7 @@ class BaseGfxText /** * Get font. * - * @return font + * @return Font which is used. */ BaseFont& getFont() { @@ -240,9 +240,9 @@ class BaseGfxText } /** - * Set GFXfont. + * Set font. * - * @param[in] gfxFont GFXfont + * @param[in] font New font to set. */ void setFont(const BaseFont& font) { diff --git a/lib/Utilities/src/LogSinkPrinter.cpp b/lib/Utilities/src/LogSinkPrinter.cpp index 4533b8f5..211cc02e 100644 --- a/lib/Utilities/src/LogSinkPrinter.cpp +++ b/lib/Utilities/src/LogSinkPrinter.cpp @@ -107,11 +107,11 @@ void LogSinkPrinter::send(const Logging::Msg& msg) * Private Methods *****************************************************************************/ -const char* LogSinkPrinter::logLevelToString(const Logging::LogLevel LogLevel) const +const char* LogSinkPrinter::logLevelToString(const Logging::LogLevel logLevel) const { const char* logLevelString = nullptr; - switch (LogLevel) + switch (logLevel) { case Logging::LOG_LEVEL_FATAL: logLevelString = "FATAL "; diff --git a/lib/Utilities/src/LogSinkPrinter.h b/lib/Utilities/src/LogSinkPrinter.h index 81efd0c7..afce16c4 100644 --- a/lib/Utilities/src/LogSinkPrinter.h +++ b/lib/Utilities/src/LogSinkPrinter.h @@ -166,7 +166,7 @@ class LogSinkPrinter : public LogSink * * @return The severity of the given logLevel as string. */ - const char* logLevelToString(const Logging::LogLevel LogLevel) const; + const char* logLevelToString(const Logging::LogLevel logLevel) const; }; /****************************************************************************** diff --git a/lib/YAWidgets/src/BitmapWidget.h b/lib/YAWidgets/src/BitmapWidget.h index 10cf346f..4a74b64a 100644 --- a/lib/YAWidgets/src/BitmapWidget.h +++ b/lib/YAWidgets/src/BitmapWidget.h @@ -164,30 +164,30 @@ class BitmapWidget : public Widget bool loadSpriteSheet(FS& fs, const String& spriteSheetFileName, const String& textureFileName); /** - * Get the animation control flag FORWARD of a sprite sheet + * Get the animation control flag FORWARD of a sprite sheet. * * @return If forward, it will return true otherwise false. */ bool isSpriteSheetForward() const; /** - * Set the animation control flag FORWARD of a sprite sheet + * Set the animation control flag FORWARD of a sprite sheet. * * @param[in] forward The state to be set. */ void setSpriteSheetForward(bool forward); /** - * Get the animation control flag REPEAT of a sprite sheet + * Get the animation control flag REPEAT of a sprite sheet. * * @return If its repeated, it will return true otherwise false. */ bool isSpriteSheetRepeatInfinite() const; /** - * Set the animation control flag REPEAT of a sprite sheet + * Set the animation control flag REPEAT of a sprite sheet. * - * @param[in] isRepeat The state to be set. + * @param[in] repeat The repeat flat to be set. */ void setSpriteSheetRepeatInfinite(bool repeat); diff --git a/lib/YAWidgets/src/TextWidget.h b/lib/YAWidgets/src/TextWidget.h index 9bd55bab..eb91c30b 100644 --- a/lib/YAWidgets/src/TextWidget.h +++ b/lib/YAWidgets/src/TextWidget.h @@ -432,10 +432,12 @@ class TextWidget : public Widget /** * Handles the keyword for color changes. * - * @param[in] gfx Graphics interface, only necessary if actions shall take place. - * @param[in] noAction The handler shall take no action. This is only used to get rid of the keywords in the text. - * @param[in] formatStr String which may contain keywords. - * @param[out] overstep Number of characters, which must be overstepped before the next normal character comes. + * @param[in] gfx Graphics interface, only necessary if actions shall take place. + * @param[in] gfxText The text to handle with the keyword. + * @param[in] noAction The handler shall take no action. This is only used to get rid of the keywords in the text. + * @param[in] formatStr String which may contain keywords. + * @param[in] isScrolling Is scrolling active? + * @param[out] overstep Number of characters, which must be overstepped before the next normal character comes. * * @return If keyword is handled successful, it returns true otherwise false. */ @@ -444,10 +446,12 @@ class TextWidget : public Widget /** * Handles the keyword for alignment changes. * - * @param[in] gfx Graphics interface, only necessary if actions shall take place. - * @param[in] noAction The handler shall take no action. This is only used to get rid of the keywords in the text. - * @param[in] formatStr String which may contain keywords. - * @param[out] overstep Number of characters, which must be overstepped before the next normal character comes. + * @param[in] gfx Graphics interface, only necessary if actions shall take place. + * @param[in] gfxText The text to handle with the keyword. + * @param[in] noAction The handler shall take no action. This is only used to get rid of the keywords in the text. + * @param[in] formatStr String which may contain keywords. + * @param[in] isScrolling Is scrolling active? + * @param[out] overstep Number of characters, which must be overstepped before the next normal character comes. * * @return If keyword is handled successful, it returns true otherwise false. */ @@ -456,10 +460,12 @@ class TextWidget : public Widget /** * Handles the keyword for character code. * - * @param[in] gfx Graphics interface, only necessary if actions shall take place. - * @param[in] noAction The handler shall take no action. This is only used to get rid of the keywords in the text. - * @param[in] formatStr String which may contain keywords. - * @param[out] overstep Number of characters, which must be overstepped before the next normal character comes. + * @param[in] gfx Graphics interface, only necessary if actions shall take place. + * @param[in] gfxText The text to handle with the keyword. + * @param[in] noAction The handler shall take no action. This is only used to get rid of the keywords in the text. + * @param[in] formatStr String which may contain keywords. + * @param[in] isScrolling Is scrolling active? + * @param[out] overstep Number of characters, which must be overstepped before the next normal character comes. * * @return If keyword is handled successful, it returns true otherwise false. */ diff --git a/scripts/Services.cpp b/scripts/Services.cpp index 1239f010..fd2197e0 100644 --- a/scripts/Services.cpp +++ b/scripts/Services.cpp @@ -27,6 +27,8 @@ /** * @brief Services * @author Andreas Merkle + * + * @{ */ /****************************************************************************** diff --git a/scripts/TopicHandlers.cpp b/scripts/TopicHandlers.cpp index f44efdd5..359bd4c3 100644 --- a/scripts/TopicHandlers.cpp +++ b/scripts/TopicHandlers.cpp @@ -27,6 +27,8 @@ /** * @brief Topic handlers * @author Andreas Merkle + * + * @{ */ /****************************************************************************** diff --git a/src/Common/ResetMon.h b/src/Common/ResetMon.h index fa19cff8..3bd2f5e5 100644 --- a/src/Common/ResetMon.h +++ b/src/Common/ResetMon.h @@ -94,7 +94,7 @@ class ResetMon SimpleTimer m_timer; /**< Timer used for cyclic processing. */ RESET_REASON m_resetReasonAppCpu; /**< The reset reason of the APP cpu. */ - RESET_REASON m_resetReasonProCpu; /**< The reset reason of the PRO cpu. + RESET_REASON m_resetReasonProCpu; /**< The reset reason of the PRO cpu. */ /** * Constructs the reset monitor. diff --git a/src/Gfx/SlotList.cpp b/src/Gfx/SlotList.cpp index dd922779..32f3a568 100644 --- a/src/Gfx/SlotList.cpp +++ b/src/Gfx/SlotList.cpp @@ -319,7 +319,7 @@ uint8_t SlotList::getStickySlot() const * * If slot is empty or the plugin is disabled, it will fail. * - * @param[in] slotId The id of the slot which to set sticky. + * @param[in] slotId The id of the slot which to set sticky. * * @return If successful it will return true otherwise false. */ diff --git a/src/Web/Pages.cpp b/src/Web/Pages.cpp index 8627c10d..008e895b 100644 --- a/src/Web/Pages.cpp +++ b/src/Web/Pages.cpp @@ -261,11 +261,6 @@ void Pages::init(AsyncWebServer& srv) } } -/** - * Error web page used in case a requested path was not found. - * - * @param[in] request HTTP request - */ void Pages::error(AsyncWebServerRequest* request) { if (nullptr == request) diff --git a/src/Web/RestUtil.cpp b/src/Web/RestUtil.cpp index 6d6be1f5..68733391 100644 --- a/src/Web/RestUtil.cpp +++ b/src/Web/RestUtil.cpp @@ -72,13 +72,6 @@ * External Functions *****************************************************************************/ -/** - * Prepare JSON document for success response. - * - * @param[out] jsonDoc JSON document - * - * @return JSON object where to add additional data. - */ JsonVariant RestUtil::prepareRspSuccess(JsonDocument& jsonDoc) { JsonObject dataObj = jsonDoc.createNestedObject("data"); @@ -88,12 +81,6 @@ JsonVariant RestUtil::prepareRspSuccess(JsonDocument& jsonDoc) return dataObj; } -/** - * Prepare JSON document for error response. - * - * @param[out] jsonDoc JSON document where to add error response. - * @param[in] msg Error message - */ void RestUtil::prepareRspError(JsonDocument& jsonDoc, const char* msg) { JsonObject errorObj = jsonDoc.createNestedObject("error"); @@ -102,23 +89,11 @@ void RestUtil::prepareRspError(JsonDocument& jsonDoc, const char* msg) jsonDoc["status"] = "error"; } -/** - * Prepare JSON document for concrete error response: HTTP method not supported. - * - * @param[out] jsonDoc JSON document where to add error response. - */ void RestUtil::prepareRspErrorHttpMethodNotSupported(JsonDocument& jsonDoc) { prepareRspError(jsonDoc, "HTTP method not supported."); } -/** - * Send a application/json response to the client back. - * - * @param[in] request Client request - * @param[in] jsonDoc JSON response document - * @param[in] httpStatusCode HTTP status code - */ void RestUtil::sendJsonRsp(AsyncWebServerRequest* request, const JsonDocument& jsonDoc, uint32_t httpStatusCode) { if (true == jsonDoc.overflowed()) From 34bd221d9b17c849b4f675789206f98f31971a19 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sat, 18 Nov 2023 10:30:49 +0100 Subject: [PATCH 055/105] Improved file handling in the web upload handler. --- .../src/RestApiTopicHandler.cpp | 12 ++++++------ lib/RestApiTopicHandler/src/RestApiTopicHandler.h | 4 +--- src/Web/RestApi.cpp | 15 +++++++-------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp b/lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp index b25c1135..16c62c6c 100644 --- a/lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp +++ b/lib/RestApiTopicHandler/src/RestApiTopicHandler.cpp @@ -290,9 +290,9 @@ void RestApiTopicHandler::uploadHandler(AsyncWebServerRequest *request, const St else { /* Create a new file and overwrite a existing one. */ - topicMetaData->fd = FILESYSTEM.open(topicMetaData->fullPath, "w"); + request->_tempFile = FILESYSTEM.open(topicMetaData->fullPath, "w"); - if (false == topicMetaData->fd) + if (false == request->_tempFile) { LOG_ERROR("Couldn't create file: %s", topicMetaData->fullPath.c_str()); topicMetaData->isUploadError = true; @@ -305,14 +305,14 @@ void RestApiTopicHandler::uploadHandler(AsyncWebServerRequest *request, const St if (false == topicMetaData->isUploadError) { /* If file is open, write data to it. */ - if (true == topicMetaData->fd) + if (true == request->_tempFile) { - if (len != topicMetaData->fd.write(data, len)) + if (len != request->_tempFile.write(data, len)) { LOG_ERROR("Less data written, upload aborted."); topicMetaData->isUploadError = true; topicMetaData->fullPath.clear(); - topicMetaData->fd.close(); + request->_tempFile.close(); } } @@ -321,7 +321,7 @@ void RestApiTopicHandler::uploadHandler(AsyncWebServerRequest *request, const St { LOG_INFO("Upload of %s finished.", filename.c_str()); - topicMetaData->fd.close(); + request->_tempFile.close(); } } } diff --git a/lib/RestApiTopicHandler/src/RestApiTopicHandler.h b/lib/RestApiTopicHandler/src/RestApiTopicHandler.h index 94607446..643c65aa 100644 --- a/lib/RestApiTopicHandler/src/RestApiTopicHandler.h +++ b/lib/RestApiTopicHandler/src/RestApiTopicHandler.h @@ -158,7 +158,6 @@ class RestApiTopicHandler : public ITopicHandler String uri; /**< URI where the handler is registered. */ bool isUploadError; /**< If upload error happened, it will be true otherwise false. */ String fullPath; /**< Full path of uploaded file. If empty, there is no file available. */ - File fd; /**< Upload file descriptor */ /** * Initialize topic meta data. @@ -173,8 +172,7 @@ class RestApiTopicHandler : public ITopicHandler webHandler(nullptr), uri(), isUploadError(false), - fullPath(), - fd() + fullPath() { } }; diff --git a/src/Web/RestApi.cpp b/src/Web/RestApi.cpp index de82312a..37c4dbb7 100644 --- a/src/Web/RestApi.cpp +++ b/src/Web/RestApi.cpp @@ -1484,15 +1484,14 @@ static void handleFilePost(AsyncWebServerRequest* request) */ static void uploadHandler(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) { - static File fd; - bool isError = false; + bool isError = false; /* Begin of upload? */ if (0 == index) { - fd = FILESYSTEM.open(filename, "w"); + request->_tempFile = FILESYSTEM.open(filename, "w"); - if (false == fd) + if (false == request->_tempFile) { isError = true; } @@ -1502,9 +1501,9 @@ static void uploadHandler(AsyncWebServerRequest *request, const String& filename } } - if (true == fd) + if (true == request->_tempFile) { - (void)fd.write(data, len); + (void)request->_tempFile.write(data, len); } if ((true == final) && @@ -1512,13 +1511,13 @@ static void uploadHandler(AsyncWebServerRequest *request, const String& filename { LOG_INFO("File %s successful written.", filename.c_str()); - fd.close(); + request->_tempFile.close(); } else if (true == isError) { LOG_INFO("File %s upload aborted.", filename.c_str()); - fd.close(); + request->_tempFile.close(); } if (true == isError) From a5efe25796278f12850b1f7492df1141020c5ed4 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 19 Nov 2023 19:52:27 +0100 Subject: [PATCH 056/105] The size of the display in the Display web page will automatically resize depended on the target display with and size. #151 --- data/display.html | 13 ++++++++++++- data/js/ws.js | 6 +++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/data/display.html b/data/display.html index 943c916d..c5499996 100644 --- a/data/display.html +++ b/data/display.html @@ -157,11 +157,22 @@

                                                            Display

                                                            $("#slotId").text(rsp.slotId); + /* If necessary, resize the canvas. */ + var $canvas = $("#canvas"); + var ctx = $canvas[0].getContext('2d'); + var width = rsp.width * pixelWidth + rsp.width + 1; + var height = rsp.height * pixelWidth + rsp.height + 1; + + if ((ctx.canvas.width != width) || (ctx.canvas.height != height)) { + ctx.canvas.width = width; + ctx.canvas.height = height; + } + /* Handle display data */ for(y = 0; y < rsp.height; ++y) { for(x = 0; x < rsp.width; ++x) { if (rsp.data.length > index) { - color = parseInt(rsp.data[index]); + color = rsp.data[index]; red = (color & 0xff0000) >> 16; green = (color & 0x00ff00) >> 8; blue = (color & 0x0000ff) >> 0; diff --git a/data/js/ws.js b/data/js/ws.js index a01aa369..7941118c 100644 --- a/data/js/ws.js +++ b/data/js/ws.js @@ -170,9 +170,9 @@ pixelix.ws.Client.prototype._onMessage = function(msg) { rsp.name = data[0]; this._pendingCmd.resolve(rsp); } else if ("GETDISP" === this._pendingCmd.name) { - rsp.slotId = data.shift(); - rsp.width = data.shift(); - rsp.height = data.shift(); + rsp.slotId = parseInt(data.shift()); + rsp.width = parseInt(data.shift()); + rsp.height = parseInt(data.shift()); rsp.data = []; for(index = 0; index < data.length; ++index) { rsp.data.push(parseInt(data[index], 16)); From 9e3472e661a0cff75b2ab1983405cc9d3a5fca2c Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 19 Nov 2023 19:55:14 +0100 Subject: [PATCH 057/105] TOC updated --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 489936fb..c4842866 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Full RGB LED matrix, based on an ESP32 and WS2812B LEDs. * [Is it possible to use a font with 8px height?](#is-it-possible-to-use-a-font-with-8px-height) * [How to configure the date/time format?](#how-to-configure-the-datetime-format) * [How to configure my own list of plugins?](#how-to-configure-my-own-list-of-plugins) + * [Is there an easy way to rotate the display by 180° ? I need to turn the display when putting it into a housing.](#is-there-an-easy-way-to-rotate-the-display-by-180--i-need-to-turn-the-display-when-putting-it-into-a-housing) * [Used Libraries](#used-libraries) * [Issues, Ideas And Bugs](#issues-ideas-and-bugs) * [License](#license) From d68a5512c8cfcf4515b4f57be1926c4f1ea32c1a Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Tue, 21 Nov 2023 00:29:05 +0100 Subject: [PATCH 058/105] Bugfix: Typo in JSON key for sprite sheet full path fixed in ThreeIconPlugin. --- lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp b/lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp index f9474af8..0744de01 100644 --- a/lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp +++ b/lib/ThreeIconPlugin/src/ThreeIconPlugin.cpp @@ -196,7 +196,7 @@ bool ThreeIconPlugin::setTopic(const String& topic, const JsonObjectConst& value JsonVariantConst jsonIsForward = value["forward"]; JsonVariantConst jsonIsRepeat = value["repeat"]; JsonVariantConst jsonIconFullPath = value["iconFullPath"]; - JsonVariantConst jsonSpriteSheetFullPath = value["spritesheetFullPath"]; + JsonVariantConst jsonSpriteSheetFullPath = value["spriteSheetFullPath"]; if (false == jsonIsForward.isNull()) { From 7323bf91368da716f97f2892548c5171b1e3c77c Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Tue, 21 Nov 2023 00:30:06 +0100 Subject: [PATCH 059/105] Bugfix: Time format was not changeable via REST API or MQTT in the SunrisePlugin. --- lib/SunrisePlugin/src/SunrisePlugin.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/SunrisePlugin/src/SunrisePlugin.cpp b/lib/SunrisePlugin/src/SunrisePlugin.cpp index 53fe2437..f4893af4 100644 --- a/lib/SunrisePlugin/src/SunrisePlugin.cpp +++ b/lib/SunrisePlugin/src/SunrisePlugin.cpp @@ -102,6 +102,7 @@ bool SunrisePlugin::setTopic(const String& topic, const JsonObjectConst& value) JsonObject jsonCfg = jsonDoc.to(); JsonVariantConst jsonLongitude = value["longitude"]; JsonVariantConst jsonLatitude = value["latitude"]; + JsonVariantConst jsonTimeFormat = value["timeFormat"]; /* The received configuration may not contain all single key/value pair. * Therefore read first the complete internal configuration and @@ -126,6 +127,12 @@ bool SunrisePlugin::setTopic(const String& topic, const JsonObjectConst& value) isSuccessful = true; } + if (false == jsonTimeFormat.isNull()) + { + jsonCfg["timeFormat"] = jsonTimeFormat.as(); + isSuccessful = true; + } + if (true == isSuccessful) { JsonObjectConst jsonCfgConst = jsonCfg; From 80f3375cfbf8643ca717f4bcdfdb3995397eeadd Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Tue, 21 Nov 2023 00:34:56 +0100 Subject: [PATCH 060/105] Use JsonVariantConst to ensure that the JSON result contains the right data. --- lib/BTCQuotePlugin/src/BTCQuotePlugin.cpp | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/BTCQuotePlugin/src/BTCQuotePlugin.cpp b/lib/BTCQuotePlugin/src/BTCQuotePlugin.cpp index d2d9df12..65ef9cc3 100644 --- a/lib/BTCQuotePlugin/src/BTCQuotePlugin.cpp +++ b/lib/BTCQuotePlugin/src/BTCQuotePlugin.cpp @@ -235,8 +235,8 @@ void BTCQuotePlugin::initHttpClient() StaticJsonDocument filter; DeserializationError error; - filter["bpi"]["USD"]["rate_float"] = true; - filter["bpi"]["USD"]["rate"] = true; + filter["bpi"]["USD"]["rate_float"] = true; + filter["bpi"]["USD"]["rate"] = true; if (true == filter.overflowed()) { @@ -269,12 +269,19 @@ void BTCQuotePlugin::initHttpClient() void BTCQuotePlugin::handleWebResponse(DynamicJsonDocument& jsonDoc) { - m_relevantResponsePart = jsonDoc["bpi"]["USD"]["rate"].as() + " $/BTC"; - m_relevantResponsePart.replace(",", "'"); // beautify to european(?) standard formatting ' for 1000s - - LOG_INFO("BTC/USD to print %s", m_relevantResponsePart.c_str()); + JsonVariantConst jsonBpi = jsonDoc["bpi"]; + JsonVariantConst jsonUsd = jsonBpi["USD"]; + JsonVariantConst jsonRate = jsonUsd["rate"]; - m_textWidget.setFormatStr(m_relevantResponsePart); + if (false == jsonRate.isNull()) + { + m_relevantResponsePart = jsonRate.as() + " $/BTC"; + m_relevantResponsePart.replace(",", "'"); /* Beautify to european(?) standard formatting ' for 1000s */ + + LOG_INFO("BTC/USD to print %s", m_relevantResponsePart.c_str()); + + m_textWidget.setFormatStr(m_relevantResponsePart); + } } void BTCQuotePlugin::clearQueue() From 6fe66703e391cfc0d2015a7ed84473fcc3fcc0d8 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Wed, 22 Nov 2023 23:23:36 +0100 Subject: [PATCH 061/105] Avoid reinterpret_cast<>, therefore replaced at several places with static_cast<>. --- lib/AsyncHttpClient/src/AsyncHttpClient.cpp | 8 ++++--- lib/AudioService/src/AudioDrv.cpp | 2 +- lib/BTCQuotePlugin/src/BTCQuotePlugin.cpp | 3 ++- lib/DDPPlugin/src/DDPServer.cpp | 2 +- .../src/GrabViaRestPlugin.cpp | 3 ++- lib/GruenbeckPlugin/src/GruenbeckPlugin.cpp | 3 ++- .../src/OpenWeatherPlugin.cpp | 3 ++- lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h | 22 +++++++++---------- lib/SunrisePlugin/src/SunrisePlugin.cpp | 3 ++- lib/VolumioPlugin/src/VolumioPlugin.cpp | 3 ++- src/Gfx/DisplayMgr.cpp | 4 ++-- src/Hal/ButtonDrv.cpp | 4 ++-- src/Web/WebSocket.cpp | 6 ++--- 13 files changed, 37 insertions(+), 29 deletions(-) diff --git a/lib/AsyncHttpClient/src/AsyncHttpClient.cpp b/lib/AsyncHttpClient/src/AsyncHttpClient.cpp index 8d468bf2..26b79143 100644 --- a/lib/AsyncHttpClient/src/AsyncHttpClient.cpp +++ b/lib/AsyncHttpClient/src/AsyncHttpClient.cpp @@ -562,7 +562,7 @@ void AsyncHttpClient::clearEvtQueue() void AsyncHttpClient::processTask(void* parameters) { - AsyncHttpClient* tthis = reinterpret_cast(parameters); + AsyncHttpClient* tthis = static_cast(parameters); if ((nullptr != tthis) && (nullptr != tthis->m_processTaskSemaphore)) @@ -716,7 +716,8 @@ void AsyncHttpClient::onError(int8_t error) void AsyncHttpClient::onData(const uint8_t* data, size_t len) { size_t index = 0U; - const char* asciiData = reinterpret_cast(data); + const void* vData = data; + const char* asciiData = static_cast(vData); bool isError = false; /* RFC2616 - Response = Status-Line @@ -1311,7 +1312,8 @@ bool AsyncHttpClient::parseChunkedResponseTrailer(const char* data, size_t len, bool AsyncHttpClient::parseChunkedResponse(const uint8_t* data, size_t len, size_t& index) { - const char* asciiData = reinterpret_cast(data); + const void* vData = data; + const char* asciiData = static_cast(vData); bool isChunkEOF = false; /* diff --git a/lib/AudioService/src/AudioDrv.cpp b/lib/AudioService/src/AudioDrv.cpp index c1db8bf6..0b83b315 100644 --- a/lib/AudioService/src/AudioDrv.cpp +++ b/lib/AudioService/src/AudioDrv.cpp @@ -177,7 +177,7 @@ void AudioDrv::stop() void AudioDrv::processTask(void* parameters) { - AudioDrv* tthis = reinterpret_cast(parameters); + AudioDrv* tthis = static_cast(parameters); if ((nullptr != tthis) && (nullptr != tthis->m_xSemaphore)) diff --git a/lib/BTCQuotePlugin/src/BTCQuotePlugin.cpp b/lib/BTCQuotePlugin/src/BTCQuotePlugin.cpp index 65ef9cc3..43bb4a9d 100644 --- a/lib/BTCQuotePlugin/src/BTCQuotePlugin.cpp +++ b/lib/BTCQuotePlugin/src/BTCQuotePlugin.cpp @@ -230,7 +230,8 @@ void BTCQuotePlugin::initHttpClient() if (nullptr != jsonDoc) { size_t payloadSize = 0U; - const char* payload = reinterpret_cast(rsp.getPayload(payloadSize)); + const void* vPayload = rsp.getPayload(payloadSize); + const char* payload = static_cast(vPayload); const size_t FILTER_SIZE = 128U; StaticJsonDocument filter; DeserializationError error; diff --git a/lib/DDPPlugin/src/DDPServer.cpp b/lib/DDPPlugin/src/DDPServer.cpp index c78173ee..73bffccf 100644 --- a/lib/DDPPlugin/src/DDPServer.cpp +++ b/lib/DDPPlugin/src/DDPServer.cpp @@ -198,7 +198,7 @@ bool DDPServer::begin(const String& deviceManufacturer, const String& deviceMode { m_udpServer.onPacket([](void* arg, AsyncUDPPacket& packet) { - DDPServer* tthis = reinterpret_cast(arg); + DDPServer* tthis = static_cast(arg); if (nullptr != tthis) { diff --git a/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp b/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp index 8e8f88e9..8c07e249 100644 --- a/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp +++ b/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp @@ -606,7 +606,8 @@ void GrabViaRestPlugin::initHttpClient() if (nullptr != jsonDoc) { size_t payloadSize = 0U; - const char* payload = reinterpret_cast(rsp.getPayload(payloadSize)); + const void* vPayload = rsp.getPayload(payloadSize); + const char* payload = static_cast(vPayload); const size_t FILTER_SIZE = 128U; DeserializationError error; diff --git a/lib/GruenbeckPlugin/src/GruenbeckPlugin.cpp b/lib/GruenbeckPlugin/src/GruenbeckPlugin.cpp index ed23b8d2..7986de4d 100644 --- a/lib/GruenbeckPlugin/src/GruenbeckPlugin.cpp +++ b/lib/GruenbeckPlugin/src/GruenbeckPlugin.cpp @@ -478,7 +478,8 @@ void GruenbeckPlugin::initHttpClient() const uint32_t RELEVANT_DATA_LENGTH = 3U; size_t payloadSize = 0U; - const char* payload = reinterpret_cast(rsp.getPayload(payloadSize)); + const void* vPayload = rsp.getPayload(payloadSize); + const char* payload = static_cast(vPayload); char restCapacity[RELEVANT_DATA_LENGTH + 1]; Msg msg; diff --git a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp index dc549a36..a84fe39e 100644 --- a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp +++ b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp @@ -750,7 +750,8 @@ void OpenWeatherPlugin::initHttpClient() if (nullptr != jsonDoc) { size_t payloadSize = 0U; - const char* payload = reinterpret_cast(rsp.getPayload(payloadSize)); + const void* vPayload = rsp.getPayload(payloadSize); + const char* payload = static_cast(vPayload); const size_t FILTER_SIZE = 128U; StaticJsonDocument filter; JsonObject filterCurrent = filter.createNestedObject("current"); diff --git a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h index 2b11cbcd..704d4124 100644 --- a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h +++ b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h @@ -69,6 +69,17 @@ class OpenWeatherPlugin : public Plugin, private PluginConfigFsHandler { public: + /** + * Enumeration to choose an additional weather information to be displayed. + */ + enum OtherWeatherInformation + { + OTHER_WEATHER_INFO_UVI = 0, /**< Display UV Index as additional information. */ + OTHER_WEATHER_INFO_HUMIDITY, /**< Display humidity in % as additional information. */ + OTHER_WEATHER_INFO_WIND, /**< Display windspeed in m/s as additional information. */ + OTHER_WEATHER_INFO_OFF /**< Display only general weather information. */ + }; + /** * Constructs the plugin. * @@ -112,17 +123,6 @@ class OpenWeatherPlugin : public Plugin, private PluginConfigFsHandler (void)m_mutex.create(); } - /** - * Enumeration to choose an additional weather information to be displayed. - */ - enum OtherWeatherInformation - { - OTHER_WEATHER_INFO_UVI = 0, /**< Display UV Index as additional information. */ - OTHER_WEATHER_INFO_HUMIDITY, /**< Display humidity in % as additional information. */ - OTHER_WEATHER_INFO_WIND, /**< Display windspeed in m/s as additional information. */ - OTHER_WEATHER_INFO_OFF /**< Display only general weather information. */ - }; - /** * Destroys the plugin. */ diff --git a/lib/SunrisePlugin/src/SunrisePlugin.cpp b/lib/SunrisePlugin/src/SunrisePlugin.cpp index f4893af4..df4bfd73 100644 --- a/lib/SunrisePlugin/src/SunrisePlugin.cpp +++ b/lib/SunrisePlugin/src/SunrisePlugin.cpp @@ -448,7 +448,8 @@ void SunrisePlugin::initHttpClient() if (nullptr != jsonDoc) { size_t payloadSize = 0U; - const char* payload = reinterpret_cast(rsp.getPayload(payloadSize)); + const void* vPayload = rsp.getPayload(payloadSize); + const char* payload = static_cast(vPayload); const size_t FILTER_SIZE = 128U; StaticJsonDocument filter; DeserializationError error; diff --git a/lib/VolumioPlugin/src/VolumioPlugin.cpp b/lib/VolumioPlugin/src/VolumioPlugin.cpp index 02ffe0d2..d59c3007 100644 --- a/lib/VolumioPlugin/src/VolumioPlugin.cpp +++ b/lib/VolumioPlugin/src/VolumioPlugin.cpp @@ -535,7 +535,8 @@ void VolumioPlugin::initHttpClient() if (nullptr != jsonDoc) { size_t payloadSize = 0U; - const char* payload = reinterpret_cast(rsp.getPayload(payloadSize)); + const void* vPayload = rsp.getPayload(payloadSize); + const char* payload = static_cast(vPayload); const size_t FILTER_SIZE = 128U; StaticJsonDocument filter; DeserializationError error; diff --git a/src/Gfx/DisplayMgr.cpp b/src/Gfx/DisplayMgr.cpp index e1c148a3..591d8340 100644 --- a/src/Gfx/DisplayMgr.cpp +++ b/src/Gfx/DisplayMgr.cpp @@ -1339,7 +1339,7 @@ void DisplayMgr::destroyUpdateTask() void DisplayMgr::processTask(void* parameters) { - DisplayMgr* tthis = reinterpret_cast(parameters); + DisplayMgr* tthis = static_cast(parameters); if ((nullptr != tthis) && (nullptr != tthis->m_processTaskSemaphore)) @@ -1376,7 +1376,7 @@ void DisplayMgr::processTask(void* parameters) void DisplayMgr::updateTask(void* parameters) { - DisplayMgr* tthis = reinterpret_cast(parameters); + DisplayMgr* tthis = static_cast(parameters); if ((nullptr != tthis) && (nullptr != tthis->m_updateTaskSemaphore)) diff --git a/src/Hal/ButtonDrv.cpp b/src/Hal/ButtonDrv.cpp index d79402c9..68c94b0d 100644 --- a/src/Hal/ButtonDrv.cpp +++ b/src/Hal/ButtonDrv.cpp @@ -285,7 +285,7 @@ void ButtonDrv::setState(ButtonId buttonId, ButtonState state) void ButtonDrv::buttonTask(void *parameters) { - ButtonDrv* buttonDrv = reinterpret_cast(parameters); + ButtonDrv* buttonDrv = static_cast(parameters); uint8_t buttonIdx = 0U; /* The ISR shall notify about on change to determine whether the @@ -409,7 +409,7 @@ void ButtonDrv::buttonTaskMainLoop() */ static void IRAM_ATTR isrButton(void* arg) { - ButtonId buttonId = *reinterpret_cast(arg); + ButtonId buttonId = *static_cast(arg); BaseType_t xHigherPriorityTaskWoken = pdFALSE; (void)xQueueSendFromISR(gxQueue, &buttonId, &xHigherPriorityTaskWoken ); diff --git a/src/Web/WebSocket.cpp b/src/Web/WebSocket.cpp index 61110574..9215c0ca 100644 --- a/src/Web/WebSocket.cpp +++ b/src/Web/WebSocket.cpp @@ -193,7 +193,7 @@ void WebSocketSrv::onEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, { /* Client connected */ case WS_EVT_CONNECT: - getInstance().onConnect(server, client, reinterpret_cast(arg)); + getInstance().onConnect(server, client, static_cast(arg)); break; /* Client disconnected */ @@ -208,12 +208,12 @@ void WebSocketSrv::onEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, /* Remote error */ case WS_EVT_ERROR: - getInstance().onError(server, client, *reinterpret_cast(arg), reinterpret_cast(data), len); + getInstance().onError(server, client, *static_cast(arg), reinterpret_cast(data), len); break; /* Data */ case WS_EVT_DATA: - getInstance().onData(server, client, reinterpret_cast(arg), data, len); + getInstance().onData(server, client, static_cast(arg), data, len); break; default: From 566dc262aa1d89eeb0cf1d4acc73e84d739113cc Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Fri, 24 Nov 2023 19:25:58 +0100 Subject: [PATCH 062/105] OpenWeather supports now the "Current weather", One-Call API v2.5 and One-Call API v3.0. It can be choosen by the source id. --- .../src/IOpenWeatherSource.h | 203 ++++++++++++ .../src/OpenWeatherCurrent.cpp | 177 +++++++++++ .../src/OpenWeatherCurrent.h | 273 ++++++++++++++++ .../src/OpenWeatherOneCall.cpp | 172 ++++++++++ .../src/OpenWeatherOneCall.h | 279 +++++++++++++++++ .../src/OpenWeatherPlugin.cpp | 296 +++++++++++------- lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h | 94 ++++-- .../web/OpenWeatherPlugin.html | 15 +- 8 files changed, 1357 insertions(+), 152 deletions(-) create mode 100644 lib/OpenWeatherPlugin/src/IOpenWeatherSource.h create mode 100644 lib/OpenWeatherPlugin/src/OpenWeatherCurrent.cpp create mode 100644 lib/OpenWeatherPlugin/src/OpenWeatherCurrent.h create mode 100644 lib/OpenWeatherPlugin/src/OpenWeatherOneCall.cpp create mode 100644 lib/OpenWeatherPlugin/src/OpenWeatherOneCall.h diff --git a/lib/OpenWeatherPlugin/src/IOpenWeatherSource.h b/lib/OpenWeatherPlugin/src/IOpenWeatherSource.h new file mode 100644 index 00000000..f5e85ae0 --- /dev/null +++ b/lib/OpenWeatherPlugin/src/IOpenWeatherSource.h @@ -0,0 +1,203 @@ +/* MIT License + * + * Copyright (c) 2019 - 2023 Andreas Merkle + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/******************************************************************************* + DESCRIPTION +*******************************************************************************/ +/** + * @brief OpenWeather source interface + * @author Andreas Merkle + * + * @addtogroup plugin + * + * @{ + */ + +#ifndef IOPENWEATHERSOURCE_H +#define IOPENWEATHERSOURCE_H + +/****************************************************************************** + * Compile Switches + *****************************************************************************/ + +/****************************************************************************** + * Includes + *****************************************************************************/ +#include +#include +#include + +/****************************************************************************** + * Macros + *****************************************************************************/ + +/** Default latitude (Berlin) */ +#define DEFAULT_LATITUDE "52.519" + +/** Default longitude (Berlin) */ +#define DEFAULT_LONGITUDE "13.376" + +/** Default units */ +#define DEFAULT_UNITS "metric" + +/****************************************************************************** + * Types and Classes + *****************************************************************************/ + +/** This type is the abstract interface for a OpenWeather source. */ +class IOpenWeatherSource +{ +public: + + /** + * Get the API key. + * + * @return API key + */ + virtual const String& getApiKey() const = 0; + + /** + * Set the API key. + * + * @param[in] apiKey The API key which to set. + */ + virtual void setApiKey(const String& apiKey) = 0; + + /** + * Get the latitude. + * + * @return Latitude + */ + virtual const String& getLatitude() const = 0; + + /** + * Set thel latidue. + * + * @param[in] latitude The latitude which to set. + */ + virtual void setLatitude(const String& latitude) = 0; + + /** + * Get the longitude. + * + * @return Longitude + */ + virtual const String& getLongitude() const = 0; + + /** + * Set the longitude. + * + * @param[in] longitude The longitude which to set. + */ + virtual void setLongitude(const String& longitude) = 0; + + /** + * Get the units which are used for temperature and + * wind speed. + * + * @return Units + */ + virtual const String& getUnits() const = 0; + + /** + * Set the units to use temperature and wind speed. + * + * @param[in] units The units which to set. + */ + virtual void setUnits(const String& units) = 0; + + /** + * Adds the URI to the base URL. + * + * @param[out] url The base URL to use. + */ + virtual void getUrl(String& url) const = 0; + + /** + * Get the filter which to apply on the response from the weather source. + * Its a positive filter, which means everything marked with true, will + * be used. Everything else will not be considered. + * + * @param[out] jsonFilterDoc The filter which to use. + */ + virtual void getFilter(JsonDocument& jsonFilterDoc) const = 0; + + /** + * Parse a response from the weather source and will update its internal + * data. + * + * @param[out] jsonDoc The JSON response which to parse. + */ + virtual void parse(const JsonDocument& jsonDoc) = 0; + + /** + * Get the temperature. + * Might be NaN in case no response was never parsed + * or its not supported by the OpenWeather source. + * + * @return Temperature, the unit is according to configuration. + */ + virtual float getTemperature() const = 0; + + /** + * Get the weather icon id. + * + * @return Weather icon id + */ + virtual const String& getWeatherIconId() const = 0; + + /** + * Get the UV-index. + * Might be NaN in case no response was never parsed + * or its not supported by the OpenWeather source. + * + * @return UV-index. + */ + virtual float getUvIndex() const = 0; + + /** + * Get the humidity. + * + * @return Humidity in %. + */ + virtual int getHumidity() const = 0; + + /** + * Get the wind speed. + * Might be NaN in case no response was never parsed + * or its not supported by the OpenWeather source. + * + * @return Wind speed, the unit is according to configuration. + */ + virtual float getWindSpeed() const = 0; + +protected: +}; + +/****************************************************************************** + * Functions + *****************************************************************************/ + +#endif /* IOPENWEATHERSOURCE_H */ + +/** @} */ \ No newline at end of file diff --git a/lib/OpenWeatherPlugin/src/OpenWeatherCurrent.cpp b/lib/OpenWeatherPlugin/src/OpenWeatherCurrent.cpp new file mode 100644 index 00000000..a1d63a1c --- /dev/null +++ b/lib/OpenWeatherPlugin/src/OpenWeatherCurrent.cpp @@ -0,0 +1,177 @@ +/* MIT License + * + * Copyright (c) 2019 - 2023 Andreas Merkle + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/******************************************************************************* + DESCRIPTION +*******************************************************************************/ +/** + * @brief OpenWeather source for current weather data + * @author Andreas Merkle + */ + +/****************************************************************************** + * Includes + *****************************************************************************/ +#include "OpenWeatherCurrent.h" + +/****************************************************************************** + * Compiler Switches + *****************************************************************************/ + +/****************************************************************************** + * Macros + *****************************************************************************/ + +/****************************************************************************** + * Types and classes + *****************************************************************************/ + +/****************************************************************************** + * Prototypes + *****************************************************************************/ + +/****************************************************************************** + * Local Variables + *****************************************************************************/ + +/****************************************************************************** + * Public Methods + *****************************************************************************/ + +void OpenWeatherCurrent::getUrl(String& url) const +{ + url += "/data/2.5/weather?lat="; + url += m_latitude; + url += "&lon="; + url += m_longitude; + url += "&units="; + url += m_units; + url += "&appid="; + url += m_apiKey; +} + +void OpenWeatherCurrent::getFilter(JsonDocument& jsonFilterDoc) const +{ + /* + + { + "coord": { + "lon": 10.99, + "lat": 44.34 + }, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + } + ], + "base": "stations", + "main": { + "temp": 298.48, + "feels_like": 298.74, + "temp_min": 297.56, + "temp_max": 300.05, + "pressure": 1015, + "humidity": 64, + "sea_level": 1015, + "grnd_level": 933 + }, + "visibility": 10000, + "wind": { + "speed": 0.62, + "deg": 349, + "gust": 1.18 + }, + "rain": { + "1h": 3.16 + }, + "clouds": { + "all": 100 + }, + "dt": 1661870592, + "sys": { + "type": 2, + "id": 2075663, + "country": "IT", + "sunrise": 1661834187, + "sunset": 1661882248 + }, + "timezone": 7200, + "id": 3163858, + "name": "Zocca", + "cod": 200 + } + + */ + + jsonFilterDoc["main"]["temp"] = true; + jsonFilterDoc["main"]["humidity"] = true; + jsonFilterDoc["wind"]["speed"] = true; + jsonFilterDoc["weather"][0]["icon"] = true; +} + +void OpenWeatherCurrent::parse(const JsonDocument& jsonDoc) +{ + JsonVariantConst jsonTemperature = jsonDoc["main"]["temp"]; + JsonVariantConst jsonHumidity = jsonDoc["main"]["humidity"]; + JsonVariantConst jsonWindSpeed = jsonDoc["wind"]["speed"]; + JsonVariantConst jsonIcon = jsonDoc["weather"][0]["icon"]; + + if (false == jsonTemperature.isNull()) + { + m_temperature = jsonTemperature.as(); + } + + if (false == jsonHumidity.isNull()) + { + m_humidity = jsonHumidity.as(); + } + + if (false == jsonWindSpeed.isNull()) + { + m_windSpeed = jsonWindSpeed.as(); + } + + if (false == jsonIcon.isNull()) + { + m_weatherIconId = jsonIcon.as(); + } +} + +/****************************************************************************** + * Protected Methods + *****************************************************************************/ + +/****************************************************************************** + * Private Methods + *****************************************************************************/ + +/****************************************************************************** + * External Functions + *****************************************************************************/ + +/****************************************************************************** + * Local Functions + *****************************************************************************/ diff --git a/lib/OpenWeatherPlugin/src/OpenWeatherCurrent.h b/lib/OpenWeatherPlugin/src/OpenWeatherCurrent.h new file mode 100644 index 00000000..ba2413d8 --- /dev/null +++ b/lib/OpenWeatherPlugin/src/OpenWeatherCurrent.h @@ -0,0 +1,273 @@ +/* MIT License + * + * Copyright (c) 2019 - 2023 Andreas Merkle + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/******************************************************************************* + DESCRIPTION +*******************************************************************************/ +/** + * @brief OpenWeather source for current weather data + * @author Andreas Merkle + * + * @addtogroup plugin + * + * @{ + */ + +#ifndef OPENWEATHERCURRENT_H +#define OPENWEATHERCURRENT_H + +/****************************************************************************** + * Compile Switches + *****************************************************************************/ + +/****************************************************************************** + * Includes + *****************************************************************************/ +#include "IOpenWeatherSource.h" +#include + +/****************************************************************************** + * Macros + *****************************************************************************/ + +/****************************************************************************** + * Types and Classes + *****************************************************************************/ + +/** + * The OpenWeather source for current weather data. + * https://openweathermap.org/current + */ +class OpenWeatherCurrent : public IOpenWeatherSource +{ +public: + + /** + * Constructs the OpenWeather source. + */ + OpenWeatherCurrent() : + IOpenWeatherSource(), + m_apiKey(), + m_latitude(DEFAULT_LATITUDE), + m_longitude(DEFAULT_LONGITUDE), + m_units(DEFAULT_UNITS), + m_temperature(std::numeric_limits::quiet_NaN()), + m_weatherIconId(), + m_uvIndex(std::numeric_limits::quiet_NaN()), + m_humidity(0), + m_windSpeed(std::numeric_limits::quiet_NaN()) + { + } + + /** + * Destroys the OpenWeather source. + */ + virtual ~OpenWeatherCurrent() + { + } + + /** + * Get the API key. + * + * @return API key + */ + const String& getApiKey() const final + { + return m_apiKey; + } + + /** + * Set the API key. + * + * @param[in] apiKey The API key which to set. + */ + void setApiKey(const String& apiKey) final + { + m_apiKey = apiKey; + } + + /** + * Get the latitude. + * + * @return Latitude + */ + const String& getLatitude() const final + { + return m_latitude; + } + + /** + * Set thel latidue. + * + * @param[in] latitude The latitude which to set. + */ + void setLatitude(const String& latitude) final + { + m_latitude = latitude; + } + + /** + * Get the longitude. + * + * @return Longitude + */ + const String& getLongitude() const final + { + return m_longitude; + } + + /** + * Set the longitude. + * + * @param[in] longitude The longitude which to set. + */ + void setLongitude(const String& longitude) final + { + m_longitude = longitude; + } + + /** + * Get the units which are used for temperature and + * wind speed. + * + * @return Units + */ + const String& getUnits() const final + { + return m_units; + } + + /** + * Set the units to use temperature and wind speed. + * + * @param[in] units The units which to set. + */ + void setUnits(const String& units) final + { + m_units = units; + } + + /** + * Adds the URI to the base URL. + * + * @param[out] url The base URL to use. + */ + void getUrl(String& url) const final; + + /** + * Get the filter which to apply on the response from the weather source. + * Its a positive filter, which means everything marked with true, will + * be used. Everything else will not be considered. + * + * @param[out] jsonFilterDoc The filter which to use. + */ + void getFilter(JsonDocument& jsonFilterDoc) const final; + + /** + * Parse a response from the weather source and will update its internal + * data. + * + * @param[out] jsonDoc The JSON response which to parse. + */ + void parse(const JsonDocument& jsonDoc) final; + + /** + * Get the temperature. + * Might be NaN in case no response was never parsed + * or its not supported by the OpenWeather source. + * + * @return Temperature, the unit is according to configuration. + */ + float getTemperature() const final + { + return m_temperature; + } + + /** + * Get the weather icon id. + * + * @return Weather icon id + */ + const String& getWeatherIconId() const final + { + return m_weatherIconId; + } + + /** + * Get the UV-index. + * Might be NaN in case no response was never parsed + * or its not supported by the OpenWeather source. + * + * @return UV-index. + */ + float getUvIndex() const final + { + return m_uvIndex; + } + + /** + * Get the humidity. + * + * @return Humidity in %. + */ + int getHumidity() const final + { + return m_humidity; + } + + /** + * Get the wind speed. + * Might be NaN in case no response was never parsed + * or its not supported by the OpenWeather source. + * + * @return Wind speed, the unit is according to configuration. + */ + float getWindSpeed() const final + { + return m_windSpeed; + } + +private: + + String m_apiKey; /**< OpenWeather API Key */ + String m_latitude; /**< The latitude. */ + String m_longitude; /**< The longitude. */ + String m_units; /**< The units to use for temperature and wind speed. */ + float m_temperature; /**< Temperature, unit according to configuration. */ + String m_weatherIconId; /**< Weather icon id. */ + float m_uvIndex; /**< UV-index */ + int m_humidity; /**< Humidity in %. */ + float m_windSpeed; /**< Wind speed, unit according to configuration. */ + + /* Not allowed. */ + OpenWeatherCurrent(const OpenWeatherCurrent& other); + OpenWeatherCurrent& operator=(const OpenWeatherCurrent& other); +}; + +/****************************************************************************** + * Functions + *****************************************************************************/ + +#endif /* OPENWEATHERCURRENT_H */ + +/** @} */ \ No newline at end of file diff --git a/lib/OpenWeatherPlugin/src/OpenWeatherOneCall.cpp b/lib/OpenWeatherPlugin/src/OpenWeatherOneCall.cpp new file mode 100644 index 00000000..93583247 --- /dev/null +++ b/lib/OpenWeatherPlugin/src/OpenWeatherOneCall.cpp @@ -0,0 +1,172 @@ +/* MIT License + * + * Copyright (c) 2019 - 2023 Andreas Merkle + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/******************************************************************************* + DESCRIPTION +*******************************************************************************/ +/** + * @brief OpenWeather source for One-Call API + * @author Andreas Merkle + */ + +/****************************************************************************** + * Includes + *****************************************************************************/ +#include "OpenWeatherOneCall.h" + +/****************************************************************************** + * Compiler Switches + *****************************************************************************/ + +/****************************************************************************** + * Macros + *****************************************************************************/ + +/****************************************************************************** + * Types and classes + *****************************************************************************/ + +/****************************************************************************** + * Prototypes + *****************************************************************************/ + +/****************************************************************************** + * Local Variables + *****************************************************************************/ + +/****************************************************************************** + * Public Methods + *****************************************************************************/ + +void OpenWeatherOneCall::getUrl(String& url) const +{ + url += "/data/"; + url += m_oneCallApiVersion; + url += "/onecall?lat="; + url += m_latitude; + url += "&lon="; + url += m_longitude; + url += "&units="; + url += m_units; + url += "&appid="; + url += m_apiKey; + url += "&exclude=minutely,hourly,daily,alerts"; +} + +void OpenWeatherOneCall::getFilter(JsonDocument& jsonFilterDoc) const +{ + JsonObject jsonCurrent = jsonFilterDoc.createNestedObject("current"); + + /* + + { + "lat": 33.44, + "lon": -94.04, + "timezone": "America/Chicago", + "timezone_offset": -21600, + "current": { + "dt": 1618317040, + "sunrise": 1618282134, + "sunset": 1618333901, + "temp": 284.07, + "feels_like": 282.84, + "pressure": 1019, + "humidity": 62, + "dew_point": 277.08, + "uvi": 0.89, + "clouds": 0, + "visibility": 10000, + "wind_speed": 6, + "wind_deg": 300, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "rain": { + "1h": 0.21 + } + } + + */ + + jsonCurrent["temp"] = true; + jsonCurrent["uvi"] = true; + jsonCurrent["humidity"] = true; + jsonCurrent["wind_speed"] = true; + jsonCurrent["weather"][0]["icon"] = true; +} + +void OpenWeatherOneCall::parse(const JsonDocument& jsonDoc) +{ + JsonVariantConst jsonCurrent = jsonDoc["current"]; + JsonVariantConst jsonTemperature = jsonCurrent["temp"]; + JsonVariantConst jsonUvi = jsonCurrent["uvi"]; + JsonVariantConst jsonHumidity = jsonCurrent["humidity"]; + JsonVariantConst jsonWindSpeed = jsonCurrent["wind_speed"]; + JsonVariantConst jsonIcon = jsonCurrent["weather"][0]["icon"]; + + if (false == jsonTemperature.isNull()) + { + m_temperature = jsonTemperature.as(); + } + + if (false == jsonUvi.isNull()) + { + m_uvIndex = jsonUvi.as(); + } + + if (false == jsonHumidity.isNull()) + { + m_humidity = jsonHumidity.as(); + } + + if (false == jsonWindSpeed.isNull()) + { + m_windSpeed = jsonWindSpeed.as(); + } + + if (false == jsonIcon.isNull()) + { + m_weatherIconId = jsonIcon.as(); + } +} + +/****************************************************************************** + * Protected Methods + *****************************************************************************/ + +/****************************************************************************** + * Private Methods + *****************************************************************************/ + +/****************************************************************************** + * External Functions + *****************************************************************************/ + +/****************************************************************************** + * Local Functions + *****************************************************************************/ diff --git a/lib/OpenWeatherPlugin/src/OpenWeatherOneCall.h b/lib/OpenWeatherPlugin/src/OpenWeatherOneCall.h new file mode 100644 index 00000000..cab0893a --- /dev/null +++ b/lib/OpenWeatherPlugin/src/OpenWeatherOneCall.h @@ -0,0 +1,279 @@ +/* MIT License + * + * Copyright (c) 2019 - 2023 Andreas Merkle + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/******************************************************************************* + DESCRIPTION +*******************************************************************************/ +/** + * @brief OpenWeather source for One-Call API + * @author Andreas Merkle + * + * @addtogroup plugin + * + * @{ + */ + +#ifndef OPENWEATHER_ONECALL_H +#define OPENWEATHER_ONECALL_H + +/****************************************************************************** + * Compile Switches + *****************************************************************************/ + +/****************************************************************************** + * Includes + *****************************************************************************/ +#include "IOpenWeatherSource.h" +#include + +/****************************************************************************** + * Macros + *****************************************************************************/ + +/****************************************************************************** + * Types and Classes + *****************************************************************************/ + +/** + * The OpenWeather source for One-Call API + * v2.5 see https://openweathermap.org/api/one-call-api + * v3.0 see https://openweathermap.org/api/one-call-3 + */ +class OpenWeatherOneCall : public IOpenWeatherSource +{ +public: + + /** + * Constructs the OpenWeather source. + * + * @param[in] oneCallApiVersion Version of the One-Call API to use. Supported: "2.5" and "3.0" + */ + OpenWeatherOneCall(const String& oneCallApiVersion) : + IOpenWeatherSource(), + m_oneCallApiVersion(oneCallApiVersion), + m_apiKey(), + m_latitude(DEFAULT_LATITUDE), + m_longitude(DEFAULT_LONGITUDE), + m_units(DEFAULT_UNITS), + m_temperature(std::numeric_limits::quiet_NaN()), + m_weatherIconId(), + m_uvIndex(std::numeric_limits::quiet_NaN()), + m_humidity(0), + m_windSpeed(std::numeric_limits::quiet_NaN()) + { + } + + /** + * Destroys the OpenWeather source. + */ + virtual ~OpenWeatherOneCall() + { + } + + /** + * Get the API key. + * + * @return API key + */ + const String& getApiKey() const final + { + return m_apiKey; + } + + /** + * Set the API key. + * + * @param[in] apiKey The API key which to set. + */ + void setApiKey(const String& apiKey) final + { + m_apiKey = apiKey; + } + + /** + * Get the latitude. + * + * @return Latitude + */ + const String& getLatitude() const final + { + return m_latitude; + } + + /** + * Set thel latidue. + * + * @param[in] latitude The latitude which to set. + */ + void setLatitude(const String& latitude) final + { + m_latitude = latitude; + } + + /** + * Get the longitude. + * + * @return Longitude + */ + const String& getLongitude() const final + { + return m_longitude; + } + + /** + * Set the longitude. + * + * @param[in] longitude The longitude which to set. + */ + void setLongitude(const String& longitude) final + { + m_longitude = longitude; + } + + /** + * Get the units which are used for temperature and + * wind speed. + * + * @return Units + */ + const String& getUnits() const final + { + return m_units; + } + + /** + * Set the units to use temperature and wind speed. + * + * @param[in] units The units which to set. + */ + void setUnits(const String& units) final + { + m_units = units; + } + + /** + * Adds the URI to the base URL. + * + * @param[out] url The base URL to use. + */ + void getUrl(String& url) const final; + + /** + * Get the filter which to apply on the response from the weather source. + * Its a positive filter, which means everything marked with true, will + * be used. Everything else will not be considered. + * + * @param[out] jsonFilterDoc The filter which to use. + */ + void getFilter(JsonDocument& jsonFilterDoc) const final; + + /** + * Parse a response from the weather source and will update its internal + * data. + * + * @param[out] jsonDoc The JSON response which to parse. + */ + void parse(const JsonDocument& jsonDoc) final; + + /** + * Get the temperature. + * Might be NaN in case no response was never parsed + * or its not supported by the OpenWeather source. + * + * @return Temperature, the unit is according to configuration. + */ + float getTemperature() const final + { + return m_temperature; + } + + /** + * Get the weather icon id. + * + * @return Weather icon id + */ + const String& getWeatherIconId() const final + { + return m_weatherIconId; + } + + /** + * Get the UV-index. + * Might be NaN in case no response was never parsed + * or its not supported by the OpenWeather source. + * + * @return UV-index. + */ + float getUvIndex() const final + { + return m_uvIndex; + } + + /** + * Get the humidity. + * + * @return Humidity in %. + */ + int getHumidity() const final + { + return m_humidity; + } + + /** + * Get the wind speed. + * Might be NaN in case no response was never parsed + * or its not supported by the OpenWeather source. + * + * @return Wind speed, the unit is according to configuration. + */ + float getWindSpeed() const final + { + return m_windSpeed; + } + +private: + + String m_oneCallApiVersion; /**< OpenWeather One-Call API version */ + String m_apiKey; /**< OpenWeather API Key */ + String m_latitude; /**< The latitude. */ + String m_longitude; /**< The longitude. */ + String m_units; /**< The units to use for temperature and wind speed. */ + float m_temperature; /**< Temperature, unit according to configuration. */ + String m_weatherIconId; /**< Weather icon id. */ + float m_uvIndex; /**< UV-index */ + int m_humidity; /**< Humidity in %. */ + float m_windSpeed; /**< Wind speed, unit according to configuration. */ + + /* Not allowed. */ + OpenWeatherOneCall(); + OpenWeatherOneCall(const OpenWeatherOneCall& other); + OpenWeatherOneCall& operator=(const OpenWeatherOneCall& other); +}; + +/****************************************************************************** + * Functions + *****************************************************************************/ + +#endif /* OPENWEATHER_ONECALL_H */ + +/** @} */ \ No newline at end of file diff --git a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp index a84fe39e..40612a26 100644 --- a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp +++ b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp @@ -33,10 +33,13 @@ * Includes *****************************************************************************/ #include "OpenWeatherPlugin.h" +#include "OpenWeatherCurrent.h" +#include "OpenWeatherOneCall.h" #include #include #include +#include /****************************************************************************** * Compiler Switches @@ -143,6 +146,7 @@ bool OpenWeatherPlugin::setTopic(const String& topic, const JsonObjectConst& val const size_t JSON_DOC_SIZE = 512U; DynamicJsonDocument jsonDoc(JSON_DOC_SIZE); JsonObject jsonCfg = jsonDoc.to(); + JsonVariantConst jsonSourceId = value["sourceId"]; JsonVariantConst jsonApiKey = value["apiKey"]; JsonVariantConst jsonLatitude = value["latitude"]; JsonVariantConst jsonLongitude = value["longitude"]; @@ -160,6 +164,12 @@ bool OpenWeatherPlugin::setTopic(const String& topic, const JsonObjectConst& val * The type check will follow in the setConfiguration(). */ + if (false == jsonSourceId.isNull()) + { + jsonCfg["sourceId"] = jsonSourceId.as(); + isSuccessful = true; + } + if (false == jsonApiKey.isNull()) { jsonCfg["apiKey"] = jsonApiKey.as(); @@ -472,6 +482,38 @@ void OpenWeatherPlugin::update(YAGfx& gfx) * Private Methods *****************************************************************************/ +void OpenWeatherPlugin::createOpenWeatherSource(OpenWeatherSource id) +{ + destroyOpenWeatherSource(); + + switch(id) + { + case OPENWEATHER_SOURCE_CURRENT: + m_source = new (std::nothrow) OpenWeatherCurrent(); + break; + + case OPENWEATHER_SOURCE_ONE_CALL_25: + m_source = new (std::nothrow) OpenWeatherOneCall("2.5"); + break; + + case OPENWEATHER_SOURCE_ONE_CALL_30: + m_source = new (std::nothrow) OpenWeatherOneCall("3.0"); + break; + + default: + break; + } +} + +void OpenWeatherPlugin::destroyOpenWeatherSource() +{ + if (nullptr != m_source) + { + delete m_source; + m_source = nullptr; + } +} + void OpenWeatherPlugin::requestStoreToPersistentMemory() { MutexGuard guard(m_mutex); @@ -483,23 +525,36 @@ void OpenWeatherPlugin::getConfiguration(JsonObject& jsonCfg) const { MutexGuard guard(m_mutex); - jsonCfg["apiKey"] = m_apiKey; - jsonCfg["latitude"] = m_latitude; - jsonCfg["longitude"] = m_longitude; - jsonCfg["other"] = static_cast(m_additionalInformation); - jsonCfg["units"] = m_units; + if (nullptr == m_source) + { + LOG_ERROR("No OpenWeather source available."); + } + else + { + jsonCfg["sourceId"] = static_cast(m_sourceId); + jsonCfg["apiKey"] = m_source->getApiKey(); + jsonCfg["latitude"] = m_source->getLatitude(); + jsonCfg["longitude"] = m_source->getLongitude(); + jsonCfg["units"] = m_source->getUnits(); + jsonCfg["other"] = static_cast(m_additionalInformation); + } } bool OpenWeatherPlugin::setConfiguration(JsonObjectConst& jsonCfg) { bool status = false; + JsonVariantConst jsonSourceId = jsonCfg["sourceId"]; JsonVariantConst jsonApiKey = jsonCfg["apiKey"]; JsonVariantConst jsonLatitude = jsonCfg["latitude"]; JsonVariantConst jsonLongitude = jsonCfg["longitude"]; JsonVariantConst jsonOther = jsonCfg["other"]; JsonVariantConst jsonUnits = jsonCfg["units"]; - if (false == jsonApiKey.is()) + if (false == jsonSourceId.is()) + { + LOG_WARNING("Source id not found or invalid type."); + } + else if (false == jsonApiKey.is()) { LOG_WARNING("API key not found or invalid type."); } @@ -521,13 +576,29 @@ bool OpenWeatherPlugin::setConfiguration(JsonObjectConst& jsonCfg) } else { - MutexGuard guard(m_mutex); + MutexGuard guard(m_mutex); + OpenWeatherSource sourceId = static_cast(jsonSourceId.as()); + + if (m_sourceId != sourceId) + { + destroyOpenWeatherSource(); + m_sourceId = sourceId; + createOpenWeatherSource(m_sourceId); + } + + if (nullptr == m_source) + { + LOG_ERROR("No OpenWeather source available."); + } + else + { + m_source->setApiKey(jsonApiKey.as()); + m_source->setLatitude(jsonLatitude.as()); + m_source->setLongitude(jsonLongitude.as()); + m_source->setUnits(jsonUnits.as()); + } - m_apiKey = jsonApiKey.as(); - m_latitude = jsonLatitude.as(); - m_longitude = jsonLongitude.as(); m_additionalInformation = static_cast(jsonOther.as()); - m_units = jsonUnits.as(); /* Force update on display */ m_requestTimer.start(UPDATE_PERIOD_SHORT); @@ -623,17 +694,17 @@ void OpenWeatherPlugin::updateDisplay(bool force) { if (true == m_hasWeatherIconChanged) { - String spriteSheetPath = m_currentWeatherIcon.substring(0U, m_currentWeatherIcon.length() - strlen(FILE_EXT_BITMAP)) + FILE_EXT_SPRITE_SHEET; + String spriteSheetPath = m_currentWeatherIconFullPath.substring(0U, m_currentWeatherIconFullPath.length() - strlen(FILE_EXT_BITMAP)) + FILE_EXT_SPRITE_SHEET; /* If there is an icon in the filesystem, it will be loaded otherwise * the standard icon. First check whether it is a animated sprite sheet * and if not, try to load just the bitmap image. */ - if (false == m_bitmapWidget.loadSpriteSheet(FILESYSTEM, spriteSheetPath, m_currentWeatherIcon)) + if (false == m_bitmapWidget.loadSpriteSheet(FILESYSTEM, spriteSheetPath, m_currentWeatherIconFullPath)) { - if (false == m_bitmapWidget.load(FILESYSTEM, m_currentWeatherIcon)) + if (false == m_bitmapWidget.load(FILESYSTEM, m_currentWeatherIconFullPath)) { - LOG_WARNING("Icon doesn't exists: %s", m_currentWeatherIcon.c_str()); + LOG_WARNING("Icon doesn't exists: %s", m_currentWeatherIconFullPath.c_str()); (void)m_bitmapWidget.load(FILESYSTEM, IMAGE_PATH_STD_ICON); } @@ -701,23 +772,15 @@ bool OpenWeatherPlugin::startHttpRequest() { bool status = false; - if ((false == m_latitude.isEmpty()) && - (false == m_longitude.isEmpty()) && - (false == m_units.isEmpty()) && - (false == m_apiKey.isEmpty())) + if ((nullptr != m_source) && + (false == m_source->getApiKey().isEmpty()) && + (false == m_source->getLatitude().isEmpty()) && + (false == m_source->getLongitude().isEmpty()) && + (false == m_source->getUnits().isEmpty())) { String url = OPEN_WEATHER_BASE_URI; - /* Get current weather information: https://openweathermap.org/api/one-call-api */ - url += "/data/2.5/onecall?lat="; - url += m_latitude; - url += "&lon="; - url += m_longitude; - url += "&units="; - url += m_units; - url += "&appid="; - url += m_apiKey; - url += "&exclude=minutely,hourly,daily,alerts"; + m_source->getUrl(url); if (true == m_client.begin(url)) { @@ -744,48 +807,45 @@ void OpenWeatherPlugin::initHttpClient() m_client.regOnResponse( [this](const HttpResponse& rsp) { - const size_t JSON_DOC_SIZE = 256U; - DynamicJsonDocument* jsonDoc = new(std::nothrow) DynamicJsonDocument(JSON_DOC_SIZE); - - if (nullptr != jsonDoc) + if (nullptr != m_source) { - size_t payloadSize = 0U; - const void* vPayload = rsp.getPayload(payloadSize); - const char* payload = static_cast(vPayload); - const size_t FILTER_SIZE = 128U; - StaticJsonDocument filter; - JsonObject filterCurrent = filter.createNestedObject("current"); - DeserializationError error; - - /* See https://openweathermap.org/api/one-call-api for an example of API response. */ - filterCurrent["temp"] = true; - filterCurrent["uvi"] = true; - filterCurrent["humidity"] = true; - filterCurrent["wind_speed"] = true; - filterCurrent["weather"][0]["icon"] = true; - - if (true == filter.overflowed()) + const size_t JSON_DOC_SIZE = 256U; + DynamicJsonDocument* jsonDoc = new(std::nothrow) DynamicJsonDocument(JSON_DOC_SIZE); + + if (nullptr != jsonDoc) { - LOG_ERROR("Less memory for filter available."); - } + size_t payloadSize = 0U; + const void* vPayload = rsp.getPayload(payloadSize); + const char* payload = static_cast(vPayload); + const size_t FILTER_SIZE = 128U; + StaticJsonDocument jsonFilterDoc; + DeserializationError error; - error = deserializeJson(*jsonDoc, payload, payloadSize, DeserializationOption::Filter(filter)); + m_source->getFilter(jsonFilterDoc); - if (DeserializationError::Ok != error.code()) - { - LOG_WARNING("JSON parse error: %s", error.c_str()); - } - else - { - Msg msg; + if (true == jsonFilterDoc.overflowed()) + { + LOG_ERROR("Less memory for filter available."); + } - msg.type = MSG_TYPE_RSP; - msg.rsp = jsonDoc; + error = deserializeJson(*jsonDoc, payload, payloadSize, DeserializationOption::Filter(jsonFilterDoc)); - if (false == this->m_taskProxy.send(msg)) + if (DeserializationError::Ok != error.code()) { - delete jsonDoc; - jsonDoc = nullptr; + LOG_WARNING("JSON parse error: %s", error.c_str()); + } + else + { + Msg msg; + + msg.type = MSG_TYPE_RSP; + msg.rsp = jsonDoc; + + if (false == this->m_taskProxy.send(msg)) + { + delete jsonDoc; + jsonDoc = nullptr; + } } } } @@ -817,58 +877,54 @@ void OpenWeatherPlugin::initHttpClient() void OpenWeatherPlugin::handleWebResponse(DynamicJsonDocument& jsonDoc) { - JsonVariantConst jsonCurrent = jsonDoc["current"]; - JsonVariantConst jsonTemperature = jsonCurrent["temp"]; - JsonVariantConst jsonUvi = jsonCurrent["uvi"]; - JsonVariantConst jsonHumidity = jsonCurrent["humidity"]; - JsonVariantConst jsonWindSpeed = jsonCurrent["wind_speed"]; - JsonVariantConst jsonIcon = jsonCurrent["weather"][0]["icon"]; - - if (false == jsonTemperature.is()) + if (nullptr != m_source) { - LOG_WARNING("JSON temp type mismatch or missing."); + m_source->parse(jsonDoc); + prepareDataToShow(); } - else if (false == jsonUvi.is()) - { - LOG_WARNING("JSON uvi type mismatch or missing."); - } - else if (false == jsonHumidity.is()) - { - LOG_WARNING("JSON humidity type mismatch or missing."); - } - else if (false == jsonWindSpeed.is()) - { - LOG_WARNING("JSON wind_speed type mismatch or missing."); - } - else if (false == jsonIcon.is()) - { - LOG_WARNING("JSON weather icon id type mismatch or missing."); - } - else +} + +void OpenWeatherPlugin::prepareDataToShow() +{ + if (nullptr != m_source) { - float temperature = jsonTemperature.as(); - String weatherIconId = jsonIcon.as(); - float uvIndex = jsonUvi.as(); - int humidity = jsonHumidity.as(); - float windSpeed = jsonWindSpeed.as(); + float temperature = m_source->getTemperature(); + String weatherIconId = m_source->getWeatherIconId(); + float uvIndex = m_source->getUvIndex(); + int humidity = m_source->getHumidity(); + float windSpeed = m_source->getWindSpeed(); char tempReducedPrecison[6] = { 0 }; char windReducedPrecison[5] = { 0 }; - String weatherConditionIcon; + String weatherConditionIconFullPath; /* Generate UV-Index string and adapt color of string accordingly. */ - m_currentUvIndex = "\\calign"; - m_currentUvIndex += uvIndexToColor(static_cast(uvIndex)); - m_currentUvIndex += uvIndex; + if (true == std::isnan(uvIndex)) + { + m_currentUvIndex = "\\calignN/A"; + } + else + { + m_currentUvIndex = "\\calign"; + m_currentUvIndex += uvIndexToColor(static_cast(uvIndex)); + m_currentUvIndex += uvIndex; + } - const char* reducePrecision = (temperature < -9.9F) ? "%.0f" : "%.1f"; + if (true == std::isnan(temperature)) + { + m_currentUvIndex = "\\calignN/A"; + } + else + { + const char* reducePrecision = (temperature < -9.9F) ? "%.0f" : "%.1f"; - /* Generate temperature string with reduced precision and add unit °C/°F. */ - (void)snprintf(tempReducedPrecison, sizeof(tempReducedPrecison), reducePrecision, temperature); + /* Generate temperature string with reduced precision and add unit °C/°F. */ + (void)snprintf(tempReducedPrecison, sizeof(tempReducedPrecison), reducePrecision, temperature); - m_currentTemp = "\\calign"; - m_currentTemp += tempReducedPrecison; - m_currentTemp += "\x8E"; - m_currentTemp += (m_units == "metric")?"C":"F"; + m_currentTemp = "\\calign"; + m_currentTemp += tempReducedPrecison; + m_currentTemp += "\x8E"; + m_currentTemp += (m_source->getUnits() == "metric") ? "C" : "F"; + } /* Generate humidity string */ m_currentHumidity = "\\calign"; @@ -876,10 +932,18 @@ void OpenWeatherPlugin::handleWebResponse(DynamicJsonDocument& jsonDoc) m_currentHumidity += "%"; /* Generate windapeed string and add unit.*/ - (void)snprintf(windReducedPrecison, sizeof(windReducedPrecison), "%.1f", windSpeed); - m_currentWindspeed = "\\calign"; - m_currentWindspeed += windReducedPrecison; - m_currentWindspeed += "m/s"; + if (true == std::isnan(windSpeed)) + { + m_currentUvIndex = "\\calignN/A"; + } + else + { + (void)snprintf(windReducedPrecison, sizeof(windReducedPrecison), "%.1f", windSpeed); + + m_currentWindspeed = "\\calign"; + m_currentWindspeed += windReducedPrecison; + m_currentWindspeed += "m/s"; + } /* Handle icon depended on weather icon id. * See https://openweathermap.org/weather-conditions @@ -888,20 +952,20 @@ void OpenWeatherPlugin::handleWebResponse(DynamicJsonDocument& jsonDoc) * If not, check for a generic weather icon. * If this is not available too, use the standard OpenWeather icon. */ - weatherConditionIcon = IMAGE_PATH + weatherIconId + FILE_EXT_BITMAP; - if (false == FILESYSTEM.exists(weatherConditionIcon)) + weatherConditionIconFullPath = IMAGE_PATH + weatherIconId + FILE_EXT_BITMAP; + if (false == FILESYSTEM.exists(weatherConditionIconFullPath)) { - weatherConditionIcon = IMAGE_PATH + weatherIconId.substring(0U, weatherIconId.length() - 1U); - weatherConditionIcon += FILE_EXT_BITMAP; + weatherConditionIconFullPath = IMAGE_PATH + weatherIconId.substring(0U, weatherIconId.length() - 1U); + weatherConditionIconFullPath += FILE_EXT_BITMAP; } /* If there is really a change, the display shall be updated otherwise * not to not destroy running animations. */ - if (weatherConditionIcon != m_currentWeatherIcon) + if (weatherConditionIconFullPath != m_currentWeatherIconFullPath) { m_hasWeatherIconChanged = true; - m_currentWeatherIcon = weatherConditionIcon; + m_currentWeatherIconFullPath = weatherConditionIconFullPath; } updateDisplay(true); diff --git a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h index 704d4124..3cd8a5d3 100644 --- a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h +++ b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h @@ -46,6 +46,7 @@ #include #include "Plugin.hpp" #include "AsyncHttpClient.h" +#include "IOpenWeatherSource.h" #include #include @@ -69,6 +70,16 @@ class OpenWeatherPlugin : public Plugin, private PluginConfigFsHandler { public: + /** + * The supported OpenWeather sources. + */ + enum OpenWeatherSource + { + OPENWEATHER_SOURCE_CURRENT = 0, /**< Current weather data */ + OPENWEATHER_SOURCE_ONE_CALL_25, /**< OpenWeather One-Call API v2.5 */ + OPENWEATHER_SOURCE_ONE_CALL_30, /**< OpenWeather One-Call API v3.0 */ + }; + /** * Enumeration to choose an additional weather information to be displayed. */ @@ -94,11 +105,9 @@ class OpenWeatherPlugin : public Plugin, private PluginConfigFsHandler m_iconCanvas(), m_bitmapWidget(), m_textWidget("\\calign?"), - m_apiKey(""), - m_latitude("48.858"),/* Example data */ - m_longitude("2.295"),/* Example data */ + m_sourceId(OPENWEATHER_SOURCE_ONE_CALL_25), + m_source(nullptr), m_additionalInformation(OTHER_WEATHER_INFO_OFF), - m_units("metric"), m_configurationFilename(), m_client(), m_requestTimer(), @@ -106,7 +115,7 @@ class OpenWeatherPlugin : public Plugin, private PluginConfigFsHandler m_mutex(), m_isConnectionError(false), m_currentTemp("\\calign?"), - m_currentWeatherIcon(IMAGE_PATH_STD_ICON), + m_currentWeatherIconFullPath(IMAGE_PATH_STD_ICON), m_currentUvIndex("\\calign?"), m_currentHumidity("\\calign?"), m_currentWindspeed("\\calign?"), @@ -121,6 +130,7 @@ class OpenWeatherPlugin : public Plugin, private PluginConfigFsHandler m_taskProxy() { (void)m_mutex.create(); + createOpenWeatherSource(m_sourceId); /* Default */ } /** @@ -138,6 +148,7 @@ class OpenWeatherPlugin : public Plugin, private PluginConfigFsHandler m_client.end(); clearQueue(); + destroyOpenWeatherSource(); m_mutex.destroy(); } @@ -457,35 +468,33 @@ class OpenWeatherPlugin : public Plugin, private PluginConfigFsHandler */ static const uint32_t CFG_RELOAD_PERIOD = SIMPLE_TIMER_SECONDS(30U); - Fonts::FontType m_fontType; /**< Font type which shall be used if there is no conflict with the layout. */ - WidgetGroup m_textCanvas; /**< Canvas used for the text widget. */ - WidgetGroup m_iconCanvas; /**< Canvas used for the bitmap widget. */ - BitmapWidget m_bitmapWidget; /**< Bitmap widget, used to show the icon. */ - TextWidget m_textWidget; /**< Text widget, used for showing the text. */ - String m_apiKey; /**< OpenWeather API Key */ - String m_latitude; /**< The latitude. */ - String m_longitude; /**< The longitude. */ - OtherWeatherInformation m_additionalInformation; /**< The configured additional weather information. */ - String m_units; /**< The units. */ - String m_configurationFilename; /**< String used for specifying the configuration filename. */ - AsyncHttpClient m_client; /**< Asynchronous HTTP client. */ - SimpleTimer m_requestTimer; /**< Timer used for cyclic request of new data. */ - SimpleTimer m_updateContentTimer; /**< Timer used for duration ticks in [s]. */ - mutable MutexRecursive m_mutex; /**< Mutex to protect against concurrent access. */ - bool m_isConnectionError; /**< Is connection error happened? */ - String m_currentTemp; /**< The current temperature. */ - String m_currentWeatherIcon; /**< The current weather condition icon. */ - String m_currentUvIndex; /**< The current UV index. */ - String m_currentHumidity; /**< The current humidity. */ - String m_currentWindspeed; /**< The current wind speed. */ - bool m_hasWeatherIconChanged; /**< Has weather icon changed? If yes, it will be updated otherwise skipped to not disturb running animations. */ - const ISlotPlugin* m_slotInterf; /**< Slot interface */ - uint8_t m_durationCounter; /**< Variable to count the Plugin duration in DURATION_TICK_PERIOD ticks. */ - bool m_isUpdateAvailable; /**< Flag to indicate an updated date value. */ - SimpleTimer m_cfgReloadTimer; /**< Timer is used to cyclic reload the configuration from persistent memory. */ - bool m_storeConfigReq; /**< Is requested to store the configuration in persistent memory? */ - bool m_reloadConfigReq; /**< Is requested to reload the configuration from persistent memory? */ - bool m_hasTopicChanged; /**< Has the topic content changed? */ + Fonts::FontType m_fontType; /**< Font type which shall be used if there is no conflict with the layout. */ + WidgetGroup m_textCanvas; /**< Canvas used for the text widget. */ + WidgetGroup m_iconCanvas; /**< Canvas used for the bitmap widget. */ + BitmapWidget m_bitmapWidget; /**< Bitmap widget, used to show the icon. */ + TextWidget m_textWidget; /**< Text widget, used for showing the text. */ + OpenWeatherSource m_sourceId; /**< OpenWeather source id. */ + IOpenWeatherSource* m_source; /**< OpenWeather source to use to retrieve weather information. */ + OtherWeatherInformation m_additionalInformation; /**< The configured additional weather information. */ + String m_configurationFilename; /**< String used for specifying the configuration filename. */ + AsyncHttpClient m_client; /**< Asynchronous HTTP client. */ + SimpleTimer m_requestTimer; /**< Timer used for cyclic request of new data. */ + SimpleTimer m_updateContentTimer; /**< Timer used for duration ticks in [s]. */ + mutable MutexRecursive m_mutex; /**< Mutex to protect against concurrent access. */ + bool m_isConnectionError; /**< Is connection error happened? */ + String m_currentTemp; /**< The current temperature. */ + String m_currentWeatherIconFullPath; /**< The current weather condition icon full path. */ + String m_currentUvIndex; /**< The current UV index. */ + String m_currentHumidity; /**< The current humidity. */ + String m_currentWindspeed; /**< The current wind speed. */ + bool m_hasWeatherIconChanged; /**< Has weather icon changed? If yes, it will be updated otherwise skipped to not disturb running animations. */ + const ISlotPlugin* m_slotInterf; /**< Slot interface */ + uint8_t m_durationCounter; /**< Variable to count the Plugin duration in DURATION_TICK_PERIOD ticks. */ + bool m_isUpdateAvailable; /**< Flag to indicate an updated date value. */ + SimpleTimer m_cfgReloadTimer; /**< Timer is used to cyclic reload the configuration from persistent memory. */ + bool m_storeConfigReq; /**< Is requested to store the configuration in persistent memory? */ + bool m_reloadConfigReq; /**< Is requested to reload the configuration from persistent memory? */ + bool m_hasTopicChanged; /**< Has the topic content changed? */ /** * Defines the message types, which are necessary for HTTP client/server handling. @@ -521,6 +530,18 @@ class OpenWeatherPlugin : public Plugin, private PluginConfigFsHandler */ TaskProxy m_taskProxy; + /** + * Create OpenWeather source according to id. + * + * @param[in] id OpenWeather source id + */ + void createOpenWeatherSource(OpenWeatherSource id); + + /** + * Destroy OpenWeatherSource. + */ + void destroyOpenWeatherSource(); + /** * Request to store configuration to persistent memory. */ @@ -573,6 +594,11 @@ class OpenWeatherPlugin : public Plugin, private PluginConfigFsHandler */ void handleWebResponse(DynamicJsonDocument& jsonDoc); + /** + * Prepares the data to show from the OpenWeather source data. + */ + void prepareDataToShow(); + /** * Clear the task proxy queue. */ diff --git a/lib/OpenWeatherPlugin/web/OpenWeatherPlugin.html b/lib/OpenWeatherPlugin/web/OpenWeatherPlugin.html index 7cf62a51..0d6c066e 100644 --- a/lib/OpenWeatherPlugin/web/OpenWeatherPlugin.html +++ b/lib/OpenWeatherPlugin/web/OpenWeatherPlugin.html @@ -44,11 +44,12 @@

                                                            Get the OpenWeather related configuration .

                                                          • PLUGIN-ALIAS: The plugin alias name.

                                                          Set OpenWeather related configuration.

                                                          -
                                                          POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/weather?apiKey=<API-KEY>&latitude=<LATITUDE>&longitude=<LONGITUDE>&other=<OTHER>&units=<UNITS>
                                                          -
                                                          POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/weather?apiKey=<API-KEY>&latitude=<LATITUDE>&longitude=<LONGITUDE>&other=<OTHER>&units=<UNITS>
                                                          +
                                                          POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/weather?sourceId=<SOURCE-ID>&apiKey=<API-KEY>&latitude=<LATITUDE>&longitude=<LONGITUDE>&other=<OTHER>&units=<UNITS>
                                                          +
                                                          POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/weather?sourceId=<SOURCE-ID>&apiKey=<API-KEY>&latitude=<LATITUDE>&longitude=<LONGITUDE>&other=<OTHER>&units=<UNITS>
                                                          • PLUGIN-UID: The plugin unique id.
                                                          • PLUGIN-ALIAS: The plugin alias name.
                                                          • +
                                                          • SOURCE-ID: The OpenWeather source id - 0: Current weather / 1: One-Call API v2.5 / 2: One-Call API v3.0
                                                          • API-KEY: The OpenWeather api key.
                                                          • LATITUDE: The latitude of the location.
                                                          • LONGITUDE: The longitude of the location.
                                                          • @@ -63,6 +64,14 @@

                                                            Configuration

    +
    + + +
    @@ -174,6 +183,7 @@

    Configuration

    url: "/rest/api/v1/display/uid/" + pluginUid + "/weather", isJsonResponse: true }).then(function(rsp) { + $("#sourceId").val(rsp.data.sourceId); $("#apiKey").val(rsp.data.apiKey); $("#latitude").val(rsp.data.latitude); $("#longitude").val(rsp.data.longitude); @@ -194,6 +204,7 @@

    Configuration

    url: "/rest/api/v1/display/uid/" + pluginUid + "/weather", isJsonResponse: true, parameter: { + sourceId: $("#sourceId").val(), apiKey: $("#apiKey").val(), latitude: $("#latitude").val(), longitude: $("#longitude").val(), From e904696ff87d4d0bcfa394eadee3ba31c93313b2 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 26 Nov 2023 17:49:28 +0100 Subject: [PATCH 063/105] Print the line number in log messages right behind the filename. This gives the possibility that the editor can jump to its location by just clicking on the log message. --- lib/Utilities/src/LogSinkPrinter.cpp | 2 +- test/test_Logging/TestLogging.cpp | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Utilities/src/LogSinkPrinter.cpp b/lib/Utilities/src/LogSinkPrinter.cpp index 211cc02e..db39e3e0 100644 --- a/lib/Utilities/src/LogSinkPrinter.cpp +++ b/lib/Utilities/src/LogSinkPrinter.cpp @@ -69,7 +69,7 @@ void LogSinkPrinter::send(const Logging::Msg& msg) written = snprintf( buffer, LOG_MESSAGE_BUFFER_SIZE - STR_CUT_OFF_SEQ_LEN + 1U, /* + 1U for cut off detection. */ - "%*u %*s %*s:%*d %s\n", + "%*u %*s %*s:%.*d %s\n", TIMESTAMP_LEN, msg.timestamp, LOG_LEVEL_LEN, diff --git a/test/test_Logging/TestLogging.cpp b/test/test_Logging/TestLogging.cpp index 1ea3c8d7..23820987 100644 --- a/test/test_Logging/TestLogging.cpp +++ b/test/test_Logging/TestLogging.cpp @@ -129,6 +129,7 @@ static void testLogging() const char* LOG_MODULE = strrchr(__FILE__, '\\'); /* Windows backslash */ const char* TEST_STRING_1 = "TestMessage"; const String TEST_STRING_2 = "TestMessageAsString"; + const char* LOG_FORMAT = "%*s %*s:%.*d %s\n"; char expectedLogMessage[128]; int lineNo = 0; @@ -167,7 +168,7 @@ static void testLogging() /* Check expected error log output, with type const char* string. */ LOG_ERROR(TEST_STRING_1); lineNo = __LINE__; - (void)snprintf(expectedLogMessage, sizeof(expectedLogMessage), "%*s %*s:%*d %s\n", + (void)snprintf(expectedLogMessage, sizeof(expectedLogMessage), LOG_FORMAT, LogSinkPrinter::LOG_LEVEL_LEN, "ERROR ", LogSinkPrinter::FILENAME_LEN, @@ -184,7 +185,7 @@ static void testLogging() /* Check expected error log output, with type const String string. */ LOG_ERROR(TEST_STRING_2); lineNo = __LINE__; - (void)snprintf(expectedLogMessage, sizeof(expectedLogMessage), "%*s %*s:%*d %s\n", + (void)snprintf(expectedLogMessage, sizeof(expectedLogMessage), LOG_FORMAT, LogSinkPrinter::LOG_LEVEL_LEN, "ERROR ", LogSinkPrinter::FILENAME_LEN, From 449c6fe064f33568ab9d48fcbbba2f2355351bec Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 26 Nov 2023 22:39:59 +0100 Subject: [PATCH 064/105] Bugfix: Wrong JSON document size used for filter. --- lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp | 2 +- lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp b/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp index e9caae4d..93530fe9 100644 --- a/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp +++ b/lib/GrabViaMqttPlugin/src/GrabViaMqttPlugin.cpp @@ -126,7 +126,7 @@ bool GrabViaMqttPlugin::setTopic(const String& topic, const JsonObjectConst& val else if (true == jsonFilter.is()) { const size_t JSON_DOC_FILTER_SIZE = 256U; - DynamicJsonDocument jsonDocFilter(JSON_DOC_SIZE); + DynamicJsonDocument jsonDocFilter(JSON_DOC_FILTER_SIZE); DeserializationError result = deserializeJson(jsonDocFilter, jsonFilter.as()); if (DeserializationError::Ok == result) diff --git a/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp b/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp index 8c07e249..4949e6bf 100644 --- a/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp +++ b/lib/GrabViaRestPlugin/src/GrabViaRestPlugin.cpp @@ -132,7 +132,7 @@ bool GrabViaRestPlugin::setTopic(const String& topic, const JsonObjectConst& val else if (true == jsonFilter.is()) { const size_t JSON_DOC_FILTER_SIZE = 256U; - DynamicJsonDocument jsonDocFilter(JSON_DOC_SIZE); + DynamicJsonDocument jsonDocFilter(JSON_DOC_FILTER_SIZE); DeserializationError result = deserializeJson(jsonDocFilter, jsonFilter.as()); if (DeserializationError::Ok == result) From 342a919714fb6abcb83f2bba07ae01ca8b4878ab Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 26 Nov 2023 22:40:41 +0100 Subject: [PATCH 065/105] Bugfix: Variable defined twice. --- lib/MqttService/src/MqttService.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/MqttService/src/MqttService.cpp b/lib/MqttService/src/MqttService.cpp index 8582d554..6c08d337 100644 --- a/lib/MqttService/src/MqttService.cpp +++ b/lib/MqttService/src/MqttService.cpp @@ -324,8 +324,6 @@ void MqttService::disconnectedState() /* Connection to broker successful. */ else { - String willTopic = m_hostname + "/status"; - LOG_INFO("Connection to MQTT broker established."); m_state = STATE_CONNECTED; From 021f73ab5302626b4297bdce35d2b35c5766de56 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 26 Nov 2023 22:41:33 +0100 Subject: [PATCH 066/105] Bugfix: If buffer allocation failed, send a negative response. --- src/Web/WsCommand/WsCmdGetDisp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Web/WsCommand/WsCmdGetDisp.cpp b/src/Web/WsCommand/WsCmdGetDisp.cpp index 8b743a48..022f3fe6 100644 --- a/src/Web/WsCommand/WsCmdGetDisp.cpp +++ b/src/Web/WsCommand/WsCmdGetDisp.cpp @@ -84,7 +84,7 @@ void WsCmdGetDisp::execute(AsyncWebSocket* server, AsyncWebSocketClient* client) if (nullptr == framebuffer) { - m_isError = true; + sendNegativeResponse(server, client, "\"Internal error.\""); } else { From 35712bd0abf598dd4ccd0a7bb1dea31d641bed3f Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 26 Nov 2023 22:42:14 +0100 Subject: [PATCH 067/105] Bugfix: Useless condition in TopicHandlerService::start(). --- lib/TopicHandlerService/src/TopicHandlerService.cpp | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/TopicHandlerService/src/TopicHandlerService.cpp b/lib/TopicHandlerService/src/TopicHandlerService.cpp index 4c714bf9..53b559bf 100644 --- a/lib/TopicHandlerService/src/TopicHandlerService.cpp +++ b/lib/TopicHandlerService/src/TopicHandlerService.cpp @@ -66,22 +66,13 @@ const char* TopicHandlerService::DEFAULT_ACCESS = "rw"; bool TopicHandlerService::start() { - bool isSuccessful = true; - startAllHandlers(); m_onChangeTimer.start(ON_CHANGE_PERIOD); - if (false == isSuccessful) - { - stop(); - } - else - { - LOG_INFO("Topic handler service started."); - } + LOG_INFO("Topic handler service started."); - return isSuccessful; + return true; } void TopicHandlerService::stop() From 144b2707fdb469b717f1276164337c76a5351d9f Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 26 Nov 2023 22:44:48 +0100 Subject: [PATCH 068/105] Variable scope reduced and parameter made const. --- .../src/OpenWeatherPlugin.cpp | 19 ++++++++++--------- lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp index 40612a26..770d37fc 100644 --- a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp +++ b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp @@ -875,7 +875,7 @@ void OpenWeatherPlugin::initHttpClient() ); } -void OpenWeatherPlugin::handleWebResponse(DynamicJsonDocument& jsonDoc) +void OpenWeatherPlugin::handleWebResponse(const DynamicJsonDocument& jsonDoc) { if (nullptr != m_source) { @@ -888,13 +888,11 @@ void OpenWeatherPlugin::prepareDataToShow() { if (nullptr != m_source) { - float temperature = m_source->getTemperature(); - String weatherIconId = m_source->getWeatherIconId(); - float uvIndex = m_source->getUvIndex(); - int humidity = m_source->getHumidity(); - float windSpeed = m_source->getWindSpeed(); - char tempReducedPrecison[6] = { 0 }; - char windReducedPrecison[5] = { 0 }; + float temperature = m_source->getTemperature(); + String weatherIconId = m_source->getWeatherIconId(); + float uvIndex = m_source->getUvIndex(); + int humidity = m_source->getHumidity(); + float windSpeed = m_source->getWindSpeed(); String weatherConditionIconFullPath; /* Generate UV-Index string and adapt color of string accordingly. */ @@ -915,7 +913,8 @@ void OpenWeatherPlugin::prepareDataToShow() } else { - const char* reducePrecision = (temperature < -9.9F) ? "%.0f" : "%.1f"; + const char* reducePrecision = (temperature < -9.9F) ? "%.0f" : "%.1f"; + char tempReducedPrecison[6] = { 0 }; /* Generate temperature string with reduced precision and add unit °C/°F. */ (void)snprintf(tempReducedPrecison, sizeof(tempReducedPrecison), reducePrecision, temperature); @@ -938,6 +937,8 @@ void OpenWeatherPlugin::prepareDataToShow() } else { + char windReducedPrecison[5] = { 0 }; + (void)snprintf(windReducedPrecison, sizeof(windReducedPrecison), "%.1f", windSpeed); m_currentWindspeed = "\\calign"; diff --git a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h index 3cd8a5d3..a0cd9a45 100644 --- a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h +++ b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h @@ -592,7 +592,7 @@ class OpenWeatherPlugin : public Plugin, private PluginConfigFsHandler * * @param[in] jsonDoc Web response as JSON document */ - void handleWebResponse(DynamicJsonDocument& jsonDoc); + void handleWebResponse(const DynamicJsonDocument& jsonDoc); /** * Prepares the data to show from the OpenWeather source data. From 0847882a37a1adebf48e03fb21cebdd097ac3662 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Sun, 26 Nov 2023 22:54:35 +0100 Subject: [PATCH 069/105] Useless pointer checks removed. --- src/Hal/SensorDataProvider.cpp | 73 +++++++++++----------------------- 1 file changed, 23 insertions(+), 50 deletions(-) diff --git a/src/Hal/SensorDataProvider.cpp b/src/Hal/SensorDataProvider.cpp index ada9971e..792f048f 100644 --- a/src/Hal/SensorDataProvider.cpp +++ b/src/Hal/SensorDataProvider.cpp @@ -283,11 +283,12 @@ bool SensorDataProvider::load() JsonFile jsonFile(FILESYSTEM); const size_t JSON_DOC_SIZE = 512U; DynamicJsonDocument jsonDoc(JSON_DOC_SIZE); - uint8_t sensorIdx = 0U; - const uint8_t SENSOR_CNT = m_impl->getNumSensors(); if (true == jsonFile.load(SENSOR_CALIB_FILE_NAME, jsonDoc)) { + uint8_t sensorIdx = 0U; + const uint8_t SENSOR_CNT = m_impl->getNumSensors(); + while(SENSOR_CNT > sensorIdx) { ISensor* sensor = m_impl->getSensor(sensorIdx); @@ -416,14 +417,7 @@ void SensorDataProvider::channelOffsetToJson(JsonArray& jsonOffset, const ISenso { const SensorChannelUInt32* uint32Channel = reinterpret_cast(&channel); - if (nullptr == uint32Channel) - { - jsonOffset.add("NaN"); - } - else - { - jsonOffset.add(uint32Channel->getOffset()); - } + jsonOffset.add(uint32Channel->getOffset()); } break; @@ -431,15 +425,7 @@ void SensorDataProvider::channelOffsetToJson(JsonArray& jsonOffset, const ISenso { const SensorChannelInt32* int32Channel = reinterpret_cast(&channel); - if (nullptr == int32Channel) - { - jsonOffset.add("NaN"); - } - else - { - jsonOffset.add(int32Channel->getOffset()); - } - + jsonOffset.add(int32Channel->getOffset()); } break; @@ -447,14 +433,7 @@ void SensorDataProvider::channelOffsetToJson(JsonArray& jsonOffset, const ISenso { const SensorChannelFloat32* float32Channel = reinterpret_cast(&channel); - if (nullptr == float32Channel) - { - jsonOffset.add("NaN"); - } - else - { - jsonOffset.add(float32Channel->getOffset()); - } + jsonOffset.add(float32Channel->getOffset()); } break; @@ -479,8 +458,7 @@ void SensorDataProvider::channelOffsetFromJson(ISensorChannel& channel, JsonVari { SensorChannelUInt32* uint32Channel = reinterpret_cast(&channel); - if ((nullptr != uint32Channel) && - (true == jsonOffset.is())) + if (true == jsonOffset.is()) { uint32Channel->setOffset(jsonOffset.as()); } @@ -491,8 +469,7 @@ void SensorDataProvider::channelOffsetFromJson(ISensorChannel& channel, JsonVari { SensorChannelInt32* int32Channel = reinterpret_cast(&channel); - if ((nullptr != int32Channel) && - (true == jsonOffset.is())) + if (true == jsonOffset.is()) { int32Channel->setOffset(jsonOffset.as()); } @@ -503,8 +480,7 @@ void SensorDataProvider::channelOffsetFromJson(ISensorChannel& channel, JsonVari { SensorChannelFloat32* float32Channel = reinterpret_cast(&channel); - if ((nullptr != float32Channel) && - (true == jsonOffset.is())) + if (true == jsonOffset.is()) { float32Channel->setOffset(jsonOffset.as()); } @@ -516,6 +492,7 @@ void SensorDataProvider::channelOffsetFromJson(ISensorChannel& channel, JsonVari break; default: + /* Not supported. */ break; } } @@ -532,32 +509,28 @@ void SensorDataProvider::createCalibrationFile() for(index = 0U; index < defaultValues; ++index) { - const SensorChannelDefaultValue* value = &sensorChannelDefaultValueList[index]; + const SensorChannelDefaultValue* value = &sensorChannelDefaultValueList[index]; + ISensor* sensor = getSensor(value->sensorId); - if (nullptr != value) + if (nullptr == sensor) + { + LOG_ERROR("Sensor %u doesn't exists.", value->sensorId); + } + else { - ISensor* sensor = getSensor(value->sensorId); + ISensorChannel* channel = sensor->getChannel(value->channelId); - if (nullptr == sensor) + if (nullptr == channel) { - LOG_ERROR("Sensor %u doesn't exists.", value->sensorId); + LOG_ERROR("Sensor %u has no channel %u.", value->sensorId, value->channelId); } else { - ISensorChannel* channel = sensor->getChannel(value->channelId); + DynamicJsonDocument jsonDoc(256U); - if (nullptr == channel) - { - LOG_ERROR("Sensor %u has no channel %u.", value->sensorId, value->channelId); - } - else + if (DeserializationError::Ok == deserializeJson(jsonDoc, value->jsonStrValue)) { - DynamicJsonDocument jsonDoc(256U); - - if (DeserializationError::Ok == deserializeJson(jsonDoc, value->jsonStrValue)) - { - channelOffsetFromJson(*channel, jsonDoc["offset"]); - } + channelOffsetFromJson(*channel, jsonDoc["offset"]); } } } From 6b47075081202b0c14045a7e4c4590ac24c68663 Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Mon, 27 Nov 2023 00:02:32 +0100 Subject: [PATCH 070/105] The OpenWeather update period can now be configured. Default is still every 10 minutes according to OpenWeather recommendation. --- .../src/OpenWeatherPlugin.cpp | 46 +++++++++++++---- lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h | 2 + .../web/OpenWeatherPlugin.html | 49 +++++++++++-------- 3 files changed, 67 insertions(+), 30 deletions(-) diff --git a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp index 770d37fc..f48e2a45 100644 --- a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp +++ b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.cpp @@ -147,6 +147,7 @@ bool OpenWeatherPlugin::setTopic(const String& topic, const JsonObjectConst& val DynamicJsonDocument jsonDoc(JSON_DOC_SIZE); JsonObject jsonCfg = jsonDoc.to(); JsonVariantConst jsonSourceId = value["sourceId"]; + JsonVariantConst jsonUpdatePeriod = value["updatePeriod"]; JsonVariantConst jsonApiKey = value["apiKey"]; JsonVariantConst jsonLatitude = value["latitude"]; JsonVariantConst jsonLongitude = value["longitude"]; @@ -170,6 +171,12 @@ bool OpenWeatherPlugin::setTopic(const String& topic, const JsonObjectConst& val isSuccessful = true; } + if (false == jsonUpdatePeriod.isNull()) + { + jsonCfg["updatePeriod"] = jsonUpdatePeriod.as(); + isSuccessful = true; + } + if (false == jsonApiKey.isNull()) { jsonCfg["apiKey"] = jsonApiKey.as(); @@ -357,7 +364,7 @@ void OpenWeatherPlugin::process(bool isConnected) } else { - m_requestTimer.start(UPDATE_PERIOD); + m_requestTimer.start(m_updatePeriod); } } } @@ -383,7 +390,7 @@ void OpenWeatherPlugin::process(bool isConnected) } else { - m_requestTimer.start(UPDATE_PERIOD); + m_requestTimer.start(m_updatePeriod); } } } @@ -532,6 +539,7 @@ void OpenWeatherPlugin::getConfiguration(JsonObject& jsonCfg) const else { jsonCfg["sourceId"] = static_cast(m_sourceId); + jsonCfg["updatePeriod"] = m_updatePeriod / (60U * 1000U); /* Conversion from ms to minutes. */ jsonCfg["apiKey"] = m_source->getApiKey(); jsonCfg["latitude"] = m_source->getLatitude(); jsonCfg["longitude"] = m_source->getLongitude(); @@ -542,18 +550,26 @@ void OpenWeatherPlugin::getConfiguration(JsonObject& jsonCfg) const bool OpenWeatherPlugin::setConfiguration(JsonObjectConst& jsonCfg) { - bool status = false; - JsonVariantConst jsonSourceId = jsonCfg["sourceId"]; - JsonVariantConst jsonApiKey = jsonCfg["apiKey"]; - JsonVariantConst jsonLatitude = jsonCfg["latitude"]; - JsonVariantConst jsonLongitude = jsonCfg["longitude"]; - JsonVariantConst jsonOther = jsonCfg["other"]; - JsonVariantConst jsonUnits = jsonCfg["units"]; + bool status = false; + JsonVariantConst jsonSourceId = jsonCfg["sourceId"]; + JsonVariantConst jsonUpdatePeriod = jsonCfg["updatePeriod"]; + JsonVariantConst jsonApiKey = jsonCfg["apiKey"]; + JsonVariantConst jsonLatitude = jsonCfg["latitude"]; + JsonVariantConst jsonLongitude = jsonCfg["longitude"]; + JsonVariantConst jsonOther = jsonCfg["other"]; + JsonVariantConst jsonUnits = jsonCfg["units"]; + + const uint32_t UPDATE_PERIOD_LOWER_LIMIT = 1U; /* minutes */ + const uint32_t UPDATE_PERIOD_UPPER_LIMIT = 120U; /* minutes */ if (false == jsonSourceId.is()) { LOG_WARNING("Source id not found or invalid type."); } + else if (false == jsonUpdatePeriod.is()) + { + LOG_WARNING("Update period not found or invalid type."); + } else if (false == jsonApiKey.is()) { LOG_WARNING("API key not found or invalid type."); @@ -586,6 +602,18 @@ bool OpenWeatherPlugin::setConfiguration(JsonObjectConst& jsonCfg) createOpenWeatherSource(m_sourceId); } + m_updatePeriod = jsonUpdatePeriod.as(); + + if ((UPDATE_PERIOD_LOWER_LIMIT > m_updatePeriod) || + (UPDATE_PERIOD_UPPER_LIMIT < m_updatePeriod)) + { + m_updatePeriod = UPDATE_PERIOD; + } + else + { + m_updatePeriod = SIMPLE_TIMER_MINUTES(m_updatePeriod); + } + if (nullptr == m_source) { LOG_ERROR("No OpenWeather source available."); diff --git a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h index a0cd9a45..efe27757 100644 --- a/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h +++ b/lib/OpenWeatherPlugin/src/OpenWeatherPlugin.h @@ -106,6 +106,7 @@ class OpenWeatherPlugin : public Plugin, private PluginConfigFsHandler m_bitmapWidget(), m_textWidget("\\calign?"), m_sourceId(OPENWEATHER_SOURCE_ONE_CALL_25), + m_updatePeriod(UPDATE_PERIOD), m_source(nullptr), m_additionalInformation(OTHER_WEATHER_INFO_OFF), m_configurationFilename(), @@ -474,6 +475,7 @@ class OpenWeatherPlugin : public Plugin, private PluginConfigFsHandler BitmapWidget m_bitmapWidget; /**< Bitmap widget, used to show the icon. */ TextWidget m_textWidget; /**< Text widget, used for showing the text. */ OpenWeatherSource m_sourceId; /**< OpenWeather source id. */ + uint32_t m_updatePeriod; /**< Period in ms for requesting data from server. This is used in case the last request to the server was successful. */ IOpenWeatherSource* m_source; /**< OpenWeather source to use to retrieve weather information. */ OtherWeatherInformation m_additionalInformation; /**< The configured additional weather information. */ String m_configurationFilename; /**< String used for specifying the configuration filename. */ diff --git a/lib/OpenWeatherPlugin/web/OpenWeatherPlugin.html b/lib/OpenWeatherPlugin/web/OpenWeatherPlugin.html index 0d6c066e..282817f6 100644 --- a/lib/OpenWeatherPlugin/web/OpenWeatherPlugin.html +++ b/lib/OpenWeatherPlugin/web/OpenWeatherPlugin.html @@ -44,12 +44,13 @@

    Get the OpenWeather related configuration .

  • PLUGIN-ALIAS: The plugin alias name.
  • Set OpenWeather related configuration.

    -
    POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/weather?sourceId=<SOURCE-ID>&apiKey=<API-KEY>&latitude=<LATITUDE>&longitude=<LONGITUDE>&other=<OTHER>&units=<UNITS>
    -
    POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/weather?sourceId=<SOURCE-ID>&apiKey=<API-KEY>&latitude=<LATITUDE>&longitude=<LONGITUDE>&other=<OTHER>&units=<UNITS>
    +
    POST {{ORIGIN}}/rest/api/v1/display/uid/<PLUGIN-UID>/weather?sourceId=<SOURCE-ID>&updatePeriod=<UPDATE-PERIOD>&apiKey=<API-KEY>&latitude=<LATITUDE>&longitude=<LONGITUDE>&other=<OTHER>&units=<UNITS>
    +
    POST {{ORIGIN}}/rest/api/v1/display/alias/<PLUGIN-ALIAS>/weather?sourceId=<SOURCE-ID>&updatePeriod=<UPDATE-PERIOD>&apiKey=<API-KEY>&latitude=<LATITUDE>&longitude=<LONGITUDE>&other=<OTHER>&units=<UNITS>
    • PLUGIN-UID: The plugin unique id.
    • PLUGIN-ALIAS: The plugin alias name.
    • SOURCE-ID: The OpenWeather source id - 0: Current weather / 1: One-Call API v2.5 / 2: One-Call API v3.0
    • +
    • UPDATE-PERIOD: The period in minutes for updating the weather information. Please consider your limits, see professional collections price table.
    • API-KEY: The OpenWeather api key.
    • LATITUDE: The latitude of the location.
    • LONGITUDE: The longitude of the location.
    • @@ -62,7 +63,7 @@

      Configuration

      +
      @@ -72,17 +73,21 @@

      Configuration

      +
      + + +
      - +
      - +
      - +
      @@ -91,15 +96,15 @@

      Configuration

      - +
      -
      + +
    @@ -129,15 +134,15 @@

    Configuration

    var pluginName = "OpenWeatherPlugin"; var restClient = new pixelix.rest.Client(); - + function enableUI() { utils.enableForm("myForm", true); } - + function disableUI() { utils.enableForm("myForm", false); } - + function getPluginInstances() { return restClient.getPluginInstances().then(function(rsp) { var slotIndex = 0; @@ -150,7 +155,7 @@

    Configuration

    optionText = rsp.data.slots[slotIndex].uid; optionText += " ("; - + if (0 === rsp.data.slots[slotIndex].alias.length) { optionText += "-" } else { @@ -162,7 +167,7 @@

    Configuration

    $option = $("
    - +