├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── src ├── boomstring.cpp ├── boomstring.h ├── cachedstring.cpp ├── cachedstring.h ├── dbus_functions.cpp ├── dbus_functions.h ├── dbuserrorguard.cpp ├── dbuserrorguard.h ├── dbusmessageguard.cpp ├── dbusmessageguard.h ├── dbusmessageiteropencontainerguard.cpp ├── dbusmessageiteropencontainerguard.h ├── dbusmessageitersignature.cpp ├── dbusmessageitersignature.h ├── dbuspendingmessagecallguard.cpp ├── dbuspendingmessagecallguard.h ├── dbusutils.cpp ├── dbusutils.h ├── exceptions.cpp ├── exceptions.h ├── fdguard.cpp ├── fdguard.h ├── flashmq-dbus-plugin-tests.cpp ├── flashmq-dbus-plugin-tests.h ├── flashmq-dbus-plugin.cpp ├── flashmqfunctionreplacements.cpp ├── flashmqfunctionreplacements.h ├── network.cpp ├── network.h ├── queuedtasks.cpp ├── queuedtasks.h ├── serviceidentifier.cpp ├── serviceidentifier.h ├── shortservicename.cpp ├── shortservicename.h ├── state.cpp ├── state.h ├── testerglobals.cpp ├── testerglobals.h ├── types.cpp ├── types.h ├── utils.cpp ├── utils.h ├── vevariant.cpp └── vevariant.h ├── vendor ├── flashmq_plugin.h └── json.hpp └── venus-ca.crt /.gitignore: -------------------------------------------------------------------------------- 1 | *.user 2 | *.swp 3 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | cmake_policy(SET CMP0048 NEW) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | project(flashmq-dbus-plugin VERSION 0.0.1 LANGUAGES CXX) 8 | 9 | add_compile_options(-Wall -Wno-psabi) 10 | 11 | include(GNUInstallDirs) 12 | 13 | find_package(PkgConfig REQUIRED) 14 | 15 | pkg_check_modules(DBUS REQUIRED IMPORTED_TARGET dbus-1) 16 | 17 | add_library(flashmq-dbus-plugin SHARED 18 | vendor/json.hpp 19 | vendor/flashmq_plugin.h 20 | 21 | src/dbus_functions.h 22 | src/flashmq-dbus-plugin.cpp 23 | src/dbus_functions.cpp 24 | src/state.h src/state.cpp 25 | src/utils.h src/utils.cpp 26 | src/queuedtasks.cpp src/queuedtasks.h 27 | src/dbusmessageguard.h src/dbusmessageguard.cpp 28 | src/types.h src/types.cpp 29 | src/exceptions.h src/exceptions.cpp 30 | src/dbusmessageitersignature.h src/dbusmessageitersignature.cpp 31 | src/dbusutils.h src/dbusutils.cpp 32 | src/vevariant.h src/vevariant.cpp 33 | src/shortservicename.h src/shortservicename.cpp 34 | src/dbuserrorguard.h src/dbuserrorguard.cpp 35 | src/cachedstring.h src/cachedstring.cpp 36 | src/dbusmessageiteropencontainerguard.h src/dbusmessageiteropencontainerguard.cpp 37 | src/boomstring.h src/boomstring.cpp 38 | src/fdguard.h src/fdguard.cpp 39 | src/serviceidentifier.h src/serviceidentifier.cpp 40 | src/dbuspendingmessagecallguard.h src/dbuspendingmessagecallguard.cpp 41 | src/network.h src/network.cpp 42 | ) 43 | 44 | add_executable(flashmq-dbus-plugin-tests 45 | vendor/json.hpp 46 | vendor/flashmq_plugin.h 47 | 48 | src/flashmq-dbus-plugin-tests.h 49 | src/dbus_functions.h 50 | src/dbus_functions.cpp 51 | src/flashmq-dbus-plugin.cpp 52 | src/flashmq-dbus-plugin-tests.cpp 53 | src/state.h src/state.cpp 54 | src/flashmqfunctionreplacements.cpp src/flashmqfunctionreplacements.h 55 | src/testerglobals.h src/testerglobals.cpp 56 | src/utils.h src/utils.cpp 57 | src/queuedtasks.cpp src/queuedtasks.h 58 | src/dbusmessageguard.h src/dbusmessageguard.cpp 59 | src/types.h src/types.cpp 60 | src/exceptions.h src/exceptions.cpp 61 | src/dbusmessageitersignature.h src/dbusmessageitersignature.cpp 62 | src/dbusutils.h src/dbusutils.cpp 63 | src/vevariant.h src/vevariant.cpp 64 | src/shortservicename.h src/shortservicename.cpp 65 | src/dbuserrorguard.h src/dbuserrorguard.cpp 66 | src/cachedstring.h src/cachedstring.cpp 67 | src/dbusmessageiteropencontainerguard.h src/dbusmessageiteropencontainerguard.cpp 68 | src/boomstring.h src/boomstring.cpp 69 | src/fdguard.h src/fdguard.cpp 70 | src/serviceidentifier.h src/serviceidentifier.cpp 71 | src/dbuspendingmessagecallguard.h src/dbuspendingmessagecallguard.cpp 72 | src/network.h src/network.cpp 73 | ) 74 | 75 | target_include_directories(flashmq-dbus-plugin PUBLIC ${DBUS_INCLUDE_DIRS} .) 76 | target_include_directories(flashmq-dbus-plugin-tests PUBLIC ${DBUS_INCLUDE_DIRS} .) 77 | 78 | target_link_libraries(flashmq-dbus-plugin pthread dbus-1 resolv crypt) 79 | 80 | target_link_libraries(flashmq-dbus-plugin-tests pthread dbus-1 resolv crypt) 81 | 82 | install(TARGETS flashmq-dbus-plugin-tests RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) 83 | install(TARGETS flashmq-dbus-plugin DESTINATION "${CMAKE_INSTALL_LIBEXECDIR}/flashmq") 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Victron Energy BV 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dbus-mqtt-flashmq-plugin 2 | ========= 3 | 4 | Contents 5 | -------- 6 | 7 | - Introduction 8 | - Where to find support 9 | - Notifications 10 | - Write requests 11 | - Read requests 12 | - Keep-alive 13 | - Migrating from previous versions and backwards compatability 14 | - Connecting to the VRM MQTT servers 15 | - Building 16 | 17 | Introduction 18 | ------------ 19 | dbus-flashmq is the plugin that makes FlashMQ, the MQTT Broker used on Venus OS, a D-Bus aware MQTT broker. It also supports receiving requests to change values on the local D-Bus. It's a faster replacement for the original Python dbus-mqtt. And, because it integrated with FlashMQ's event loop, it can coordinate dbus and mqtt activity as efficiently as possible. 20 | 21 | FlashMQ with this plugin runs as a service since [Venus OS](https://github.com/victronenergy/venus/wiki) version 3.20. To enable the MQTT service, go to 'Settings -> Services -> MQTT' on the menus of the GX device. 22 | 23 | dbus-flashmq runs inside the FlashMQ broker. FlashMQ is accessible on the local network at TCP port 1883 and/or 8883 (depending on your security settings). Furthermore, MQTT traffic can be configured to be forwarded to the central Victron VRM MQTT broker farm. For further details, see the [Connecting to the VRM MQTT servers](#connecting-to-the-vrm-mqtt-servers) chapter below. 24 | 25 | 26 | Support 27 | ------- 28 | Please don't use the issue tracker of this repo. 29 | 30 | Instead, search & post at the Modifications section of our community forum: 31 | 32 | https://community.victronenergy.com/c/modifications/5 33 | 34 | 35 | Notifications 36 | ------------- 37 | 38 | When a value on the D-Bus changes, the plugin will initiate a publish. The MQTT topic looks like this: 39 | 40 | ``` 41 | N//// 42 | ``` 43 | 44 | * Portal ID is the VRM portal ID associated with the GX Device. You can find the portal ID on the GX Device in 45 | Settings->VRM online portal->VRM Portal ID. On the VRM portal itself, you can find the ID in Settings 46 | tab. 47 | * Service type is the part of the D-Bus service name that describes the service. 48 | * Device instance is a number used to make all services of the same type unique (this value is published 49 | on the D-Bus as `/DeviceInstance`). 50 | 51 | The payload of the D-Bus value is wrapped in a dictionary and converted to json. 52 | 53 | Unlike the previous Python implementation, this plugin no longer causes messages to be retained by the broker, either locally or on the internet. There are fundamental problems with retained messages, that are solved by simply requesting the current topics as needed. See the [chapter on keep-alives](#keep-alive) and [backwards compatability](#migrating-from-previous-versions-and-backwards-compatability) below. 54 | 55 | As an example of a notification, suppose we have a PV inverter, which reports a total AC power of 936W. The topic of the MQTT message would be: 56 | 57 | Topic: N//pvinverter/20/Ac/Power 58 | Payload: {"value": 936} 59 | 60 | The value 20 in the topic is the device instance which may be different on other systems. 61 | 62 | There are 2 special cases: 63 | 64 | * A D-Bus value may be invalid. This happens with values that are not always present. For example: a single 65 | phase PV inverter will not provide a power value on phase 2. So /Ac/L2/Power is invalid. In that case the 66 | payload of the MQTT message will be `{"value": null}`. 67 | * A device may disappear from the D-Bus. For example: most PV inverters shut down at night, causing a 68 | communication breakdown. If this happens a notification will be sent for all topics related to the device. 69 | The payload will be empty (zero bytes, so no valid JSON). 70 | 71 | If you want a roundup of all devices connected to the GX Device subscribe to this topic: 72 | 73 | ```sh 74 | mosquitto_sub -t 'N//+/+/ProductId' -h '' -v 75 | 'N//+/+/ProductId' 76 | ``` 77 | 78 | And then send a keepalive to refresh all topic values (this is described in more detail in the [keep-alive chapter](#keep-alive) below): 79 | 80 | ```sh 81 | mosquitto_pub -t 'R//keepalive' -m '' -h '' 82 | ``` 83 | 84 | This also is a convenient way to find out which device instances are used, which comes in handy when there are 85 | multiple devices of the same type present. 86 | 87 | 88 | 89 | 90 | Write requests 91 | -------------- 92 | 93 | Write requests can be sent to change values on the D-Bus. The format looks like the notification, however instead of an `N`, the topic should start with a `W`. The payload format is identical. 94 | 95 | Example: on a Hub-4 system we can change the AC-In setpoint with this command line, using `mosquitto_pub`: 96 | 97 | ```sh 98 | mosquitto_pub -h '' -t 'W//vebus/257/Hub4/L1/AcPowerSetpoint' -m '{"value": -200}' 99 | ``` 100 | 101 | The device instance (in this case 257) of a service usually depends on the communication port used the 102 | connect the device to the GX Device, so it is a good idea to check it before sending write requests. A nice way to 103 | do this is by subscribing to the broker using wildcards. 104 | 105 | For example: 106 | 107 | ```sh 108 | mosquitto_sub -v -h '' -t 'N//vebus/+/Hub4/L1/AcPowerSetpoint' 109 | ``` 110 | 111 | will get you the list of all registered Multis/Quattros (=vebus services) which have published `/Hub4/L1/AcPowerSetpoint` D-Bus path. You can pick the device instance from the topics in the list. As before, this does require a topic refresh by sending a keep-alive. See the [chapter on keep-alives](#keep-alive). 112 | 113 | 114 | 115 | 116 | Read requests 117 | ------------- 118 | 119 | A read request will force the plugin to re-read the current value from the dbus and send a notification 120 | message of a specific D-Bus value. Again the 121 | topic is identical to the notification message itself, except that the first character is a `R`. Wildcards 122 | in the topic are not supported. The payload will be ignored (it's best to keep it empty). 123 | 124 | Example: to retrieve the AC power of our favorite PV inverter we publish (with an empty payload): 125 | 126 | ```sh 127 | mosquitto_pub -h '' -t 'R//pvinverter/20/Ac/Power' -m '' 128 | ``` 129 | 130 | The Venus device will reply with this message (make sure you subscribe to it): 131 | 132 | ``` 133 | Topic: N//pvinverter/20/Ac/Power 134 | Payload: {"value": 926} 135 | ``` 136 | 137 | Normally you do not need to use read requests, because most values are published automatically as they 138 | change. For values that don't change often, most notably settings (`com.victronenergy.settings` on D-Bus), 139 | you will have to use a read request to retrieve the current value. 140 | 141 | You can also do a read request on sub-paths, like `R//solarcharger/279/Dc`. This results in updates of the underlying paths: 142 | 143 | ``` 144 | N//solarcharger/279/Dc/0/Voltage {"value":20.43000030517578} 145 | N//solarcharger/279/Dc/0/Current {"value":0.0} 146 | N//solarcharger/279/Dc/0/Voltage {"value":20.43000030517578} 147 | ``` 148 | 149 | Note that this is different from the previous API, which replied on `N//solarcharger/279/Dc` with a serialized json representation of the deeper topics. 150 | 151 | 152 | 153 | 154 | Keep-alive 155 | ---------- 156 | 157 | In order to avoid a lot of traffic, there is keep-alive mechanism. It works slightly differently from dbus-mqtt shipped in earlier versions of Venus. 158 | 159 | To activate keep-alive, send a read request to `R//keepalive` (or the legacy `R//system/0/Serial`). It will send all topics it has, whether the system is alive or not. This is the replacement for retained messages as used by the Python dbus-mqtt. Because messages are no longer retained, if you are subscribing to a path like `N//+/+/ProductId` to see all products, you must initiate a keep-alive request afterwards to see the values. See the next section. 160 | 161 | Keep-alive timeout is 60 seconds. After it expires, `null` values are not published. See the next section about changes in behavior. 162 | 163 | When a keep-alive is received and all topics are published, the last topic will be `N//full_publish_completed` with a payload like `{"value":1702459522}`. This topic signals that you have received all topics with their values, and can be a trigger for an application, to update the GUI, or go to the next state, etc. Starting from Venus 3.50, you can include a keep-alive option to have it echo back a specific payload, like: `{ "keepalive-options" : [ {"full-publish-completed-echo": "Ucq2YLVt6ZWdgUNf" } ] }`. When you do this, the response looks like `{"full-publish-completed-echo":"Ucq2YLVt6ZWdgUNf","value":1718860914}`. This allows for better concurrent use, and this way you'll know it was _your_ request that has completed, and not somebody else's that happened at the same time. 164 | 165 | You can specify `{ "keepalive-options" : ["suppress-republish"] }` to forgo sending all topics. Once you have woken up a system and continue to send keep-alives, you don't need a full update on all topics each time, and you can/should include the `suppress-republish` keepalive-option from that point on. A typical implementation would be to put the keep-alive with `{ "keepalive-options" : ["suppress-republish"] }` on a timer, and send keep-alives with empty payload explicitely, when the state of the program requires it. 166 | 167 | Note that on recent Venus versions, dbus-flashmq will only do the full republish at most three times per second. This should not impact any normal use case, yet protect against accidental overload. 168 | 169 | Here is a simple command to send keep alives from a Linux system: 170 | 171 | run this command in a separate session and/or terminal window: 172 | 173 | ```sh 174 | ( first=""; while true; do if [[ -n "$first" ]]; then echo '{ "keepalive-options" : ["suppress-republish"] }'; else echo ""; fi ; first=true; sleep 30; done ) | mosquitto_pub -t 'R//keepalive' -l -h '192.168.8.60' 175 | 176 | ``` 177 | 178 | You will need to install the mosquitto client package. On a Debian or Ubuntu 179 | system this can be done with: 180 | 181 | ```sh 182 | sudo apt-get install mosquitto-clients 183 | ``` 184 | 185 | 186 | Migrating from previous versions and backwards compatability 187 | ---------------- 188 | 189 | With this new replacement for Python dbus-mqtt there are some changes: 190 | 191 | #### 1) No more retained messages 192 | 193 | Retained messages are messages that are kept by the MQTT server and are given to clients on subscription to an MQTT pattern. With dbus-flashmq, messages are no longer published as retained. This caused too much confusion about which topics were still valid and which weren't. Instead, when receiving a keep-alive, the system will simply republish all values. This allows existing clients to see all the topics once they connect and send the keep-alive, whether they are the first, second, third, etc. See the [section on keep-alives](#keep-alive). 194 | 195 | A consequence of this, is that you will no longer immediately see a list of topics+values when you subscribe. You need to request all topic values with the keep-alive. 196 | 197 | Another consequence, is that after keep-alive timeout, `null` values for all topics will not be sent. This was originally required to unpublish retained messages. Now, topics with `null` are only sent when devices disappear. 198 | 199 | #### 2) No more selective keep-alive 200 | 201 | Another change is that 'selective keep-alive' is, at least for now, not supported. Selective keep-alives was a mechanism to only keep certain topics alive, to reduce traffic and load. However, this effect was actually not achieved well, and with this new faster implementation, it's simply no problem to send all topics. 202 | 203 | #### 3) Reading a sub-tree at once 204 | 205 | As described in the [Read requests](#read-requests) section, doing a read on a sub-tree like: 206 | 207 | ```sh 208 | mosquitto_pub -t 'R//settings/0/Settings' -m '' -h '' 209 | ``` 210 | 211 | will give you an update on all sub-topics: 212 | 213 | ``` 214 | N//settings/0/Settings/LEDs/Enable {"max":1,"min":0,"value":1} 215 | N//settings/0/Settings/Vrmlogger/Http/Proxy {"value":""} 216 | N//settings/0/Settings/Vrmlogger/Url {"value":""} 217 | N//settings/0/Settings/Vrmlogger/Logmode {"max":2,"min":0,"value":1} 218 | N//settings/0/Settings/Vrmlogger/HttpsEnabled {"max":1,"min":0,"value":1} 219 | N//settings/0/Settings/VenusApp/LockButtonState {"max":1,"min":0,"value":0} 220 | etc 221 | ``` 222 | 223 | The previous implementation serialized all the answers as json. 224 | 225 | Connecting to the VRM MQTT servers 226 | ------------------------------------- 227 | 228 | If the MQTT service is enabled, the GX Device will forward all notifications from the GX device to the Victron MQTT 229 | servers (see the broker URL section for the correct URL). All communication is encrypted using TLS. 230 | 231 | There are two ways to authenticate: 232 | 233 | - VRM access token. 234 | - VRM username + password. 235 | 236 | The recommended way is to use access tokens. In VRM, you can [manage access tokens here](https://vrm.victronenergy.com/access-tokens). To use them, give your VRM username (email address) as MQTT user, and as password, supply `Token `. 237 | 238 | You can subscribe to the notifications sent by your GX device, as well as send read and write requests to it. You can only receive notifications from systems in your own VRM 239 | site list, and to send write requests you need the 'Full Control' permission. This is the default is you have 240 | registered the system yourself. The 'Monitor Only' permission allows subscription to notifications only 241 | (read only access). 242 | 243 | A convenient way to test this is using the mosquitto_sub tool, which is part of Mosquitto (on debian linux 244 | you need to install the `mosquitto-clients` package). 245 | 246 | This command will get you the total system consumption: 247 | 248 | ```sh 249 | mosquitto_sub -v -I 'myclient_' -c -t 'N//system/0/Ac/Consumption/Total/Power' -h '' -u '' -P '' --cafile 'venus-ca.crt' -p 8883 250 | ``` 251 | 252 | You may need the full path to the cert file. On the GX Device it is in 253 | `/etc/ssl/certs/ccgx-ca.pem`. You can also find the certificate in this repository as `venus-ca.crt`. 254 | 255 | In case you do not receive the value you expect, please read the keep-alive section. 256 | 257 | If you have Full Control permissions on the VRM site, write requests will also be processed. For example: 258 | 259 | ```sh 260 | mosquitto_pub -I 'myclient_' -t 'W//hub4/0/AcPowerSetpoint' -m '{"value":-100}' -h '' -u '' -P '' --cafile 'venus-ca.crt' -p 8883 261 | ``` 262 | 263 | Again: do not set the retain flag when sending write requests. 264 | 265 | ### Determining the broker URL for a given installation 266 | 267 | To allow broker scaling, each installation connects to one of 128 available broker hostnames. To determine the hostname of the broker for an installation, you can either request it from the VRM API, or use an alghorithm. 268 | 269 | For the VRM API, see the [documentation for listing a user's installations](https://vrm-api-docs.victronenergy.com/#/operations/users/idUser/installations). Each site has a field `mqtt_webhost` and `mqtt_host`. Be sure to add `?extended=1` to the API URL. So, for instance: `https://vrmapi.victronenergy.com/v2/users//installations?extended=1`. 270 | 271 | If it's preferred to calculate it yourself, you can use this alghorithm: 272 | 273 | - the `ord()` value of each charachter of the VRM portal ID should be summed. 274 | - the modulo of the sum and 128 determines the broker index number 275 | - the broker URL then is: `mqtt.victronenergy.com`, e.g.: `mqtt101.victronenergy.com` 276 | - the same goes for the websocket host: `webmqtt.victronenergy.com` 277 | 278 | An example implementation of this algorithm in Python is: 279 | 280 | ```python 281 | def _get_vrm_broker_url(self): 282 | sum = 0 283 | for character in self._system_id.lower().strip(): 284 | sum += ord(character) 285 | broker_index = sum % 128 286 | return "mqtt{}.victronenergy.com".format(broker_index) 287 | ``` 288 | 289 | ### On-line server wildcard subscription limitation 290 | 291 | The internet MQTT servers no longer grant very wide subscriptions, such as `#`, `N/#` or `N/+/system/0/Ac/ConsumptionOnOutput/L1/Power`. Instead, subscriptions have to be placed per installation, like `N//#`, or `N//system/0/Ac/ConsumptionOnOutput/+/Power`. 292 | 293 | The reason is that wildcard subscriptions incur a very high load on the servers. If clients ask for every single message, every single message will have to be validated against the permissions of the client in question. This easily multiplies to an excessive degree. 294 | 295 | To find out to which installations you need to subscribe to, you can use the [VRM API](https://vrm-api-docs.victronenergy.com/#/operations/users/idUser/installations). You can periodically query that to find out what installations you need to monitor, and have your MQTT client send subscriptions to the server. 296 | 297 | It doesn't matter that a subscription already exists on the server; if the subscription filter is the same as one you sent before, you won't end-up with duplicate message delivery. So, you don't need to keep track of what you subscribed to, you can just periodically subscribe to `N//#` for all your installations. 298 | 299 | Building 300 | -------- 301 | Once the Venus SDK is active, it's a simple matter of: 302 | 303 | ```sh 304 | mkdir build 305 | cmake [-DCMAKE_BUILD_TYPE=Release] /path/to/project/root 306 | make 307 | ``` 308 | 309 | 310 | -------------------------------------------------------------------------------- /src/boomstring.cpp: -------------------------------------------------------------------------------- 1 | #include "boomstring.h" 2 | 3 | #include 4 | 5 | using namespace dbus_flashmq; 6 | 7 | BoomString::BoomString(const std::string &val) : 8 | s(val) 9 | { 10 | 11 | } 12 | 13 | BoomString::BoomString(const BoomString &other) 14 | { 15 | *this = other; 16 | } 17 | 18 | BoomString &BoomString::operator=(const BoomString &other) 19 | { 20 | if (!this->s.empty() && this->s != other.s) 21 | throw std::runtime_error("Programming error: trying to change a read-only string"); 22 | 23 | this->s = other.s; 24 | return *this; 25 | } 26 | 27 | const std::string &BoomString::get() const 28 | { 29 | return s; 30 | } 31 | -------------------------------------------------------------------------------- /src/boomstring.h: -------------------------------------------------------------------------------- 1 | #ifndef BOOMSTRING_H 2 | #define BOOMSTRING_H 3 | 4 | #include 5 | 6 | namespace dbus_flashmq 7 | { 8 | 9 | class BoomString 10 | { 11 | std::string s; 12 | public: 13 | BoomString() = default; 14 | BoomString(const std::string &val); 15 | BoomString(const BoomString &other); 16 | BoomString &operator=(const BoomString &other); 17 | const std::string &get() const; 18 | }; 19 | 20 | } 21 | 22 | #endif // BOOMSTRING_H 23 | -------------------------------------------------------------------------------- /src/cachedstring.cpp: -------------------------------------------------------------------------------- 1 | #include "cachedstring.h" 2 | 3 | using namespace dbus_flashmq; 4 | 5 | CachedString::CachedString(const CachedString &other) 6 | { 7 | *this = other; 8 | } 9 | 10 | CachedString &CachedString::operator=(const CachedString &other) 11 | { 12 | this->v.clear(); 13 | return *this; 14 | } 15 | -------------------------------------------------------------------------------- /src/cachedstring.h: -------------------------------------------------------------------------------- 1 | #ifndef CACHEDSTRING_H 2 | #define CACHEDSTRING_H 3 | 4 | #include 5 | 6 | namespace dbus_flashmq 7 | { 8 | 9 | /** 10 | * @brief Cached string that by the grace of RAII should be imune to staleness when used as a member of a class. 11 | */ 12 | struct CachedString 13 | { 14 | std::string v; 15 | 16 | CachedString() = default; 17 | CachedString(CachedString &&other) = delete; 18 | CachedString(const CachedString &other); 19 | CachedString &operator=(const CachedString &other); 20 | }; 21 | 22 | } 23 | 24 | #endif // CACHEDSTRING_H 25 | -------------------------------------------------------------------------------- /src/dbus_functions.cpp: -------------------------------------------------------------------------------- 1 | #include "dbus_functions.h" 2 | #include "vendor/flashmq_plugin.h" 3 | #include "state.h" 4 | #include "utils.h" 5 | #include "dbusmessageguard.h" 6 | 7 | #include "dbusutils.h" 8 | #include "dbuserrorguard.h" 9 | #include "dbusmessageitersignature.h" 10 | 11 | using namespace dbus_flashmq; 12 | 13 | void dbus_flashmq::dbus_dispatch_status_function(DBusConnection *connection, DBusDispatchStatus new_status, void *data) 14 | { 15 | State *state = static_cast(data); 16 | 17 | if (new_status == DBusDispatchStatus::DBUS_DISPATCH_DATA_REMAINS) 18 | { 19 | state->setDispatchable(); 20 | } 21 | } 22 | 23 | dbus_bool_t dbus_flashmq::dbus_add_watch_function(DBusWatch *watch, void *data) 24 | { 25 | State *state = static_cast(data); 26 | 27 | const int fd = dbus_watch_get_unix_fd(watch); 28 | std::shared_ptr &w = state->watches[fd]; 29 | 30 | if (!w) 31 | w = std::make_shared(); 32 | 33 | w->fd = fd; 34 | w->add_watch(watch); 35 | 36 | try 37 | { 38 | int epoll_events = w->get_combined_epoll_flags(); 39 | flashmq_poll_add_fd(fd, epoll_events, w); 40 | } 41 | catch (std::exception &ex) 42 | { 43 | flashmq_logf(LOG_ERR, ex.what()); 44 | return false; 45 | } 46 | 47 | return true; 48 | } 49 | 50 | void dbus_flashmq::dbus_remove_watch_function(DBusWatch *watch, void *data) 51 | { 52 | State *state = static_cast(data); 53 | const int fd = dbus_watch_get_unix_fd(watch); 54 | 55 | auto pos = state->watches.find(fd); 56 | if (pos == state->watches.end()) 57 | return; 58 | 59 | std::shared_ptr &w = pos->second; 60 | 61 | if (!w) 62 | return; 63 | 64 | w->remove_watch(watch); 65 | 66 | try 67 | { 68 | if (w->empty()) 69 | flashmq_poll_remove_fd(fd); 70 | } 71 | catch (std::exception &ex) 72 | { 73 | flashmq_logf(LOG_ERR, ex.what()); 74 | } 75 | } 76 | 77 | void dbus_flashmq::dbus_toggle_watch_function(DBusWatch *watch, void *data) 78 | { 79 | // I'm not fully sure what this callback wants me to do, but I'm guessing changing the EPOLL events I'm watching for, 80 | // which in FlashMQ is the same function. 81 | dbus_add_watch_function(watch, data); 82 | } 83 | 84 | void dbus_flashmq::dbus_timeout_do_handle(DBusTimeout *timeout) 85 | { 86 | dbus_bool_t result = dbus_timeout_handle(timeout); 87 | 88 | // FlashMQ's tasks are one-shot, so dbus's suggestion to let the timeout fire again needs to be explicitly queued. 89 | if (!result) 90 | { 91 | auto f = std::bind(&dbus_timeout_do_handle, timeout); 92 | int interval = dbus_timeout_get_interval(timeout); 93 | 94 | flashmq_add_task(f, interval); 95 | } 96 | } 97 | 98 | dbus_bool_t dbus_flashmq::dbus_add_timeout_function(DBusTimeout *timeout, void *data) 99 | { 100 | auto f = std::bind(&dbus_timeout_do_handle, timeout); 101 | int interval = dbus_timeout_get_interval(timeout); 102 | 103 | try 104 | { 105 | // Just storing the id as address, because it saves allocation. 106 | uint32_t id = flashmq_add_task(f, interval); 107 | int *id2 = reinterpret_cast(id); 108 | dbus_timeout_set_data(timeout, id2, nullptr); 109 | } 110 | catch (std::exception &ex) 111 | { 112 | flashmq_logf(LOG_ERR, ex.what()); 113 | return false; 114 | } 115 | 116 | return true; 117 | } 118 | 119 | void dbus_flashmq::dbus_remove_timeout_function(DBusTimeout *timeout, void *data) 120 | { 121 | try 122 | { 123 | int *id2 = static_cast(dbus_timeout_get_data(timeout)); 124 | uint32_t id = reinterpret_cast(id2); 125 | flashmq_remove_task(id); 126 | } 127 | catch (std::exception &ex) 128 | { 129 | flashmq_logf(LOG_ERR, ex.what()); 130 | } 131 | } 132 | 133 | void dbus_flashmq::dbus_toggle_timeout_function(DBusTimeout *timeout, void *data) 134 | { 135 | // Say whaaaat?! 136 | //flashmq_logf(LOG_ERR, "What do I do here?"); 137 | } 138 | 139 | /** 140 | * @brief dbus_pending_call_notify is called by dbus when a reply is received, or when it times out. 141 | * @param pending 142 | * @param data 143 | * 144 | * When the timeout happens, the handler is still called. The message type is 'DBUS_MESSAGE_TYPE_ERROR' 145 | * with 'org.freedesktop.DBus.Error.NoReply', so it will have to know how to handle that. 146 | * 147 | * Called from dbus internals, so we can't throw exceptions. 148 | */ 149 | void dbus_flashmq::dbus_pending_call_notify(DBusPendingCall *pending, void *data) noexcept 150 | { 151 | State *state = static_cast(data); 152 | 153 | if (!dbus_pending_call_get_completed(pending)) 154 | { 155 | flashmq_logf(LOG_ERR, "We were notified about an incomplete pending reply?"); 156 | return; 157 | } 158 | 159 | const DBusMessageGuard msg = dbus_pending_call_steal_reply(pending); 160 | const dbus_uint32_t reply_to = dbus_message_get_reply_serial(msg.d); 161 | 162 | if (reply_to == 0) 163 | { 164 | flashmq_logf(LOG_ERR, "Dbus reply could not be matched to antyhing."); 165 | return; 166 | } 167 | 168 | const int msg_type = dbus_message_get_type(msg.d); 169 | 170 | if (!(msg_type == DBUS_MESSAGE_TYPE_METHOD_RETURN || msg_type == DBUS_MESSAGE_TYPE_ERROR)) 171 | { 172 | flashmq_logf(LOG_ERR, "Pending call notification is not a method return or error. Weird?"); 173 | return; 174 | } 175 | 176 | auto fpos = state->async_handlers.find(reply_to); 177 | 178 | if (fpos != state->async_handlers.end()) 179 | { 180 | try 181 | { 182 | auto f = fpos->second; 183 | state->async_handlers.erase(fpos); 184 | f(msg.d); 185 | } 186 | catch (std::exception &ex) 187 | { 188 | flashmq_logf(LOG_ERR, ex.what()); 189 | } 190 | } 191 | } 192 | 193 | DBusHandlerResult dbus_flashmq::dbus_handle_message(DBusConnection *connection, DBusMessage *message, void *user_data) 194 | { 195 | const char *_signal_name = dbus_message_get_member(message); 196 | const std::string signal_name(_signal_name ? _signal_name : ""); 197 | 198 | try 199 | { 200 | State *state = static_cast(user_data); 201 | int msg_type = dbus_message_get_type(message); 202 | 203 | const char *_sender = dbus_message_get_sender(message); 204 | std::string sender(_sender ? _sender : ""); 205 | 206 | if (msg_type == DBUS_MESSAGE_TYPE_SIGNAL) 207 | { 208 | state->attempt_to_process_delayed_changes(); 209 | 210 | if (signal_name == "NameAcquired") 211 | { 212 | const char *_name = nullptr; 213 | DBusErrorGuard err; 214 | dbus_message_get_args(message, err.get(), DBUS_TYPE_STRING, &_name, DBUS_TYPE_INVALID); 215 | err.throw_error(); 216 | 217 | std::string name(_name); 218 | 219 | flashmq_logf(LOG_DEBUG, "Signal: '%s' by '%s'. Name: '%s'", signal_name.c_str(), sender.c_str(), name.c_str()); 220 | return DBusHandlerResult::DBUS_HANDLER_RESULT_NOT_YET_HANDLED; 221 | } 222 | else if (signal_name == "NameOwnerChanged") 223 | { 224 | const char *_name = nullptr; 225 | const char *_oldowner = nullptr; 226 | const char *_newowner = nullptr; 227 | 228 | DBusErrorGuard err; 229 | dbus_message_get_args(message, err.get(), DBUS_TYPE_STRING, &_name, DBUS_TYPE_STRING, &_oldowner, DBUS_TYPE_STRING, &_newowner, DBUS_TYPE_INVALID); 230 | err.throw_error(); 231 | 232 | std::string name(_name); 233 | std::string oldowner(_oldowner); 234 | std::string newowner(_newowner); 235 | 236 | if (name.find("com.victronenergy.") == std::string::npos) 237 | return DBusHandlerResult::DBUS_HANDLER_RESULT_NOT_YET_HANDLED; 238 | 239 | if (!newowner.empty()) 240 | { 241 | flashmq_logf(LOG_INFO, "[OwnerChange] Service appeared: '%s' with owner '%s'", name.c_str(), newowner.c_str()); 242 | state->set_new_id_to_owner(newowner, name); 243 | state->scan_dbus_service(name); 244 | } 245 | else if (!oldowner.empty()) 246 | { 247 | flashmq_logf(LOG_INFO, "[OwnerChange] Service disappeared: '%s' with owner '%s'", name.c_str(), oldowner.c_str()); 248 | state->remove_id_to_owner(oldowner); 249 | state->remove_dbus_service(name); 250 | } 251 | 252 | return DBusHandlerResult::DBUS_HANDLER_RESULT_HANDLED; 253 | } 254 | 255 | sender = state->get_named_owner(sender); 256 | //flashmq_logf(LOG_DEBUG, "Received signal: '%s' by '%s'", signal_name.c_str(), sender.c_str()); 257 | 258 | if (sender.find("com.victronenergy") != std::string::npos) 259 | { 260 | // The preferred signal, containing multiple items. The format is used by both ItemsChanged and the method call GetItems. 261 | if (signal_name == "ItemsChanged") 262 | { 263 | std::unordered_map changed_items = get_from_dict_with_dict_with_text_and_value(message); 264 | state->add_dbus_to_mqtt_mapping(sender, changed_items, true); 265 | 266 | return DBusHandlerResult::DBUS_HANDLER_RESULT_HANDLED; 267 | } 268 | 269 | // Will contain the update for only one item. 270 | if (signal_name == "PropertiesChanged") 271 | { 272 | std::unordered_map changed_items = get_from_properties_changed(message); 273 | state->add_dbus_to_mqtt_mapping(sender, changed_items, true); 274 | 275 | return DBusHandlerResult::DBUS_HANDLER_RESULT_HANDLED; 276 | } 277 | } 278 | 279 | flashmq_logf(LOG_INFO, "Unhandled signal: '%s' by '%s'", signal_name.c_str(), sender.c_str()); 280 | } 281 | } 282 | catch (std::exception &ex) 283 | { 284 | flashmq_logf(LOG_ERR, "On signal '%s' in dbus_handle_message: %s", signal_name.c_str(), ex.what()); 285 | return DBusHandlerResult::DBUS_HANDLER_RESULT_HANDLED; 286 | } 287 | 288 | return DBusHandlerResult::DBUS_HANDLER_RESULT_NOT_YET_HANDLED; 289 | } 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | -------------------------------------------------------------------------------- /src/dbus_functions.h: -------------------------------------------------------------------------------- 1 | #ifndef DBUS_FUNCTIONS_H 2 | #define DBUS_FUNCTIONS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace dbus_flashmq 11 | { 12 | 13 | void dbus_dispatch_status_function(DBusConnection *connection, DBusDispatchStatus new_status, void *data); 14 | 15 | dbus_bool_t dbus_add_watch_function(DBusWatch *watch, void *data); 16 | void dbus_remove_watch_function(DBusWatch *watch, void *data); 17 | void dbus_toggle_watch_function(DBusWatch *watch, void *data); 18 | DBusHandlerResult dbus_handle_message(DBusConnection *connection, DBusMessage *message, void *user_data); 19 | 20 | dbus_bool_t dbus_add_timeout_function(DBusTimeout *timeout, void *data); 21 | void dbus_remove_timeout_function(DBusTimeout *timeout, void *data); 22 | void dbus_toggle_timeout_function(DBusTimeout *timeout, void *data); 23 | void dbus_timeout_do_handle(DBusTimeout *timeout); 24 | 25 | void dbus_pending_call_notify(DBusPendingCall *pending, void *data) noexcept; 26 | 27 | } 28 | 29 | #endif // DBUS_FUNCTIONS_H 30 | -------------------------------------------------------------------------------- /src/dbuserrorguard.cpp: -------------------------------------------------------------------------------- 1 | #include "dbuserrorguard.h" 2 | #include 3 | #include 4 | 5 | using namespace dbus_flashmq; 6 | 7 | DBusErrorGuard::DBusErrorGuard() 8 | { 9 | dbus_error_init(&err); 10 | } 11 | 12 | DBusErrorGuard::~DBusErrorGuard() 13 | { 14 | if (dbus_error_is_set(&err)) 15 | { 16 | dbus_error_free(&err); 17 | } 18 | } 19 | 20 | DBusError *DBusErrorGuard::get() 21 | { 22 | return &this->err; 23 | } 24 | 25 | void DBusErrorGuard::throw_error() 26 | { 27 | if (dbus_error_is_set(&err)) 28 | { 29 | std::string s(err.message); 30 | throw std::runtime_error(s); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/dbuserrorguard.h: -------------------------------------------------------------------------------- 1 | #ifndef DBUSERRORGUARD_H 2 | #define DBUSERRORGUARD_H 3 | 4 | #include 5 | 6 | namespace dbus_flashmq 7 | { 8 | 9 | /** 10 | * @brief The DBusErrorGuard class makes dealing with DBusErrors easier and prevent leaks. 11 | * 12 | * https://dbus.freedesktop.org/doc/api/html/group__DBusErrors.html 13 | */ 14 | class DBusErrorGuard 15 | { 16 | DBusError err; 17 | public: 18 | DBusErrorGuard(); 19 | ~DBusErrorGuard(); 20 | DBusError *get(); 21 | void throw_error(); 22 | }; 23 | 24 | } 25 | 26 | #endif // DBUSERRORGUARD_H 27 | -------------------------------------------------------------------------------- /src/dbusmessageguard.cpp: -------------------------------------------------------------------------------- 1 | #include "dbusmessageguard.h" 2 | 3 | using namespace dbus_flashmq; 4 | 5 | DBusMessageGuard::DBusMessageGuard(DBusMessage *msg) : 6 | d(msg) 7 | { 8 | 9 | } 10 | 11 | DBusMessageGuard::~DBusMessageGuard() 12 | { 13 | if (d) 14 | { 15 | dbus_message_unref(d); 16 | d = nullptr; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/dbusmessageguard.h: -------------------------------------------------------------------------------- 1 | #ifndef DBUSMESSAGEGUARD_H 2 | #define DBUSMESSAGEGUARD_H 3 | 4 | #include 5 | 6 | namespace dbus_flashmq 7 | { 8 | 9 | struct DBusMessageGuard 10 | { 11 | DBusMessage *d = nullptr; 12 | 13 | DBusMessageGuard(DBusMessage *msg); 14 | DBusMessageGuard(const DBusMessageGuard &other) = delete; 15 | DBusMessageGuard(DBusMessageGuard &&other) = delete; 16 | ~DBusMessageGuard(); 17 | }; 18 | 19 | } 20 | 21 | #endif // DBUSMESSAGEGUARD_H 22 | -------------------------------------------------------------------------------- /src/dbusmessageiteropencontainerguard.cpp: -------------------------------------------------------------------------------- 1 | #include "dbusmessageiteropencontainerguard.h" 2 | 3 | #include 4 | 5 | using namespace dbus_flashmq; 6 | 7 | DBusMessageIterOpenContainerGuard::DBusMessageIterOpenContainerGuard(DBusMessageIter *iter, int container_type, const char *contained_signature) : 8 | iter(iter) 9 | { 10 | if (!dbus_message_iter_open_container(iter, container_type, contained_signature, &array_iter)) 11 | { 12 | throw std::runtime_error("dbus_message_iter_open_container failed."); 13 | } 14 | 15 | open = true; 16 | } 17 | 18 | DBusMessageIterOpenContainerGuard::~DBusMessageIterOpenContainerGuard() 19 | { 20 | if (!iter) 21 | return; 22 | 23 | if (open) 24 | dbus_message_iter_close_container(iter, &array_iter); 25 | 26 | iter = nullptr; 27 | } 28 | 29 | DBusMessageIter *DBusMessageIterOpenContainerGuard::get_array_iter() 30 | { 31 | return &array_iter; 32 | } 33 | -------------------------------------------------------------------------------- /src/dbusmessageiteropencontainerguard.h: -------------------------------------------------------------------------------- 1 | #ifndef DBUSMESSAGEITEROPENCONTAINERGUARD_H 2 | #define DBUSMESSAGEITEROPENCONTAINERGUARD_H 3 | 4 | #include 5 | 6 | namespace dbus_flashmq 7 | { 8 | 9 | class DBusMessageIterOpenContainerGuard 10 | { 11 | DBusMessageIter *iter = nullptr; 12 | DBusMessageIter array_iter; 13 | bool open = false; 14 | public: 15 | DBusMessageIterOpenContainerGuard(DBusMessageIter *iter, int container_type, const char *contained_signature); 16 | ~DBusMessageIterOpenContainerGuard(); 17 | DBusMessageIter *get_array_iter(); 18 | }; 19 | 20 | } 21 | 22 | #endif // DBUSMESSAGEITEROPENCONTAINERGUARD_H 23 | -------------------------------------------------------------------------------- /src/dbusmessageitersignature.cpp: -------------------------------------------------------------------------------- 1 | #include "dbusmessageitersignature.h" 2 | 3 | using namespace dbus_flashmq; 4 | 5 | std::string DBusMessageIterSignature::getSignature(DBusMessageIter *iter) const 6 | { 7 | char *_sig = dbus_message_iter_get_signature(iter); 8 | std::string signature(_sig); 9 | dbus_free(_sig); 10 | _sig = nullptr; 11 | return signature; 12 | } 13 | 14 | DBusMessageIterSignature::DBusMessageIterSignature(DBusMessageIter *iter) : 15 | signature(getSignature(iter)) 16 | { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/dbusmessageitersignature.h: -------------------------------------------------------------------------------- 1 | #ifndef DBUSMESSAGEITERSIGNATURE_H 2 | #define DBUSMESSAGEITERSIGNATURE_H 3 | 4 | #include 5 | #include 6 | 7 | namespace dbus_flashmq 8 | { 9 | 10 | class DBusMessageIterSignature 11 | { 12 | std::string getSignature(DBusMessageIter *iter) const; 13 | public: 14 | DBusMessageIterSignature(DBusMessageIter *iter); 15 | 16 | const std::string signature; 17 | }; 18 | 19 | } 20 | 21 | #endif // DBUSMESSAGEITERSIGNATURE_H 22 | -------------------------------------------------------------------------------- /src/dbuspendingmessagecallguard.cpp: -------------------------------------------------------------------------------- 1 | #include "dbuspendingmessagecallguard.h" 2 | 3 | using namespace dbus_flashmq; 4 | 5 | DBusPendingMessageCallGuard::DBusPendingMessageCallGuard() 6 | { 7 | 8 | } 9 | 10 | DBusPendingMessageCallGuard::~DBusPendingMessageCallGuard() 11 | { 12 | dbus_pending_call_unref(d); 13 | } 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/dbuspendingmessagecallguard.h: -------------------------------------------------------------------------------- 1 | #ifndef DBUSPENDINGMESSAGECALLGUARD_H 2 | #define DBUSPENDINGMESSAGECALLGUARD_H 3 | 4 | #include 5 | 6 | namespace dbus_flashmq 7 | { 8 | 9 | /** 10 | * @brief The DBusPendingMessageCallGuard class releases the reference you get after dbus_connection_send_with_reply(), for instance. 11 | * 12 | * The documentation of dbus_connection_send_with_reply() is not clear about whether you own a reference, but apparently you do. 13 | */ 14 | struct DBusPendingMessageCallGuard 15 | { 16 | DBusPendingCall *d = nullptr; 17 | 18 | DBusPendingMessageCallGuard(); 19 | DBusPendingMessageCallGuard(const DBusPendingMessageCallGuard &other) = delete; 20 | DBusPendingMessageCallGuard(DBusPendingMessageCallGuard &&other) = delete; 21 | ~DBusPendingMessageCallGuard(); 22 | }; 23 | 24 | } 25 | 26 | 27 | #endif // DBUSPENDINGMESSAGECALLGUARD_H 28 | -------------------------------------------------------------------------------- /src/dbusutils.cpp: -------------------------------------------------------------------------------- 1 | #include "dbusutils.h" 2 | #include "vendor/flashmq_plugin.h" 3 | #include "exceptions.h" 4 | #include "dbusmessageitersignature.h" 5 | 6 | using namespace dbus_flashmq; 7 | 8 | // TODO: we need some kind of generalization / template for this, but I'm waiting with that until I know all the variants of result 9 | // sets that I'm getting. 10 | std::vector dbus_flashmq::get_array_from_reply(DBusMessage *msg) 11 | { 12 | int msg_type = dbus_message_get_type(msg); 13 | 14 | if (msg_type != DBUS_MESSAGE_TYPE_METHOD_RETURN && msg_type != DBUS_MESSAGE_TYPE_SIGNAL) 15 | throw std::runtime_error("Message is not a method return or signal."); 16 | 17 | std::vector result; 18 | 19 | int result_n = 0; 20 | DBusMessageIter iter; 21 | int current_type = 0; 22 | dbus_message_iter_init(msg, &iter); 23 | 24 | while ((current_type = dbus_message_iter_get_arg_type (&iter)) != DBUS_TYPE_INVALID) 25 | { 26 | if (current_type != DBUS_TYPE_ARRAY) 27 | throw std::runtime_error("Result is not an array"); 28 | 29 | if (result_n++ > 1) 30 | throw std::runtime_error("there is more than one value in the result. We expected one array."); 31 | 32 | int n = dbus_message_iter_get_element_count(&iter); 33 | result.reserve(n); 34 | 35 | DBusMessageIter sub_iter; 36 | int current_sub_type = 0; 37 | dbus_message_iter_recurse(&iter, &sub_iter); 38 | while((current_sub_type = dbus_message_iter_get_arg_type(&sub_iter)) != DBUS_TYPE_INVALID) 39 | { 40 | if constexpr (std::is_same_v) // TODO: construct to be used if we template this. 41 | { 42 | if (current_sub_type != DBUS_TYPE_STRING) 43 | throw std::runtime_error("Result contains type we didn't expect."); 44 | } 45 | 46 | DBusBasicValue value; 47 | dbus_message_iter_get_basic(&sub_iter, &value); 48 | 49 | std::string s(value.str); 50 | 51 | result.push_back(std::move(s)); 52 | 53 | dbus_message_iter_next(&sub_iter); 54 | } 55 | 56 | dbus_message_iter_next(&iter); 57 | } 58 | 59 | return result; 60 | } 61 | 62 | /** 63 | * @brief Parse the result from a com.victronenergy.BusItem.GetItems() call and ItemsChanged signal. 64 | * @param msg 65 | * @return 66 | * 67 | * The signature should be a{sa{sv}}. 68 | * 69 | * Example return value as given by dbus-send: 70 | * 71 | * method return time=1681786815.914743 sender=:1.98 -> destination=:1.513923 serial=3113638 reply_serial=2 72 | * array [ 73 | * dict entry( 74 | * string "/Mgmt/ProcessName" 75 | * array [ 76 | * dict entry( 77 | * string "Value" 78 | * variant string "/opt/victronenergy/dbus-systemcalc-py/dbus_systemcalc.py" 79 | * ) 80 | * dict entry( 81 | * string "Text" 82 | * variant string "/opt/victronenergy/dbus-systemcalc-py/dbus_systemcalc.py" 83 | * ) 84 | * ] 85 | * ) 86 | * dict entry( 87 | * string "/Mgmt/ProcessVersion" 88 | * array [ 89 | * dict entry( 90 | * string "Value" 91 | * variant string "2.94" 92 | * ) 93 | * dict entry( 94 | * string "Text" 95 | * variant string "2.94" 96 | * ) 97 | * ] 98 | * ) 99 | * ] 100 | */ 101 | std::unordered_map dbus_flashmq::get_from_dict_with_dict_with_text_and_value(DBusMessage *msg) 102 | { 103 | int msg_type = dbus_message_get_type(msg); 104 | 105 | if (msg_type != DBUS_MESSAGE_TYPE_METHOD_RETURN && msg_type != DBUS_MESSAGE_TYPE_SIGNAL) 106 | throw std::runtime_error("Message is not a method return or signal."); 107 | 108 | std::unordered_map result; 109 | 110 | int result_n = 0; 111 | DBusMessageIter iter; 112 | int current_type = 0; 113 | dbus_message_iter_init(msg, &iter); 114 | 115 | DBusMessageIterSignature signature(&iter); 116 | 117 | if (signature.signature != "a{sa{sv}}") 118 | throw std::runtime_error("Return from GetItems() is not the correct signature"); 119 | 120 | while ((current_type = dbus_message_iter_get_arg_type (&iter)) != DBUS_TYPE_INVALID) 121 | { 122 | if (current_type != DBUS_TYPE_ARRAY) 123 | throw std::runtime_error("Result from GetItems is not an array"); 124 | 125 | if (result_n++ > 1) 126 | { 127 | flashmq_logf(LOG_ERR, "There is more than one value in the result of GetItems(). We expected one array. Ignoring it."); 128 | break; 129 | } 130 | 131 | DBusMessageIter sub_iter; 132 | dbus_message_iter_recurse(&iter, &sub_iter); 133 | while(dbus_message_iter_get_arg_type(&sub_iter) != DBUS_TYPE_INVALID) 134 | { 135 | try 136 | { 137 | Item item = Item::from_get_items(&sub_iter); 138 | result[item.get_path()] = item; 139 | } 140 | catch (std::exception &er) 141 | { 142 | flashmq_logf(LOG_ERR, "Skipping item creation because: %s", er.what()); 143 | } 144 | 145 | dbus_message_iter_next(&sub_iter); 146 | } 147 | 148 | dbus_message_iter_next(&iter); 149 | } 150 | 151 | return result; 152 | } 153 | 154 | /** 155 | * @brief Doing GetValue on / gives a variant with array of the items. GetItems is preferred though. For one, GetValue doesn't return 'text'. 156 | * @param msg 157 | * @return 158 | * 159 | * Example return value: 160 | * 161 | * dbus-send --system --print-reply --dest=com.victronenergy.solarcharger.ttyO2 / com.victronenergy.BusItem.GetValue 162 | * 163 | * Output: 164 | * 165 | * variant array [ 166 | * dict entry( 167 | * string "History/Daily/18/TimeInFloat" 168 | * variant double 0 169 | * ) 170 | * dict entry( 171 | * string "History/Daily/29/TimeInBulk" 172 | * variant double 0 173 | * ) 174 | * dict entry( 175 | * string "Yield/Power" 176 | * variant double 251 177 | * ) 178 | * etc 179 | * ] 180 | * 181 | */ 182 | std::unordered_map dbus_flashmq::get_from_get_value_on_root(DBusMessage *msg, const std::string &path_prefix) 183 | { 184 | int msg_type = dbus_message_get_type(msg); 185 | 186 | if (msg_type != DBUS_MESSAGE_TYPE_METHOD_RETURN && msg_type != DBUS_MESSAGE_TYPE_SIGNAL) 187 | throw std::runtime_error("Message is not a method return or signal."); 188 | 189 | std::unordered_map result; 190 | 191 | int result_n = 0; 192 | DBusMessageIter iter; 193 | int current_type = 0; 194 | dbus_message_iter_init(msg, &iter); 195 | 196 | DBusMessageIterSignature signature(&iter); 197 | 198 | if (signature.signature != "v") 199 | throw std::runtime_error("Return from GetValue() is not the correct signature"); 200 | 201 | while ((current_type = dbus_message_iter_get_arg_type (&iter)) != DBUS_TYPE_INVALID) 202 | { 203 | if (current_type != DBUS_TYPE_VARIANT) 204 | throw std::runtime_error("Result from GetValue() is not a variant"); 205 | 206 | if (result_n++ > 1) 207 | { 208 | flashmq_logf(LOG_ERR, "There is more than one value in the result of GetValue(). We expected one variant. Ignoring it."); 209 | break; 210 | } 211 | 212 | DBusMessageIter outer_variant_iter; 213 | dbus_message_iter_recurse(&iter, &outer_variant_iter); 214 | 215 | if (dbus_message_iter_get_arg_type(&outer_variant_iter) != DBUS_TYPE_ARRAY) 216 | throw std::runtime_error("Expected string as dict key."); 217 | 218 | DBusMessageIter variant_array_iter; 219 | dbus_message_iter_recurse(&outer_variant_iter, &variant_array_iter); 220 | 221 | while(dbus_message_iter_get_arg_type(&variant_array_iter) != DBUS_TYPE_INVALID) 222 | { 223 | try 224 | { 225 | Item item = Item::from_get_value(&variant_array_iter, path_prefix); 226 | result[item.get_path()] = item; 227 | } 228 | catch (std::exception &er) 229 | { 230 | flashmq_logf(LOG_ERR, "Skipping item creation because: %s", er.what()); 231 | } 232 | 233 | dbus_message_iter_next(&variant_array_iter); 234 | } 235 | 236 | dbus_message_iter_next(&iter); 237 | } 238 | 239 | return result; 240 | } 241 | 242 | /** 243 | * @brief get_from_properties_changed processes a PropertiesChanged signal into a list of Item, even though it's only one. It allows for passing to 244 | * add_dbus_to_mqtt_mapping(...). 245 | * @param msg 246 | * @return a list of Item, even though it's only one. 247 | * 248 | * Example signal PropertiesChanged: 249 | * 250 | * signal time=1691485092.671780 sender=:1.47 -> destination=(null destination) serial=8149 path=/Settings/Pump0/TankService; interface=com.victronenergy.BusItem; member=PropertiesChanged 251 | * array [ 252 | * dict entry( 253 | * string "Value" 254 | * variant string "notanksensor" 255 | * ) 256 | * dict entry( 257 | * string "Text" 258 | * variant string "notanksensor" 259 | * ) 260 | * dict entry( 261 | * string "Min" 262 | * variant int32 0 263 | * ) 264 | * dict entry( 265 | * string "Max" 266 | * variant int32 0 267 | * ) 268 | * dict entry( 269 | * string "Default" 270 | * variant string "notanksensor" 271 | * ) 272 | * ] 273 | */ 274 | std::unordered_map dbus_flashmq::get_from_properties_changed(DBusMessage *msg) 275 | { 276 | std::unordered_map result; 277 | 278 | Item item = Item::from_properties_changed(msg); 279 | result[item.get_path()] = item; 280 | 281 | return result; 282 | } 283 | 284 | // TODO: for the basic types, I think I can template this one. 285 | std::string dbus_flashmq::get_string_from_reply(DBusMessage *msg) 286 | { 287 | const int msg_type = dbus_message_get_type(msg); 288 | 289 | if (msg_type != DBUS_MESSAGE_TYPE_METHOD_RETURN && msg_type != DBUS_MESSAGE_TYPE_SIGNAL) 290 | throw std::runtime_error("Message is not a method return or signal."); 291 | 292 | DBusMessageIter iter; 293 | dbus_message_iter_init(msg, &iter); 294 | 295 | if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_STRING) 296 | { 297 | throw ValueError("Trying to read a string, but it's not."); 298 | } 299 | 300 | DBusBasicValue val; 301 | dbus_message_iter_get_basic(&iter, &val); 302 | std::string result(val.str); 303 | return result; 304 | } 305 | -------------------------------------------------------------------------------- /src/dbusutils.h: -------------------------------------------------------------------------------- 1 | #ifndef DBUSUTILS_H 2 | #define DBUSUTILS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "types.h" 10 | 11 | 12 | namespace dbus_flashmq 13 | { 14 | 15 | std::vector get_array_from_reply(DBusMessage *msg); 16 | std::unordered_map get_from_dict_with_dict_with_text_and_value(DBusMessage *msg); 17 | std::unordered_map get_from_get_value_on_root(DBusMessage *msg, const std::string &path_prefix); 18 | std::unordered_map get_from_properties_changed(DBusMessage *msg); 19 | std::string get_string_from_reply(DBusMessage *msg); 20 | 21 | } 22 | 23 | #endif // DBUSUTILS_H 24 | -------------------------------------------------------------------------------- /src/exceptions.cpp: -------------------------------------------------------------------------------- 1 | #include "exceptions.h" 2 | 3 | using namespace dbus_flashmq; 4 | 5 | ItemNotFound::ItemNotFound(const std::string &msg, const std::string &service, const std::string &dbus_like_path) : 6 | std::runtime_error(msg), 7 | service(service), 8 | dbus_like_path(dbus_like_path) 9 | { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/exceptions.h: -------------------------------------------------------------------------------- 1 | #ifndef EXCEPTIONS_H 2 | #define EXCEPTIONS_H 3 | 4 | #include 5 | 6 | namespace dbus_flashmq 7 | { 8 | 9 | class ValueError : public std::runtime_error 10 | { 11 | public: 12 | ValueError(const std::string &msg) : std::runtime_error(msg) {} 13 | }; 14 | 15 | class ItemNotFound : public std::runtime_error 16 | { 17 | public: 18 | const std::string service; 19 | const std::string dbus_like_path; 20 | 21 | ItemNotFound(const std::string &msg, const std::string &service, const std::string &dbus_like_path); 22 | }; 23 | 24 | } 25 | 26 | #endif // EXCEPTIONS_H 27 | -------------------------------------------------------------------------------- /src/fdguard.cpp: -------------------------------------------------------------------------------- 1 | #include "fdguard.h" 2 | 3 | #include 4 | 5 | using namespace dbus_flashmq; 6 | 7 | FdGuard::FdGuard(int fd) : 8 | fd(fd) 9 | { 10 | 11 | } 12 | 13 | FdGuard::~FdGuard() 14 | { 15 | if (fd > 0) 16 | close(fd); 17 | fd = -1; 18 | } 19 | 20 | int FdGuard::get() const 21 | { 22 | return this->fd; 23 | } 24 | -------------------------------------------------------------------------------- /src/fdguard.h: -------------------------------------------------------------------------------- 1 | #ifndef FDGUARD_H 2 | #define FDGUARD_H 3 | 4 | namespace dbus_flashmq 5 | { 6 | 7 | class FdGuard 8 | { 9 | int fd = -1; 10 | public: 11 | FdGuard(int fd); 12 | ~FdGuard(); 13 | int get() const; 14 | }; 15 | 16 | } 17 | 18 | #endif // FDGUARD_H 19 | -------------------------------------------------------------------------------- /src/flashmq-dbus-plugin-tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "flashmq-dbus-plugin-tests.h" 5 | #include "vendor/flashmq_plugin.h" 6 | #include "testerglobals.h" 7 | #include "utils.h" 8 | 9 | #define MAX_EVENTS 25 10 | 11 | void tests_init_once() 12 | { 13 | testCount = 0; 14 | failCount = 0; 15 | } 16 | 17 | using namespace dbus_flashmq; 18 | 19 | int pre_event_loop_test() 20 | { 21 | FMQ_COMPARE(true, true); 22 | 23 | return 0; 24 | } 25 | 26 | int main(int argc, char **argv) 27 | { 28 | tests_init_once(); 29 | 30 | if (!crypt_match("hallo", "$2a$08$LBfjL0PfMBbjWxCzLBfjLurkA7K0tuDn44rNUXDBvatSgSqHvwaHS")) 31 | { 32 | throw std::runtime_error("crypt test failed."); 33 | } 34 | 35 | std::unordered_map pluginOpts; 36 | 37 | TesterGlobals *globals = TesterGlobals::getInstance(); 38 | globals->epoll_fd = epoll_create(1024); 39 | 40 | void *data = nullptr;; 41 | 42 | flashmq_plugin_main_init(pluginOpts); 43 | flashmq_plugin_allocate_thread_memory(&data, pluginOpts); 44 | 45 | flashmq_plugin_init(data, pluginOpts, false); 46 | 47 | pre_event_loop_test(); 48 | 49 | struct epoll_event events[MAX_EVENTS]; 50 | memset(&events, 0, sizeof (struct epoll_event)*MAX_EVENTS); 51 | 52 | while (true) 53 | { 54 | const uint32_t next_task_delay = globals->delayedTasks.getTimeTillNext(); 55 | const uint32_t epoll_wait_time = std::min(next_task_delay, 100); 56 | 57 | const int num_fds = epoll_wait(globals->epoll_fd, events, MAX_EVENTS, epoll_wait_time); 58 | 59 | if (epoll_wait_time == 0) 60 | { 61 | globals->delayedTasks.performAll(); 62 | } 63 | 64 | if (num_fds < 0) 65 | { 66 | if (errno == EINTR) 67 | continue; 68 | } 69 | 70 | for (int i = 0; i < num_fds; i++) 71 | { 72 | int cur_fd = events[i].data.fd; 73 | 74 | auto pos = globals->watchedFds.find(cur_fd); 75 | if (pos != globals->watchedFds.end()) 76 | { 77 | std::weak_ptr &p = pos->second; 78 | flashmq_plugin_poll_event_received(data, cur_fd, events[i].events, p); 79 | } 80 | } 81 | } 82 | 83 | flashmq_plugin_deinit(data, pluginOpts, false); 84 | 85 | flashmq_plugin_deallocate_thread_memory(data, pluginOpts); 86 | flashmq_plugin_main_deinit(pluginOpts); 87 | 88 | if (failCount > 0) 89 | return 66; 90 | 91 | return 0; 92 | } 93 | -------------------------------------------------------------------------------- /src/flashmq-dbus-plugin-tests.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victronenergy/dbus-flashmq/6c8bd684d31de5647d3aaec806b340ea852bdf66/src/flashmq-dbus-plugin-tests.h -------------------------------------------------------------------------------- /src/flashmq-dbus-plugin.cpp: -------------------------------------------------------------------------------- 1 | #include "vendor/flashmq_plugin.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "state.h" 12 | #include "utils.h" 13 | 14 | // https://dbus.freedesktop.org/doc/api/html/index.html 15 | 16 | using namespace dbus_flashmq; 17 | 18 | int flashmq_plugin_version() 19 | { 20 | return FLASHMQ_PLUGIN_VERSION; 21 | } 22 | 23 | void flashmq_plugin_allocate_thread_memory(void **thread_data, std::unordered_map &plugin_opts) 24 | { 25 | State *state = new State(); 26 | *thread_data = state; 27 | } 28 | 29 | void flashmq_plugin_deallocate_thread_memory(void *thread_data, std::unordered_map &plugin_opts) 30 | { 31 | State *state = static_cast(thread_data); 32 | delete state; 33 | } 34 | 35 | void flashmq_plugin_main_init(std::unordered_map &plugin_opts) 36 | { 37 | dbus_threads_init_default(); 38 | } 39 | 40 | void flashmq_plugin_init(void *thread_data, std::unordered_map &plugin_opts, bool reloading) 41 | { 42 | State *state = static_cast(thread_data); 43 | 44 | if (reloading) 45 | return; 46 | 47 | state->get_unique_id(); 48 | 49 | // Venus never skips it, but the Docker development env does. 50 | auto skip_broker_reg_pos = plugin_opts.find("skip_broker_registration"); 51 | if (skip_broker_reg_pos != plugin_opts.end() && skip_broker_reg_pos->second == "true") 52 | { 53 | state->do_online_registration = false; 54 | } 55 | 56 | state->initiate_broker_registration(0); 57 | 58 | state->open(); 59 | state->scan_all_dbus_services(); 60 | 61 | // Indicate that the new keepalive mechanism is supported 62 | std::ostringstream keepalive_topic; 63 | keepalive_topic << "N/" << state->unique_vrm_id << "/keepalive"; 64 | flashmq_publish_message(keepalive_topic.str(), 0, false, "1"); 65 | 66 | state->start_one_second_timer(); 67 | } 68 | 69 | void flashmq_plugin_deinit(void *thread_data, std::unordered_map &plugin_opts, bool reloading) 70 | { 71 | // As of yet, we don't do reload actions. 72 | if (reloading) 73 | return; 74 | 75 | if (!thread_data) 76 | return; 77 | 78 | State *state = static_cast(thread_data); 79 | 80 | /* 81 | * These are async calls and because we don't have an event loop anymore at this point, it may be that the 82 | * call is never sent, when dbus buffers are full for instance. It's a rare occurance though, and not 83 | * easily fixed. 84 | */ 85 | state->write_bridge_connection_state(BRIDGE_DBUS, std::optional(), BRIDGE_DEACTIVATED_STRING); 86 | state->write_bridge_connection_state(BRIDGE_RPC, std::optional(), BRIDGE_DEACTIVATED_STRING); 87 | } 88 | 89 | AuthResult auth_success_or_delayed_fail(const std::weak_ptr &client, const std::string &username, AuthResult result) 90 | { 91 | if (result == AuthResult::success) 92 | return AuthResult::success; 93 | 94 | auto f = [client, result]() { 95 | flashmq_continue_async_authentication(client, result, "", ""); 96 | }; 97 | const uint32_t delay = get_random() % 5000 + 1000; 98 | flashmq_add_task(f, delay); 99 | flashmq_logf(LOG_NOTICE, "Sending delayed deny for login with '%s'", username.c_str()); 100 | return AuthResult::async; 101 | } 102 | 103 | AuthResult do_vnc_auth(const std::string &password) 104 | { 105 | const static std::string vnc_password_file_path = "/data/conf/vncpassword.txt"; 106 | 107 | try 108 | { 109 | if (!std::filesystem::exists(vnc_password_file_path)) 110 | return AuthResult::login_denied; 111 | 112 | if (std::filesystem::file_size(vnc_password_file_path) == 0) 113 | return AuthResult::success; 114 | 115 | std::fstream vnc_password_file(vnc_password_file_path, std::ios::in); 116 | 117 | if (!vnc_password_file) 118 | { 119 | std::string error_str(strerror(errno)); 120 | throw std::runtime_error(error_str); 121 | } 122 | 123 | std::string vnc_password_crypt; 124 | 125 | if (!getline(vnc_password_file, vnc_password_crypt)) 126 | { 127 | std::string error_str(strerror(errno)); 128 | throw std::runtime_error(error_str); 129 | } 130 | 131 | trim(vnc_password_crypt); 132 | 133 | // The file is normally 0 bytes, but disabling it again makes it 1 byte, with a newline. This is also approved. 134 | if (vnc_password_crypt.empty()) 135 | return AuthResult::success; 136 | 137 | if (crypt_match(password, vnc_password_crypt)) 138 | return AuthResult::success; 139 | } 140 | catch (std::exception &ex) 141 | { 142 | flashmq_logf(LOG_ERR, "Error in do_vnc_auth using '%s': %s", vnc_password_file_path.c_str(), ex.what()); 143 | } 144 | 145 | return AuthResult::login_denied; 146 | } 147 | 148 | AuthResult flashmq_plugin_login_check( 149 | void *thread_data, const std::string &clientid, const std::string &username, const std::string &password, 150 | const std::vector> *userProperties, const std::weak_ptr &client) 151 | { 152 | State *state = static_cast(thread_data); 153 | 154 | FlashMQSockAddr addr; 155 | memset(&addr, 0, sizeof(FlashMQSockAddr)); 156 | flashmq_get_client_address(client, nullptr, &addr); 157 | 158 | if (state->match_local_net(addr.getAddr())) 159 | { 160 | return AuthResult::success; 161 | } 162 | 163 | if (do_vnc_auth(password) == AuthResult::success) 164 | { 165 | return AuthResult::success; 166 | } 167 | 168 | return auth_success_or_delayed_fail(client, username, AuthResult::login_denied); 169 | } 170 | 171 | bool flashmq_plugin_alter_publish(void *thread_data, const std::string &clientid, std::string &topic, const std::vector &subtopics, 172 | std::string_view payload, uint8_t &qos, bool &retain, const std::optional &correlationData, 173 | const std::optional &responseTopic, std::vector> *userProperties) 174 | { 175 | State *state = static_cast(thread_data); 176 | 177 | if (!state) 178 | return false; 179 | 180 | if (subtopics.size() < 2) 181 | return false; 182 | 183 | /* 184 | * This matches the 'publish' lines in /data/conf/flashmq.d/vrm_bridge.conf. It should/can also never happen 185 | * in practice on Venus. However, we're making sure that: 186 | * 187 | * 1) The internet MQTT servers are never given a retained message, even if we receive a stray retained message 188 | * from a local client that matches one of our own topic paths. 189 | * 2) We don't touch other topics that don't involve us, so the MQTT server on Venus can be used for non-Venus 190 | * things in a network. 191 | */ 192 | if (retain && (subtopics.at(0) == "N" || subtopics.at(0) == "P" ) && subtopics.at(1) == state->unique_vrm_id) 193 | { 194 | retain = false; 195 | return true; 196 | } 197 | 198 | return false; 199 | } 200 | 201 | void handle_venus_actions( 202 | State *state, const std::string &action, const std::string &system_id, const std::string clientid, 203 | const std::string &topic, const std::vector &subtopics, std::string_view payload) 204 | { 205 | // Wo only work on strings like R//system/0/Serial. 206 | if (action == "W" || action == "R") 207 | { 208 | /* 209 | * Because we also need to respond to reads and writes from remote clients when the system is not alive, we need 210 | * to consider any AclAccess::write activity as interest. 211 | */ 212 | if (client_id_is_bridge(clientid)) 213 | state->vrmBridgeInterestTime = std::chrono::steady_clock::now(); 214 | 215 | // There's also 'P' for mqtt-rpc, but we should ignore that, and not report it. 216 | if (action == "W") 217 | { 218 | std::string payload_str(payload); 219 | state->write_to_dbus(topic, payload_str); 220 | } 221 | else if (action == "R") 222 | { 223 | std::string payload_str(payload); 224 | const std::string path = splitToVector(topic, '/', 2).at(2); 225 | if (path == "system/0/Serial" || path == "keepalive") 226 | { 227 | state->handle_keepalive(payload_str); 228 | } 229 | 230 | if (path != "keepalive") 231 | { 232 | state->handle_read(topic); 233 | } 234 | } 235 | } 236 | else if (action == "$SYS" && subtopics.size() >= 5) 237 | { 238 | /* 239 | * Deal with bridge notifications like: 240 | * 241 | * * $SYS/broker/bridge/GXdbus/connected 242 | * * $SYS/broker/bridge/GXdbus/connection_status 243 | * 244 | * Disconnection is a good time to re-register ourselves on VRM, because there is a small chance our 245 | * registration has been reset by the user at some point in the past, which would only be seen when 246 | * making a new connection. 247 | */ 248 | 249 | const std::string &bridgeName = subtopics.at(3); 250 | const std::string &which = subtopics.at(4); 251 | const std::string payload_str(payload); 252 | 253 | if (bridgeName == BRIDGE_DBUS || bridgeName == BRIDGE_RPC) 254 | { 255 | if (which == "connected") 256 | { 257 | bool &connected = state->bridges_connected[bridgeName]; 258 | const bool connected_now = payload_str == "1"; 259 | 260 | if (connected_now) 261 | { 262 | connected = true; 263 | } 264 | else if (connected && !connected_now && state->register_pending_id == 0) 265 | { 266 | connected = false; 267 | 268 | /* 269 | * Note that for the majority of users, this re-registration is not needed and it will just reconnect. We need to do it 270 | * just in case (for installations that may have had their tokens reset), and we can allow some random wait time 271 | * to prevent DDOS on the registration server. 272 | */ 273 | const uint32_t delay = get_random() % 600000; 274 | 275 | flashmq_logf(LOG_NOTICE, "Bridge '%s' disconnect detected. We will initiate a re-registration in %d ms.", bridgeName.c_str(), delay); 276 | 277 | state->initiate_broker_registration(delay); 278 | } 279 | 280 | state->bridge_connection_states[bridgeName].connected = connected_now; 281 | } 282 | else if (which == "connection_status") 283 | { 284 | state->bridge_connection_states[bridgeName].msg = payload_str; 285 | } 286 | 287 | state->write_all_bridge_connection_states_debounced(); 288 | } 289 | } 290 | } 291 | 292 | /** 293 | * @brief using ACL hook as 'on_message' handler. 294 | */ 295 | AuthResult flashmq_plugin_acl_check(void *thread_data, const AclAccess access, const std::string &clientid, const std::string &username, 296 | const std::string &topic, const std::vector &subtopics, const std::string &shareName, 297 | std::string_view payload, const uint8_t qos, const bool retain, 298 | const std::optional &correlationData, const std::optional &responseTopic, 299 | const std::vector> *userProperties) 300 | { 301 | if (access == AclAccess::subscribe || access == AclAccess::register_will) 302 | return AuthResult::success; 303 | 304 | try 305 | { 306 | State *state = static_cast(thread_data); 307 | 308 | if (subtopics.size() < 2) 309 | return AuthResult::success; 310 | 311 | const std::string &action = subtopics.at(0); 312 | const std::string &system_id = subtopics.at(1); 313 | 314 | if (access == AclAccess::write) 315 | { 316 | if ((action == "W" || action == "R" || action == "P") && system_id != state->unique_vrm_id) 317 | { 318 | flashmq_logf(LOG_ERR, "We received a '%s' request for '%s', but that's not us (but %s)", 319 | action.c_str(), system_id.c_str(), state->unique_vrm_id.c_str()); 320 | 321 | /* 322 | * With W and R we are normally the one acting on that, so we can still relay them to any 323 | * subscribers. But for P, we are ensuring nobody gets those to avoid other Venus services 324 | * mistakingly acting on them. 325 | */ 326 | if (action == "W" || action == "R") 327 | return AuthResult::success; 328 | else 329 | return AuthResult::acl_denied; 330 | } 331 | 332 | /* 333 | * We also block P//in for safety; that is currently also covered by not having an RPC bridge 334 | * connection in read-only mode, but that may not be a separate connection in the future anymore. 335 | */ 336 | if (action == "W" || (action == "P" && subtopics.size() >= 3 && subtopics.at(2) == "in")) 337 | { 338 | if (client_id_is_bridge(clientid) && state->vrm_portal_mode != VrmPortalMode::Full) 339 | return AuthResult::acl_denied; 340 | } 341 | 342 | /* 343 | * Only allow ourselves to write N messages. This avoids people's own integrations from publishing 344 | * values they are not supposed to, which can be old, wrong, etc. 345 | */ 346 | if (action == "N" && !(clientid.empty() && username.empty()) && state->unique_vrm_id == system_id) 347 | { 348 | if (!state->warningAboutNTopicsLogged) 349 | { 350 | state->warningAboutNTopicsLogged = true; 351 | flashmq_logf(LOG_WARNING, 352 | "Received external publish on N topic: '%s'. " 353 | "This is unexpected and probably a misconfigured integration. Blocking this and later ones.", 354 | topic.c_str()); 355 | } 356 | 357 | return AuthResult::acl_denied; 358 | } 359 | 360 | // The rest is not auth as such, but take actions based on the messages. 361 | handle_venus_actions(state, action, system_id, clientid, topic, subtopics, payload); 362 | } 363 | else if (access == AclAccess::read) 364 | { 365 | // Just means other MQTT clients can't see it. Doesn't affect Venus Platform. 366 | if (action == "W" && subtopics.size() >= 3 && subtopics.at(2) == "platform" && topic.find("/Security/Api") != std::string::npos) 367 | { 368 | return AuthResult::acl_denied; 369 | } 370 | 371 | /* 372 | * The if-statements below stop traffic over the bridge if there is no VRM interest. However, we only 373 | * limit our own N (notifications), to avoid accidentally denying other things. 374 | */ 375 | if (action != "N") 376 | return AuthResult::success; 377 | 378 | // We still allow normal cross-client behavior when it's all on LAN. 379 | if (!client_id_is_bridge(clientid)) 380 | return AuthResult::success; 381 | 382 | if (std::chrono::steady_clock::now() > state->vrmBridgeInterestTime + std::chrono::seconds(VRM_INTEREST_TIMEOUT_SECONDS)) 383 | return AuthResult::acl_denied; 384 | 385 | return AuthResult::success; 386 | } 387 | } 388 | catch (std::exception &ex) 389 | { 390 | flashmq_logf(LOG_ERR, "Error in flashmq_plugin_acl_check when handling '%s': %s", topic.c_str(), ex.what()); 391 | } 392 | 393 | return AuthResult::success; 394 | } 395 | 396 | void flashmq_plugin_poll_event_received(void *thread_data, int fd, uint32_t events, const std::weak_ptr &p) 397 | { 398 | //flashmq_logf(LOG_DEBUG, "flashmq_plugin_poll_event_received. Epoll flags: %d", events); 399 | 400 | State *state = static_cast(thread_data); 401 | 402 | if (fd == state->dispatch_event_fd) 403 | { 404 | uint64_t eventfd_value = 0; 405 | if (read(fd, &eventfd_value, sizeof(uint64_t)) > 0) 406 | { 407 | DBusDispatchStatus dispatch_status = DBusDispatchStatus::DBUS_DISPATCH_DATA_REMAINS; 408 | while((dispatch_status = dbus_connection_get_dispatch_status(state->con)) == DBUS_DISPATCH_DATA_REMAINS) 409 | { 410 | dbus_connection_dispatch(state->con); 411 | } 412 | 413 | // This will make us spin, but it's a method that doesn't allocate memory. 414 | if (dispatch_status == DBusDispatchStatus::DBUS_DISPATCH_NEED_MEMORY) 415 | { 416 | uint64_t one = 1; 417 | write(state->dispatch_event_fd, &one, sizeof(uint64_t)); 418 | } 419 | } 420 | else 421 | { 422 | const char *err = strerror(errno); 423 | flashmq_logf(LOG_ERR, err); 424 | } 425 | 426 | return; 427 | } 428 | 429 | std::shared_ptr w = std::static_pointer_cast(p.lock()); 430 | 431 | if (!w || w->empty()) 432 | return; 433 | 434 | // Since the process is supervised, just exit if there are major problems like running 435 | // out of memory. 436 | 437 | /* 438 | * See the 'Watch' object class doc for the reasoning behind this. 439 | */ 440 | for (DBusWatch *watch : w->get_watches()) 441 | { 442 | if (!dbus_watch_get_enabled(watch)) 443 | continue; 444 | 445 | // Adding the implicit error flags to flags_of_watch. 446 | int flags_of_watch = dbus_watch_get_flags(watch) | (DBusWatchFlags::DBUS_WATCH_ERROR | DBusWatchFlags::DBUS_WATCH_HANGUP); 447 | int readiness_in_dbus_flags = epoll_flags_to_dbus_watch_flags(events); 448 | int match_flags = flags_of_watch & readiness_in_dbus_flags; 449 | 450 | if (!dbus_watch_handle(watch, match_flags)) 451 | { 452 | // If we don't exit here, the logs are spammed with errors and we likely won't recover. 453 | flashmq_logf(LOG_WARNING, "dbus_watch_handle() returns false, so is out of memory. Exiting, because there's nothing else to do."); 454 | 455 | // The FlashMQ log writer is an async thread and we have no control or info over its commit status. Because we do want to see stuff 456 | // in the log, sleeping a bit... 457 | std::this_thread::sleep_for(std::chrono::seconds(1)); 458 | 459 | // Not exit(), because we don't want to call destructors and stuff. 460 | abort(); 461 | } 462 | } 463 | } 464 | -------------------------------------------------------------------------------- /src/flashmqfunctionreplacements.cpp: -------------------------------------------------------------------------------- 1 | //#include "flashmqfunctionreplacements.h" 2 | 3 | #include "vendor/flashmq_plugin.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "testerglobals.h" 14 | 15 | using namespace dbus_flashmq; 16 | 17 | std::string getLogLevelString(int level) 18 | { 19 | switch (level) 20 | { 21 | case LOG_NONE: 22 | return "NONE"; 23 | case LOG_INFO: 24 | return "INFO"; 25 | case LOG_NOTICE: 26 | return "NOTICE"; 27 | case LOG_WARNING: 28 | return "WARNING"; 29 | case LOG_ERR: 30 | return "ERROR"; 31 | case LOG_DEBUG: 32 | return "DEBUG"; 33 | case LOG_SUBSCRIBE: 34 | return "SUBSCRIBE"; 35 | case LOG_UNSUBSCRIBE: 36 | return "UNSUBSCRIBE"; 37 | default: 38 | return "UNKNOWN LOG LEVEL"; 39 | } 40 | } 41 | 42 | /** 43 | * @brief flashmq_logf is normally provided by FlashMQ. We need to have it in our test env as well. 44 | * @param level 45 | * @param str 46 | */ 47 | void flashmq_logf(int level, const char *str, ...) 48 | { 49 | (void)level; 50 | 51 | time_t time = std::time(nullptr); 52 | struct tm tm = *std::localtime(&time); 53 | 54 | std::ostringstream oss; 55 | oss << "[" << std::put_time(&tm, "%Y-%m-%d %H:%M:%S") << "] [" << getLogLevelString(level) << "] "; 56 | oss.flush(); 57 | const std::string prefix = oss.str(); 58 | 59 | const int buf_size = 512; 60 | char buf[buf_size + 1]; 61 | buf[buf_size] = 0; 62 | 63 | va_list valist; 64 | va_start(valist, str); 65 | vsnprintf(buf, buf_size, str, valist); 66 | va_end(valist); 67 | 68 | const std::string formatted_line(buf); 69 | std::ostringstream oss2; 70 | oss2 << prefix << formatted_line; 71 | std::cout << "\033[01;33m" << oss2.str() << "\033[00m" << std::endl; 72 | } 73 | 74 | void flashmq_poll_add_fd(int fd, uint32_t events, const std::weak_ptr &p) 75 | { 76 | TesterGlobals *globals = TesterGlobals::getInstance(); 77 | globals->pollExternalFd(fd, events, p); 78 | } 79 | 80 | void flashmq_poll_remove_fd(uint32_t fd) 81 | { 82 | TesterGlobals *globals = TesterGlobals::getInstance(); 83 | globals->pollExternalRemove(fd); 84 | } 85 | 86 | uint32_t flashmq_add_task(std::function f, uint32_t delay_in_ms) 87 | { 88 | TesterGlobals *globals = TesterGlobals::getInstance(); 89 | return globals->delayedTasks.addTask(f, delay_in_ms); 90 | } 91 | 92 | void flashmq_remove_task(uint32_t id) 93 | { 94 | TesterGlobals *globals = TesterGlobals::getInstance(); 95 | 96 | globals->delayedTasks.eraseTask(id); 97 | } 98 | 99 | void flashmq_publish_message(const std::string &topic, const uint8_t qos, const bool retain, const std::string &payload, uint32_t expiryInterval, 100 | const std::vector> *userProperties, 101 | const std::string *responseTopic, const std::string *correlationData, const std::string *contentType) 102 | { 103 | std::cout << "DUMMY: " << topic << ": " << payload << std::endl; 104 | } 105 | 106 | /** 107 | * @brief flashmq_get_client_address is normally provided by FlashMQ, but for out test binary, we need to mock it, because we have no network clients. 108 | * @param client 109 | * @param text 110 | * @param addr 111 | */ 112 | void flashmq_get_client_address(const std::weak_ptr &client, std::string *text, FlashMQSockAddr *addr) 113 | { 114 | std::shared_ptr c = client.lock(); 115 | 116 | if (!c) 117 | return; 118 | 119 | if (text) 120 | *text = "dummy-we-dont-know"; 121 | 122 | if (addr) 123 | { 124 | struct sockaddr_in dummy_addr; 125 | memset(&dummy_addr, 0, sizeof(struct sockaddr_in)); 126 | 127 | inet_pton(AF_INET, "127.0.0.1", &dummy_addr.sin_addr); 128 | dummy_addr.sin_family = AF_INET; 129 | dummy_addr.sin_port = htons(666); 130 | 131 | memcpy(addr->getAddr(), &dummy_addr, addr->getLen()); 132 | } 133 | } 134 | 135 | sockaddr *FlashMQSockAddr::getAddr() 136 | { 137 | return reinterpret_cast(&this->addr_in6); 138 | } 139 | 140 | constexpr int FlashMQSockAddr::getLen() 141 | { 142 | return sizeof(struct sockaddr_in6); 143 | } 144 | 145 | void flashmq_continue_async_authentication(const std::weak_ptr &client, AuthResult result, const std::string &authMethod, const std::string &returnData) 146 | { 147 | 148 | } 149 | -------------------------------------------------------------------------------- /src/flashmqfunctionreplacements.h: -------------------------------------------------------------------------------- 1 | #ifndef FLASHMQFUNCTIONREPLACEMENTS_H 2 | #define FLASHMQFUNCTIONREPLACEMENTS_H 3 | 4 | #include 5 | #include 6 | 7 | #endif // FLASHMQFUNCTIONREPLACEMENTS_H 8 | -------------------------------------------------------------------------------- /src/network.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of FlashMQ (https://www.flashmq.org) 3 | Copyright (C) 2021-2023 Wiebe Cazemier 4 | 5 | Relicensed by copyright holder for Victron Energy BV, dbus-flashmq: 6 | 7 | MIT License 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | 27 | */ 28 | #include "network.h" 29 | 30 | #include 31 | #include 32 | #include 33 | 34 | #include "utils.h" 35 | 36 | using namespace dbus_flashmq; 37 | 38 | Network::Network(const std::string &network) 39 | { 40 | memset(&this->data, 0, sizeof (struct sockaddr_in6)); 41 | 42 | if (network.find(".") != std::string::npos) 43 | { 44 | struct sockaddr_in *_sockaddr_in = reinterpret_cast(&this->data); 45 | 46 | _sockaddr_in->sin_family = AF_INET; 47 | 48 | int maskbits = inet_net_pton(AF_INET, network.c_str(), &_sockaddr_in->sin_addr, sizeof(struct in_addr)); 49 | 50 | if (maskbits < 0) 51 | throw std::runtime_error("Network '" + network + "' is not a valid network notation."); 52 | 53 | uint32_t _netmask = (uint64_t)0xFFFFFFFFu << (32 - maskbits); 54 | this->in_mask = htonl(_netmask); 55 | } 56 | else if (network.find(":") != std::string::npos) 57 | { 58 | // Why does inet_net_pton not support AF_INET6...? 59 | 60 | struct sockaddr_in6 *_sockaddr_in6 = reinterpret_cast(&this->data); 61 | 62 | _sockaddr_in6->sin6_family = AF_INET6; 63 | 64 | std::vector parts = splitToVector(network, '/', 2, false); 65 | std::string &addrPart = parts[0]; 66 | int maskbits = 128; 67 | 68 | if (parts.size() == 2) 69 | { 70 | const std::string &maskstring = parts[1]; 71 | 72 | const bool invalid_chars = std::any_of(maskstring.begin(), maskstring.end(), [](const char &c) { return c < '0' || c > '9'; }); 73 | if (invalid_chars || maskstring.length() > 3) 74 | throw std::runtime_error("Mask '" + maskstring + "' is not valid"); 75 | 76 | maskbits = std::stoi(maskstring); 77 | } 78 | 79 | if (inet_pton(AF_INET6, addrPart.c_str(), &_sockaddr_in6->sin6_addr) != 1) 80 | { 81 | throw std::runtime_error("Network '" + network + "' is not a valid network notation."); 82 | } 83 | 84 | if (maskbits > 128 || maskbits < 0) 85 | { 86 | throw std::runtime_error("Network '" + network + "' is not a valid network notation."); 87 | } 88 | 89 | int m = maskbits; 90 | memset(in6_mask, 0, 16); 91 | int i = 0; 92 | const uint64_t x = 0xFFFFFFFF00000000; 93 | 94 | while (m >= 0) 95 | { 96 | int shift_remainder = std::min(m, 32); 97 | uint32_t b = x >> shift_remainder; 98 | in6_mask[i++] = htonl(b); 99 | m -= 32; 100 | } 101 | 102 | for (int i = 0; i < 4; i++) 103 | { 104 | network_addr_relevant_bits.__in6_u.__u6_addr32[i] = _sockaddr_in6->sin6_addr.__in6_u.__u6_addr32[i] & in6_mask[i]; 105 | } 106 | } 107 | else 108 | { 109 | throw std::runtime_error("Network '" + network + "' is not a valid network notation."); 110 | } 111 | } 112 | 113 | bool Network::match(const sockaddr *addr) const 114 | { 115 | const struct sockaddr* _sockaddr = reinterpret_cast(&this->data); 116 | 117 | if (_sockaddr->sa_family == AF_INET) 118 | { 119 | const struct sockaddr_in *_sockaddr_in = reinterpret_cast(&this->data); 120 | const struct sockaddr_in *_addr = reinterpret_cast(addr); 121 | return _sockaddr->sa_family == addr->sa_family && ((_sockaddr_in->sin_addr.s_addr & this->in_mask) == (_addr->sin_addr.s_addr & this->in_mask)); 122 | } 123 | else if (_sockaddr->sa_family == AF_INET6) 124 | { 125 | const struct sockaddr_in6 *arg_addr = reinterpret_cast(addr); 126 | 127 | struct in6_addr arg_addr_relevant_bits; 128 | for (int i = 0; i < 4; i++) 129 | { 130 | arg_addr_relevant_bits.__in6_u.__u6_addr32[i] = arg_addr->sin6_addr.__in6_u.__u6_addr32[i] & in6_mask[i]; 131 | } 132 | 133 | uint8_t matches[4]; 134 | for (int i = 0; i < 4; i++) 135 | { 136 | matches[i] = arg_addr_relevant_bits.__in6_u.__u6_addr32[i] == network_addr_relevant_bits.__in6_u.__u6_addr32[i]; 137 | } 138 | 139 | return (_sockaddr->sa_family == addr->sa_family) & (matches[0] & matches[1] & matches[2] & matches[3]); 140 | } 141 | 142 | return false; 143 | } 144 | 145 | bool Network::match(const sockaddr_in *addr) const 146 | { 147 | const struct sockaddr *_addr = reinterpret_cast(addr); 148 | return match(_addr); 149 | } 150 | 151 | bool Network::match(const sockaddr_in6 *addr) const 152 | { 153 | const struct sockaddr *_addr = reinterpret_cast(addr); 154 | return match(_addr); 155 | } 156 | -------------------------------------------------------------------------------- /src/network.h: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of FlashMQ (https://www.flashmq.org) 3 | Copyright (C) 2021-2023 Wiebe Cazemier 4 | 5 | Relicensed by copyright holder for Victron Energy BV, dbus-flashmq: 6 | 7 | MIT License 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | 27 | */ 28 | 29 | #ifndef NETWORK_H 30 | #define NETWORK_H 31 | 32 | #include 33 | #include 34 | #include 35 | 36 | namespace dbus_flashmq 37 | { 38 | 39 | /** 40 | * @brief The Network class is taken from FlashMQ main. 41 | */ 42 | class Network 43 | { 44 | sockaddr_in6 data; 45 | 46 | uint32_t in_mask = 0; 47 | 48 | uint32_t in6_mask[4]; 49 | struct in6_addr network_addr_relevant_bits; 50 | 51 | public: 52 | Network(const std::string &network); 53 | bool match(const struct sockaddr *addr) const ; 54 | bool match(const struct sockaddr_in *addr) const ; 55 | bool match(const struct sockaddr_in6 *addr) const; 56 | }; 57 | 58 | } 59 | 60 | #endif // NETWORK_H 61 | -------------------------------------------------------------------------------- /src/queuedtasks.cpp: -------------------------------------------------------------------------------- 1 | #include "queuedtasks.h" 2 | 3 | #include 4 | 5 | #include "vendor/flashmq_plugin.h" 6 | 7 | using namespace dbus_flashmq; 8 | 9 | bool QueuedTask::operator<(const QueuedTask &rhs) const 10 | { 11 | return this->when < rhs.when; 12 | } 13 | 14 | QueuedTasks::QueuedTasks() 15 | { 16 | 17 | } 18 | 19 | uint32_t QueuedTasks::addTask(std::function f, uint32_t delayInMs) 20 | { 21 | std::chrono::time_point when = std::chrono::steady_clock::now() + std::chrono::milliseconds(delayInMs); 22 | 23 | const uint32_t id = nextId++; 24 | tasks[id] = f; 25 | 26 | QueuedTask t; 27 | t.id = id; 28 | t.when = when; 29 | 30 | queuedTasks.insert(t); 31 | 32 | next = std::min(next, when); 33 | 34 | return id; 35 | } 36 | 37 | void QueuedTasks::eraseTask(uint32_t id) 38 | { 39 | tasks.erase(id); 40 | } 41 | 42 | uint32_t QueuedTasks::getTimeTillNext() const 43 | { 44 | if (__builtin_expect(tasks.empty(), 1)) 45 | return std::numeric_limits::max(); 46 | 47 | std::chrono::milliseconds x = std::chrono::duration_cast(next - std::chrono::steady_clock::now()); 48 | std::chrono::milliseconds y = std::max(std::chrono::milliseconds(0), x); 49 | return y.count(); 50 | } 51 | 52 | void QueuedTasks::performAll() 53 | { 54 | next = std::chrono::time_point::max(); 55 | const auto now = std::chrono::steady_clock::now(); 56 | 57 | std::list> copiedTasks; 58 | 59 | auto _pos = queuedTasks.begin(); 60 | while (_pos != queuedTasks.end()) 61 | { 62 | auto pos = _pos; 63 | _pos++; 64 | 65 | const QueuedTask &t = *pos; 66 | 67 | if (t.when > now) 68 | { 69 | next = t.when; 70 | break; 71 | } 72 | 73 | const uint32_t id = t.id; 74 | queuedTasks.erase(pos); 75 | 76 | auto tpos = tasks.find(id); 77 | if (tpos != tasks.end()) 78 | { 79 | auto f = tpos->second; 80 | tasks.erase(tpos); // TODO: allow repeatable tasks? It would require more expensive nextId generation. 81 | copiedTasks.push_back(f); 82 | } 83 | } 84 | 85 | for(auto &f : copiedTasks) 86 | { 87 | try 88 | { 89 | f(); 90 | } 91 | catch (std::exception &ex) 92 | { 93 | flashmq_logf(LOG_ERR, "Error in delayed task: %s", ex.what()); 94 | } 95 | } 96 | } 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/queuedtasks.h: -------------------------------------------------------------------------------- 1 | #ifndef QUEUEDTASKS_H 2 | #define QUEUEDTASKS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace dbus_flashmq 11 | { 12 | 13 | struct QueuedTask 14 | { 15 | std::chrono::time_point when; 16 | uint32_t id = 0; 17 | 18 | bool operator<(const QueuedTask &rhs) const; 19 | }; 20 | 21 | /** 22 | * @brief Contains delayed tasks to perform. 23 | * 24 | * This is only for the test env of the plugin. Normally all the tasks stuff is provided by FlashMQ. 25 | */ 26 | class QueuedTasks 27 | { 28 | uint32_t nextId = 1; 29 | std::multiset queuedTasks; 30 | std::unordered_map> tasks; 31 | std::chrono::time_point next = std::chrono::time_point::max(); 32 | 33 | public: 34 | QueuedTasks(); 35 | uint32_t addTask(std::function f, uint32_t delayInMs); 36 | void eraseTask(uint32_t id); 37 | uint32_t getTimeTillNext() const; 38 | void performAll(); 39 | }; 40 | 41 | } 42 | 43 | #endif // QUEUEDTASKS_H 44 | -------------------------------------------------------------------------------- /src/serviceidentifier.cpp: -------------------------------------------------------------------------------- 1 | #include "serviceidentifier.h" 2 | #include 3 | 4 | using namespace dbus_flashmq; 5 | 6 | ServiceIdentifier::ServiceIdentifier(int instance) : 7 | val(std::to_string(instance)) 8 | { 9 | 10 | } 11 | 12 | ServiceIdentifier::ServiceIdentifier(const std::string &identifier) : 13 | val(identifier) 14 | { 15 | 16 | } 17 | 18 | ServiceIdentifier::ServiceIdentifier() : 19 | val(std::to_string(0)) 20 | { 21 | 22 | } 23 | 24 | const std::string &ServiceIdentifier::getValue() const 25 | { 26 | return val; 27 | } 28 | 29 | ServiceIdentifier &ServiceIdentifier::operator=(const ServiceIdentifier &rhs) 30 | { 31 | assert(this != &rhs); 32 | this->val = rhs.val; 33 | return *this; 34 | } 35 | -------------------------------------------------------------------------------- /src/serviceidentifier.h: -------------------------------------------------------------------------------- 1 | #ifndef SERVICEIDENTIFIER_H 2 | #define SERVICEIDENTIFIER_H 3 | 4 | #include 5 | 6 | namespace dbus_flashmq 7 | { 8 | 9 | /** 10 | * @brief The ServiceIdentifier class was created to support the new 'identifier' for places where DeviceInstance doesn't make sense to 11 | * represent multiple occurances. 12 | */ 13 | class ServiceIdentifier 14 | { 15 | std::string val; 16 | public: 17 | ServiceIdentifier(int instance); 18 | ServiceIdentifier(const std::string &identifier); 19 | ServiceIdentifier(); 20 | const std::string &getValue() const; 21 | ServiceIdentifier &operator=(const ServiceIdentifier &rhs); 22 | }; 23 | 24 | } 25 | 26 | #endif // SERVICEIDENTIFIER_H 27 | -------------------------------------------------------------------------------- /src/shortservicename.cpp: -------------------------------------------------------------------------------- 1 | #include "shortservicename.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "exceptions.h" 7 | #include "utils.h" 8 | 9 | using namespace dbus_flashmq; 10 | 11 | std::string ShortServiceName::get_value(const std::string &service, ServiceIdentifier instance) 12 | { 13 | std::string short_name = make_short(service); 14 | std::ostringstream o; 15 | o << short_name << '/' << instance.getValue(); 16 | return std::string(o.str()); 17 | } 18 | 19 | std::string ShortServiceName::make_short(std::string service) 20 | { 21 | if (service.find("com.victronenergy.") != std::string::npos) 22 | { 23 | const std::vector parts = splitToVector(service, '.'); 24 | service = parts.at(2); 25 | } 26 | else if (service.find(".") != std::string::npos || service.find("/") != std::string::npos) 27 | { 28 | throw ValueError("String doesn't look like com.victronenergy.something or something without dots or slashes."); 29 | } 30 | 31 | return service; 32 | } 33 | 34 | ShortServiceName::ShortServiceName(const std::string &service, ServiceIdentifier instance) : 35 | std::string(get_value(service, instance)), 36 | service_type(make_short(service)) 37 | { 38 | 39 | } 40 | 41 | ShortServiceName::ShortServiceName() : std::string() 42 | { 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/shortservicename.h: -------------------------------------------------------------------------------- 1 | #ifndef SHORTSERVICENAME_H 2 | #define SHORTSERVICENAME_H 3 | 4 | #include 5 | #include 6 | #include "serviceidentifier.h" 7 | 8 | namespace dbus_flashmq 9 | { 10 | 11 | class ShortServiceName : public std::string 12 | { 13 | static std::string get_value(const std::string &service, ServiceIdentifier instance); 14 | static std::string make_short(std::string service); 15 | public: 16 | std::string service_type; 17 | ShortServiceName(const std::string &service, ServiceIdentifier instance); 18 | ShortServiceName(); 19 | }; 20 | 21 | } 22 | 23 | namespace std { 24 | 25 | template <> 26 | struct hash 27 | { 28 | std::size_t operator()(const dbus_flashmq::ShortServiceName& k) const 29 | { 30 | return std::hash()(k); 31 | } 32 | }; 33 | 34 | } 35 | 36 | 37 | #endif // SHORTSERVICENAME_H 38 | -------------------------------------------------------------------------------- /src/state.cpp: -------------------------------------------------------------------------------- 1 | #include "state.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "dbus_functions.h" 14 | #include "dbusutils.h" 15 | #include "vendor/flashmq_plugin.h" 16 | #include "dbusmessageguard.h" 17 | #include "utils.h" 18 | #include "types.h" 19 | #include "dbuserrorguard.h" 20 | #include "exceptions.h" 21 | #include "dbusmessageiteropencontainerguard.h" 22 | #include "dbuspendingmessagecallguard.h" 23 | #include "exceptions.h" 24 | 25 | using namespace dbus_flashmq; 26 | 27 | std::atomic_int State::instance_counter = 0; 28 | 29 | Watch::~Watch() 30 | { 31 | fd = -1; 32 | } 33 | 34 | State::State() 35 | { 36 | /* 37 | * FlashMQ's threading model is spreading clients over threads. We only have one, so threads only make things difficult, mainly with 38 | * dbus. Plus, everything is async, so we don't need threads. 39 | */ 40 | if (++instance_counter > 1) 41 | { 42 | throw std::runtime_error("Set thread_count to 1 in FlashMQ's config."); 43 | } 44 | 45 | local_nets.emplace_back("127.0.0.0/8"); 46 | local_nets.emplace_back("::1/128"); 47 | 48 | bridge_connection_states[BRIDGE_DBUS].msg = "pending"; 49 | bridge_connection_states[BRIDGE_RPC].msg = "pending"; 50 | 51 | dispatch_event_fd = eventfd(0, EFD_NONBLOCK); 52 | flashmq_poll_add_fd(dispatch_event_fd, EPOLLIN, std::weak_ptr()); 53 | } 54 | 55 | State::~State() 56 | { 57 | 58 | } 59 | 60 | void State::get_unique_id() 61 | { 62 | std::fstream file; 63 | 64 | file.open("/data/venus/unique-id", std::ios::in); 65 | getline(file, this->unique_vrm_id); 66 | trim(this->unique_vrm_id); 67 | 68 | if (this->unique_vrm_id.empty()) 69 | throw std::runtime_error("Failed to obtain unique VRM identifier."); 70 | } 71 | 72 | /** 73 | * @brief State::add_dbus_to_mqtt_mapping 74 | * @param service 75 | * @param items 76 | * @param instance_must_be_known When items is a set of items from a signal, the /DeviceInstance is not among them. But when the items 77 | * are from a call to GetValue on /, it is. Set this bool to make sure you don't make the wrong assumptions. 78 | */ 79 | void State::add_dbus_to_mqtt_mapping(const std::string &service, std::unordered_map &items, bool instance_must_be_known, bool force_publish) 80 | { 81 | if (instance_must_be_known) 82 | { 83 | auto pos = dbus_service_items.find(service); 84 | if (pos == dbus_service_items.end()) 85 | { 86 | /* 87 | * We may already get ItemsChanged when a service appears before we fully know the device instance and short name (like :1.33). 88 | * In those cases, we have to queue them up to process later. 89 | */ 90 | 91 | for (auto &p : items) 92 | { 93 | Item &i = p.second; 94 | 95 | i.set_partial_mapping_details(service); 96 | 97 | flashmq_logf(LOG_DEBUG, "Queueing changed values for '%s' '%s' with value '%s' until we fully know the service.", 98 | service.c_str(), i.get_path().c_str(), i.get_value().value.as_text().c_str()); 99 | 100 | delayed_changed_values.emplace_back(i); 101 | } 102 | 103 | return; 104 | } 105 | } 106 | 107 | ServiceIdentifier device_instance = store_and_get_instance_from_service(service, items, instance_must_be_known); 108 | 109 | ShortServiceName s(service, device_instance); 110 | this->service_type_and_instance_to_full_service[s] = service; 111 | 112 | for (auto &p : items) 113 | { 114 | Item &item = p.second; 115 | add_dbus_to_mqtt_mapping(service, device_instance, item, force_publish); 116 | } 117 | 118 | attempt_to_process_delayed_changes(); 119 | } 120 | 121 | /** 122 | * @brief Like '_add_item()' in the Python version. 123 | * @param service Like 'com.victronenergy.system'. 124 | * @param instance The instance number. 125 | * @param item. 126 | */ 127 | void State::add_dbus_to_mqtt_mapping(const std::string &service, ServiceIdentifier instance, Item &item, bool force_publish) 128 | { 129 | item.set_mapping_details(unique_vrm_id, service, instance); 130 | Item &fully_mapped_item = dbus_service_items[service][item.get_path()]; 131 | fully_mapped_item = item; 132 | 133 | if (fully_mapped_item.is_vrm_portal_mode()) 134 | { 135 | this->vrm_portal_mode = parseVrmPortalMode(fully_mapped_item.get_value().value.as_int()); 136 | this->write_all_bridge_connection_states_debounced(); 137 | } 138 | 139 | if (this->alive || fully_mapped_item.should_be_retained() || force_publish) 140 | fully_mapped_item.publish(); 141 | } 142 | 143 | /** 144 | * @brief State::find_item_by_mqtt_path get item by value based on topic. 145 | * @param topic 146 | */ 147 | const Item &State::find_item_by_mqtt_path(const std::string &topic) const 148 | { 149 | // Example topic: N/48e7da87942f/solarcharger/258/Link 150 | 151 | std::vector parts = splitToVector(topic, '/', 4); 152 | 153 | const std::string &vrm_id = parts.at(1); 154 | 155 | if (vrm_id != this->unique_vrm_id) 156 | throw std::runtime_error("Second subpath should match local VRM id. It doesn't."); 157 | 158 | const std::string &short_service = parts.at(2); 159 | const std::string &instance_str = parts.at(3); 160 | const std::string &dbus_like_path = "/" + parts.at(4); 161 | ServiceIdentifier instance(instance_str); 162 | ShortServiceName short_service_name(short_service, instance); 163 | 164 | auto pos_service = this->service_type_and_instance_to_full_service.find(short_service_name); 165 | 166 | if (pos_service == this->service_type_and_instance_to_full_service.end()) 167 | { 168 | throw std::runtime_error("Can't find dbus service for " + topic); 169 | } 170 | 171 | const std::string &full_service = pos_service->second; 172 | 173 | auto pos = dbus_service_items.find(full_service); 174 | if (pos == dbus_service_items.end()) 175 | { 176 | throw std::runtime_error("Can't find service for " + full_service); 177 | } 178 | 179 | const std::unordered_map &items = pos->second; 180 | 181 | auto pos_item = items.find(dbus_like_path); 182 | if (pos_item == items.end()) 183 | { 184 | throw ItemNotFound("Can't find item for " + dbus_like_path, full_service, dbus_like_path); 185 | } 186 | 187 | return pos_item->second; 188 | } 189 | 190 | Item &State::find_matching_active_item(const Item &item) 191 | { 192 | return find_by_service_and_dbus_path(item.get_service_name(), item.get_path()); 193 | } 194 | 195 | Item &State::find_by_service_and_dbus_path(const std::string &service, const std::string &dbus_path) 196 | { 197 | auto pos = dbus_service_items.find(service); 198 | 199 | if (pos == dbus_service_items.end()) 200 | throw std::runtime_error("Can't find service: " + service); 201 | 202 | std::unordered_map &items = pos->second; 203 | 204 | auto pos_item = items.find(dbus_path); 205 | 206 | if (pos_item == items.end()) 207 | throw std::runtime_error("Can't find item with path: " + dbus_path); 208 | 209 | return pos_item->second; 210 | } 211 | 212 | void State::attempt_to_process_delayed_changes() 213 | { 214 | if (this->delayed_changed_values.empty()) 215 | return; 216 | 217 | std::vector changed_values = std::move(this->delayed_changed_values); 218 | this->delayed_changed_values.clear(); 219 | 220 | for (QueuedChangedItem &i : changed_values) 221 | { 222 | if (i.age() > std::chrono::seconds(30)) 223 | { 224 | flashmq_logf(LOG_DEBUG, "Giving up on orphaned PropertiesChanged for '%s' '%s' with value '%s'.", 225 | i.item.get_service_name().c_str(), i.item.get_path().c_str(), i.item.get_value().value.as_text().c_str()); 226 | continue; 227 | } 228 | 229 | auto pos = dbus_service_items.find(i.item.get_service_name()); 230 | if (pos == dbus_service_items.end()) 231 | { 232 | delayed_changed_values.push_back(std::move(i)); 233 | continue; 234 | } 235 | 236 | flashmq_logf(LOG_DEBUG, "Sending queued changes for '%s' '%s' with value '%s'.", 237 | i.item.get_service_name().c_str(), i.item.get_path().c_str(), i.item.get_value().value.as_text().c_str()); 238 | 239 | Item &item = find_matching_active_item(i.item); 240 | item.set_value(i.item.get_value()); 241 | 242 | if (this->alive) 243 | item.publish(); 244 | } 245 | } 246 | 247 | void State::open() 248 | { 249 | DBusErrorGuard err; 250 | con = dbus_bus_get(DBusBusType::DBUS_BUS_SYSTEM, err.get()); 251 | err.throw_error(); 252 | 253 | dbus_connection_set_dispatch_status_function(con, dbus_dispatch_status_function, this, nullptr); 254 | dbus_connection_set_watch_functions(con, dbus_add_watch_function, dbus_remove_watch_function, dbus_toggle_watch_function, this, nullptr); 255 | dbus_connection_set_timeout_functions(con, dbus_add_timeout_function, dbus_remove_timeout_function, dbus_toggle_timeout_function, this, nullptr); 256 | dbus_connection_add_filter(con, dbus_handle_message, this, nullptr); 257 | 258 | dbus_bus_add_match(con, "type='signal',interface='com.victronenergy.BusItem'", nullptr); 259 | dbus_bus_add_match(con, "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged'", nullptr); 260 | 261 | this->setDispatchable(); 262 | } 263 | 264 | dbus_uint32_t State::call_method(const std::string &service, const std::string &path, const std::string &interface, const std::string &method, 265 | const std::vector &args, bool wrap_arguments_in_variant) 266 | { 267 | if (!dbus_validate_path(path.c_str(), nullptr)) 268 | { 269 | throw std::runtime_error("Path '" + path + "' is not valid for method call."); 270 | } 271 | 272 | DBusMessageGuard msg = dbus_message_new_method_call(service.c_str(), path.c_str(), interface.c_str(), method.c_str()); 273 | 274 | if (!msg.d) 275 | { 276 | throw std::runtime_error("No DBusMessage received from dbus_message_new_method_call. Out of memory?"); 277 | } 278 | 279 | DBusMessageIter iter; 280 | dbus_message_iter_init_append(msg.d, &iter); 281 | for (const VeVariant &arg : args) 282 | { 283 | if (wrap_arguments_in_variant) 284 | { 285 | DBusMessageIterOpenContainerGuard variant_iter(&iter, DBUS_TYPE_VARIANT, arg.get_dbus_type_as_string_recursive().c_str()); 286 | arg.append_args_to_dbus_message(variant_iter.get_array_iter()); 287 | } 288 | else 289 | { 290 | arg.append_args_to_dbus_message(&iter); 291 | } 292 | 293 | } 294 | 295 | DBusPendingMessageCallGuard pendingCall; 296 | dbus_bool_t send_reply_result = dbus_connection_send_with_reply(con, msg.d, &pendingCall.d, -1); 297 | 298 | if (!pendingCall.d || !send_reply_result) 299 | throw std::runtime_error("Tried method call but failed: DBusPendingCall is null or result was false."); 300 | 301 | if (!dbus_pending_call_set_notify(pendingCall.d, dbus_pending_call_notify, this, nullptr)) 302 | { 303 | throw std::runtime_error("dbus_pending_call_set_notify returned false."); 304 | } 305 | 306 | dbus_uint32_t serial = dbus_message_get_serial(msg.d); 307 | return serial; 308 | } 309 | 310 | void State::write_to_dbus(const std::string &topic, const std::string &payload) 311 | { 312 | flashmq_logf(LOG_DEBUG, "[Write] Writing '%s' to '%s'", payload.c_str(), topic.c_str()); 313 | 314 | const nlohmann::json j = nlohmann::json::parse(payload); 315 | 316 | auto jpos = j.find("value"); 317 | if (jpos == j.end()) 318 | throw ValueError("Can't find 'value' in json."); 319 | 320 | nlohmann::json::value_type json_value = *jpos; 321 | 322 | const Item &item = find_item_by_mqtt_path(topic); 323 | 324 | VeVariant new_value(json_value); 325 | 326 | flashmq_logf(LOG_DEBUG, "[Write] Determined dbus type of '%s' as '%s'", json_value.dump().c_str(), new_value.get_dbus_type_as_string_recursive().c_str()); 327 | 328 | std::vector args; 329 | args.push_back(new_value); 330 | dbus_uint32_t serial = call_method(item.get_service_name(), item.get_path(), "com.victronenergy.BusItem", "SetValue", args, true); 331 | 332 | auto set_value_handler = [](State *state, const std::string &topic, DBusMessage *msg) { 333 | const int msg_type = dbus_message_get_type(msg); 334 | 335 | if (msg_type == DBUS_MESSAGE_TYPE_ERROR) 336 | { 337 | std::string error = dbus_message_get_error_name_safe(msg); 338 | flashmq_logf(LOG_ERR, "Error on 'SetValue' on %s: %s", topic.c_str(), error.c_str()); 339 | return; 340 | } 341 | 342 | flashmq_logf(LOG_DEBUG, "SetValue on '%s' successful.", topic.c_str()); 343 | }; 344 | 345 | auto handler = std::bind(set_value_handler, this, topic, std::placeholders::_1); 346 | this->async_handlers[serial] = handler; 347 | } 348 | 349 | ServiceIdentifier State::store_and_get_instance_from_service(const std::string &service, const std::unordered_map &items, bool instance_must_be_known) 350 | { 351 | ServiceIdentifier device_instance; 352 | auto pos = this->service_names_to_instance.find(service); 353 | if (pos == this->service_names_to_instance.end()) 354 | { 355 | if (instance_must_be_known) 356 | throw std::runtime_error("Programming error: you're assuming we know the instance already."); 357 | 358 | device_instance = get_instance_from_items(items); 359 | this->service_names_to_instance[service] = device_instance; 360 | } 361 | else 362 | device_instance = pos->second; 363 | 364 | return device_instance; 365 | } 366 | 367 | /** 368 | * @brief Keeps the installation actively publishing changes to MQTT. 369 | * @param payload options. 370 | * 371 | * Options are like: 372 | * 373 | * { "keepalive-options" : [ "suppress-republish" ] } 374 | * { "keepalive-options" : [ {"full-publish-completed-echo": "B9FMlGWoCcfMKc" } ] } 375 | * 376 | * The payload was previsouly used for selecting only certain topics. We are probably not going to support that functionality. But 377 | * Note that that format was an array of topics, not a dict with keys. That kind of limited supporting other things with it. That's why 378 | * we're using a json object now, that you can give keys. 379 | * 380 | * Suppressing the publication of all topics can be done by those clients that understand we no longer use retained messages. By defaulting 381 | * to publishing all on getting a keep-alive, you can be sure you receive all topics, whether you are the first, second, third, watcher on 382 | * an already alive installation. 383 | * 384 | * The 'full-publish-completed-echo' can be used to tell identify the 'N//full_publish_completed' topic as yours. This is to 385 | * deal with multiple concurrent clients. It will come back like: 386 | * 387 | * N//full_publish_completed {"full-publish-completed-echo":"B9FMlGWoCcfMKc","value":1718860914} 388 | */ 389 | void State::handle_keepalive(const std::string &payload) 390 | { 391 | // Cheating: I don't actually need to parse the json. 392 | bool suppress_publish_of_all = payload.find("suppress-republish") != std::string::npos; 393 | 394 | // Rate limit keep-alives that cause republish. It's been seen in the field some installations get hundreds at once. 395 | if (!suppress_publish_of_all && this->keepAliveTokens-- > 0) 396 | { 397 | std::optional payload_echo; 398 | 399 | try 400 | { 401 | if (!payload.empty()) 402 | { 403 | nlohmann::json j = nlohmann::json::parse(payload); 404 | nlohmann::json options = j["keepalive-options"]; 405 | 406 | if (options.is_array()) 407 | { 408 | for (nlohmann::json &el : options) 409 | { 410 | if (el.is_object()) 411 | { 412 | payload_echo = el["full-publish-completed-echo"]; 413 | } 414 | } 415 | } 416 | } 417 | } 418 | catch (nlohmann::json::exception &ex) 419 | { 420 | flashmq_logf(LOG_DEBUG, "Failure parsing keepalive options: %s", ex.what()); 421 | } 422 | 423 | publish_all(payload_echo); 424 | } 425 | 426 | this->alive = true; 427 | flashmq_remove_task(this->keep_alive_reset_task_id); 428 | auto f = std::bind(&State::unset_keepalive, this); 429 | this->keep_alive_reset_task_id = flashmq_add_task(f, 60000); 430 | 431 | if (!heartbeat_task_id) 432 | heartbeat(); 433 | } 434 | 435 | void State::unset_keepalive() 436 | { 437 | this->alive = false; 438 | this->keep_alive_reset_task_id = 0; 439 | } 440 | 441 | void State::heartbeat() 442 | { 443 | if (!alive) 444 | { 445 | heartbeat_task_id = 0; 446 | return; 447 | } 448 | 449 | std::ostringstream heartbeat_topic; 450 | heartbeat_topic << "N/" << unique_vrm_id << "/heartbeat"; 451 | 452 | const int64_t unix_time = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); 453 | 454 | nlohmann::json j { {"value", unix_time} }; 455 | std::string payload = j.dump(); 456 | 457 | flashmq_publish_message(heartbeat_topic.str(), 0, false, payload); 458 | 459 | auto f = std::bind(&State::heartbeat, this); 460 | heartbeat_task_id = flashmq_add_task(f, 3000); 461 | } 462 | 463 | void State::publish_all(const std::optional &payload_echo) 464 | { 465 | for (auto &p : dbus_service_items) 466 | { 467 | for (auto &p2 : p.second) 468 | { 469 | Item &i = p2.second; 470 | i.publish(); 471 | } 472 | } 473 | 474 | std::ostringstream done_topic; 475 | done_topic << "N/" << unique_vrm_id << "/full_publish_completed"; 476 | 477 | const int64_t unix_time = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); 478 | 479 | nlohmann::json j { {"value", unix_time } }; 480 | 481 | if (payload_echo) 482 | { 483 | j["full-publish-completed-echo"] = payload_echo.value(); 484 | } 485 | 486 | std::string payload = j.dump(); 487 | 488 | flashmq_publish_message(done_topic.str(), 0, false, payload); 489 | } 490 | 491 | /** 492 | * @brief State::set_new_id_to_owner 493 | * @param owner Like 1:31. 494 | * @param name Like com.victronenergy.vecan.can0 495 | */ 496 | void State::set_new_id_to_owner(const std::string &owner, const std::string &name) 497 | { 498 | assert(owner.find(":") != std::string::npos); 499 | this->service_id_to_names[owner] = name; 500 | } 501 | 502 | /** 503 | * @brief State::get_named_owner 504 | * @param sender Like 1:31 505 | * @return 506 | */ 507 | std::string State::get_named_owner(std::string sender) const 508 | { 509 | if (sender.find("com.victronenergy") == std::string::npos) 510 | { 511 | auto pos = service_id_to_names.find(sender); 512 | if (pos != service_id_to_names.end()) 513 | sender = pos->second; 514 | } 515 | 516 | return sender; 517 | } 518 | 519 | /** 520 | * @brief State::remove_id_to_owner 521 | * @param owner Like 1:31 522 | */ 523 | void State::remove_id_to_owner(const std::string &owner) 524 | { 525 | assert(owner.find(":") != std::string::npos); 526 | service_id_to_names.erase(owner); 527 | } 528 | 529 | /** 530 | * @brief State::handle_read 531 | * @param topic like 'R/48e7da87942f/system/0/Ac/Grid/L2/Power' 532 | * 533 | * Read a fresh value and make sure item is added. This is because a path may not always send 534 | * PropertiesChanged (eg /vebus/Hub4/L1/AcPowerSetpoint) but can nevertheless be read. 535 | */ 536 | void State::handle_read(const std::string &topic) 537 | { 538 | try 539 | { 540 | const Item &item = find_item_by_mqtt_path(topic); 541 | dbus_uint32_t serial = call_method(item.get_service_name(), item.get_path(), "com.victronenergy.BusItem", "GetValue"); 542 | 543 | auto get_value_handler = [](State *state, const Item &item, DBusMessage *msg) { 544 | const int msg_type = dbus_message_get_type(msg); 545 | 546 | if (msg_type == DBUS_MESSAGE_TYPE_ERROR) 547 | { 548 | std::string error = dbus_message_get_error_name_safe(msg); 549 | flashmq_logf(LOG_ERR, "Error on 'GetValue' from %s: %s", item.get_path().c_str(), error.c_str()); 550 | return; 551 | } 552 | 553 | DBusMessageIter iter; 554 | dbus_message_iter_init(msg, &iter); 555 | VeVariant answer(&iter); 556 | 557 | ValueMinMax val; 558 | val.value = std::move(answer); 559 | 560 | Item &real_item = state->find_matching_active_item(item); 561 | real_item.set_value(val); 562 | real_item.publish(); 563 | }; 564 | 565 | auto handler = std::bind(get_value_handler, this, item, std::placeholders::_1); 566 | this->async_handlers[serial] = handler; 567 | } 568 | catch (ItemNotFound &info) 569 | { 570 | get_value(info.service, info.dbus_like_path, true); 571 | } 572 | } 573 | 574 | /** 575 | * @brief State::initiate_broker_registration calls a dbus method to have Venus Platform call mosquitto_bridge_registrator.py. Contrary to 576 | * the previous dbus-mqtt, we are not root anymore, so we can't do it directly. 577 | * @param delay 578 | */ 579 | void State::initiate_broker_registration(uint32_t delay) 580 | { 581 | if (!do_online_registration) 582 | return; 583 | 584 | if (register_pending_id > 0) 585 | { 586 | flashmq_logf(LOG_WARNING, "Trying to register at VRM while a dbus method request for it is still pending."); 587 | return; 588 | } 589 | 590 | auto register_at_vrm_handler = [](State *state, DBusMessage *msg) { 591 | state->register_pending_id = 0; 592 | 593 | const int msg_type = dbus_message_get_type(msg); 594 | 595 | if (msg_type == DBUS_MESSAGE_TYPE_ERROR) 596 | { 597 | std::string error = dbus_message_get_error_name_safe(msg); 598 | flashmq_logf(LOG_ERR, "Error on SetValue on /Mqtt/RegisterOnVrm: %s", error.c_str()); 599 | return; 600 | } 601 | 602 | flashmq_logf(LOG_NOTICE, "SetValue on /Mqtt/RegisterOnVrm seemingly successful."); 603 | }; 604 | 605 | auto register_f = [register_at_vrm_handler](State *state) { 606 | flashmq_logf(LOG_NOTICE, "Initiating bridge registration."); 607 | 608 | dbus_uint32_t serial = state->call_method("com.victronenergy.platform", "/Mqtt/RegisterOnVrm", "com.victronenergy.platform", "SetValue", {{"1"}}, true); 609 | 610 | auto handler_f = std::bind(register_at_vrm_handler, state, std::placeholders::_1); 611 | state->async_handlers[serial] = handler_f; 612 | }; 613 | 614 | auto f = std::bind(register_f, this); 615 | register_pending_id = flashmq_add_task(f, delay); 616 | } 617 | 618 | void State::per_second_action() 619 | { 620 | this->period_task_id = 0; 621 | start_one_second_timer(); 622 | 623 | this->keepAliveTokens = KEEPALIVE_TOKENS; 624 | } 625 | 626 | void State::start_one_second_timer() 627 | { 628 | if (period_task_id) 629 | return; 630 | 631 | auto f = std::bind(&State::per_second_action, this); 632 | this->period_task_id = flashmq_add_task(f, ONE_SECOND_TIMER_INTERVAL); 633 | } 634 | 635 | bool State::match_local_net(const sockaddr *addr) const 636 | { 637 | return std::any_of(local_nets.begin(), local_nets.end(), [addr](const Network &net){ return net.match(addr);}); 638 | } 639 | 640 | void State::write_bridge_connection_state(const std::string &bridge, const std::optional connected, const std::string &msg) 641 | { 642 | auto answer_handler = [](State *state, const std::string &path, DBusMessage *msg) { 643 | const int msg_type = dbus_message_get_type(msg); 644 | 645 | if (msg_type == DBUS_MESSAGE_TYPE_ERROR) 646 | { 647 | std::string error = dbus_message_get_error_name_safe(msg); 648 | flashmq_logf(LOG_ERR, "Error on SetValue on '%s': %s", path.c_str(), error.c_str()); 649 | return; 650 | } 651 | }; 652 | 653 | const std::string bool_val_for_log = connected.has_value() ? std::to_string(connected.value()) : "null"; 654 | flashmq_logf(LOG_NOTICE, "Setting bridge connection status of %s to %s (%s).", 655 | bridge.c_str(), bool_val_for_log.c_str(), msg.c_str()); 656 | 657 | { 658 | VeVariant bool_variant(connected); 659 | 660 | const std::string path = "/Mqtt/Bridges/" + bridge + "/Connected"; 661 | const dbus_uint32_t serial = call_method( 662 | "com.victronenergy.platform", 663 | path, 664 | "com.victronenergy.platform", 665 | "SetValue", {bool_variant}, true); 666 | 667 | auto handler = std::bind(answer_handler, this, path, std::placeholders::_1); 668 | this->async_handlers[serial] = handler; 669 | } 670 | 671 | { 672 | VeVariant msg_variant(msg); 673 | 674 | const std::string path = "/Mqtt/Bridges/" + bridge + "/ConnectionStatus"; 675 | const dbus_uint32_t serial = call_method( 676 | "com.victronenergy.platform", 677 | path, 678 | "com.victronenergy.platform", 679 | "SetValue", {msg_variant}, true); 680 | 681 | auto handler = std::bind(answer_handler, this, path, std::placeholders::_1); 682 | this->async_handlers[serial] = handler; 683 | } 684 | } 685 | 686 | void State::write_all_bridge_connection_states_debounced() 687 | { 688 | if (write_all_bridge_states_task_id != 0) 689 | { 690 | flashmq_remove_task(write_all_bridge_states_task_id); 691 | write_all_bridge_states_task_id = 0; 692 | } 693 | 694 | if (bridge_connection_states_last_written == bridge_connection_states) 695 | return; 696 | 697 | auto f = [this] () { 698 | write_all_bridge_states_task_id = 0; 699 | 700 | /* 701 | * Unfortunately we don't have a place to read the bridge connection intention from, so we have to deduce it. 702 | */ 703 | 704 | { 705 | BridgeConnectionState &b = this->bridge_connection_states[BRIDGE_DBUS]; 706 | 707 | std::optional connected = b.connected; 708 | std::string msg = b.msg; 709 | 710 | // TODO: when FlashMQ ultimately uses -1 as connection status for a bridge that disappeared, use that. 711 | if (b.msg.find("disappeared from config") != std::string::npos || this->vrm_portal_mode < VrmPortalMode::ReadOnly) 712 | { 713 | connected.reset(); 714 | msg = BRIDGE_DEACTIVATED_STRING; 715 | } 716 | 717 | write_bridge_connection_state(BRIDGE_DBUS, connected, msg); 718 | } 719 | 720 | { 721 | BridgeConnectionState &b = this->bridge_connection_states[BRIDGE_RPC]; 722 | 723 | std::optional connected = b.connected; 724 | std::string msg = b.msg; 725 | 726 | // TODO: when FlashMQ ultimately uses -1 as connection status for a bridge that disappeared, use that. 727 | if (b.msg.find("disappeared from config") != std::string::npos || this->vrm_portal_mode < VrmPortalMode::Full) 728 | { 729 | connected.reset(); 730 | msg = BRIDGE_DEACTIVATED_STRING; 731 | } 732 | 733 | write_bridge_connection_state(BRIDGE_RPC, connected, msg); 734 | } 735 | 736 | bridge_connection_states_last_written = bridge_connection_states; 737 | 738 | }; 739 | 740 | write_all_bridge_states_task_id = flashmq_add_task(f, 2000); 741 | } 742 | 743 | void State::scan_all_dbus_services() 744 | { 745 | auto list_names_handler = [](State *state, DBusMessage *msg) { 746 | const std::vector services = get_array_from_reply(msg); 747 | 748 | for(const std::string &service : services) 749 | { 750 | if (service.find("com.victronenergy") == std::string::npos) 751 | continue; 752 | 753 | state->scan_dbus_service(service); 754 | } 755 | }; 756 | 757 | dbus_uint32_t serial = call_method("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames"); 758 | auto bla = std::bind(list_names_handler, this, std::placeholders::_1); 759 | async_handlers[serial] = bla; 760 | } 761 | 762 | void State::get_value(const std::string &service, const std::string &path, bool force_publish) 763 | { 764 | auto get_value_handler = [](State *state, const std::string &service, const std::string &path_prefix, bool force_publish, DBusMessage *msg) { 765 | const int msg_type = dbus_message_get_type(msg); 766 | 767 | if (msg_type == DBUS_MESSAGE_TYPE_ERROR) 768 | { 769 | std::string error = dbus_message_get_error_name_safe(msg); 770 | flashmq_logf(LOG_ERR, "Error on 'GetValue' from %s: %s", service.c_str(), error.c_str()); 771 | return; 772 | } 773 | 774 | std::unordered_map items = get_from_get_value_on_root(msg, path_prefix); 775 | state->add_dbus_to_mqtt_mapping(service, items, false, force_publish); 776 | }; 777 | 778 | dbus_uint32_t serial = this->call_method(service, path, "com.victronenergy.BusItem", "GetValue"); 779 | auto handler = std::bind(get_value_handler, this, service, path, force_publish, std::placeholders::_1); 780 | this->async_handlers[serial] = handler; 781 | } 782 | 783 | void State::scan_dbus_service(const std::string &service) 784 | { 785 | auto get_items_handler = [](State *state, const std::string &service, DBusMessage *msg) { 786 | const int msg_type = dbus_message_get_type(msg); 787 | 788 | if (msg_type == DBUS_MESSAGE_TYPE_ERROR) 789 | { 790 | std::string error = dbus_message_get_error_name_safe(msg); 791 | 792 | if (error == "org.freedesktop.DBus.Error.UnknownMethod") 793 | { 794 | /* 795 | * The current preferred way of getting values is GetItems, which uses async IO in Python. But, but not 796 | * all services support that. So, if we error here with (org.freedesktop.DBus.Error.UnknownObject 797 | * or) org.freedesktop.DBus.Error.UnknownMethod, we have to use the traditional GetValue on /. 798 | */ 799 | 800 | // TODO: and if this fails, introspect it? For now, we decided to not do this. QWACS is the only thing so far that seems to need it. 801 | 802 | state->get_value(service, "/"); 803 | return; 804 | } 805 | 806 | flashmq_logf(LOG_ERR, "Error on 'GetItems' from %s: %s", service.c_str(), error.c_str()); 807 | return; 808 | } 809 | 810 | std::unordered_map items = get_from_dict_with_dict_with_text_and_value(msg); 811 | state->add_dbus_to_mqtt_mapping(service, items, false); 812 | }; 813 | 814 | auto get_name_owner_handler = [get_items_handler](State *state, const std::string &service, DBusMessage *msg) { 815 | const int msg_type = dbus_message_get_type(msg); 816 | if (msg_type == DBUS_MESSAGE_TYPE_ERROR) 817 | { 818 | std::string error = dbus_message_get_error_name_safe(msg); 819 | flashmq_logf(LOG_ERR, error.c_str()); 820 | return; 821 | } 822 | 823 | const std::string name_owner = get_string_from_reply(msg); 824 | state->service_id_to_names[name_owner] = service; 825 | 826 | dbus_uint32_t serial = state->call_method(service, "/", "com.victronenergy.BusItem", "GetItems"); 827 | auto handler = std::bind(get_items_handler, state, service, std::placeholders::_1); 828 | state->async_handlers[serial] = handler; 829 | }; 830 | 831 | // We have to know the :1.66 like name for com.victronenergy.system and such, because in signals, we only have :1.66 as sender. 832 | std::vector args {service}; 833 | dbus_uint32_t s = call_method("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "GetNameOwner", args); 834 | auto handler = std::bind(get_name_owner_handler, this, service, std::placeholders::_1); 835 | this->async_handlers[s] = handler; 836 | } 837 | 838 | void State::remove_dbus_service(const std::string &service) 839 | { 840 | { 841 | auto pos = dbus_service_items.find(service); 842 | if (pos != dbus_service_items.end()) 843 | { 844 | std::unordered_map &items = pos->second; 845 | 846 | for (auto &p : items) 847 | { 848 | Item &item = p.second; 849 | item.publish(true); 850 | } 851 | } 852 | } 853 | 854 | dbus_service_items.erase(service); 855 | service_names_to_instance.erase(service); 856 | 857 | { 858 | // Looping over values because it's the best way to guarantee we find it. 859 | auto pos = service_type_and_instance_to_full_service.begin(); 860 | while (pos != service_type_and_instance_to_full_service.end()) 861 | { 862 | auto pos_use = pos++; 863 | if (pos_use->second == service) 864 | { 865 | service_type_and_instance_to_full_service.erase(pos_use); 866 | break; 867 | } 868 | } 869 | } 870 | 871 | { 872 | // This shouldn't be necessry because we did it already, but just making sure. 873 | // Looping over values because it's the best way to guarantee we find it. 874 | auto pos = service_id_to_names.begin(); 875 | while (pos != service_id_to_names.end()) 876 | { 877 | auto pos_use = pos++; 878 | if (pos_use->second == service) 879 | { 880 | service_id_to_names.erase(pos_use); 881 | break; 882 | } 883 | } 884 | } 885 | } 886 | 887 | void State::setDispatchable() 888 | { 889 | uint64_t one = 1; 890 | if (write(dispatch_event_fd, &one, sizeof(uint64_t)) < 0) 891 | { 892 | const char *err = strerror(errno); 893 | flashmq_logf(LOG_ERR, err); 894 | } 895 | } 896 | 897 | void Watch::add_watch(DBusWatch *watch) 898 | { 899 | if (std::find(watches.begin(), watches.end(), watch) != watches.end()) 900 | return; 901 | 902 | watches.push_back(watch); 903 | } 904 | 905 | void Watch::remove_watch(DBusWatch *watch) 906 | { 907 | auto pos = std::find(watches.begin(), watches.end(), watch); 908 | 909 | if (pos != watches.end()) 910 | watches.erase(pos); 911 | } 912 | 913 | const std::vector &Watch::get_watches() const 914 | { 915 | return watches; 916 | } 917 | 918 | int Watch::get_combined_epoll_flags() 919 | { 920 | int result = 0; 921 | 922 | for (DBusWatch *watch : watches) 923 | { 924 | if (!dbus_watch_get_enabled(watch)) 925 | continue; 926 | 927 | int dbus_flags = dbus_watch_get_flags(watch); 928 | int epoll_flags = dbus_watch_flags_to_epoll(dbus_flags); 929 | result |= epoll_flags; 930 | } 931 | 932 | return result; 933 | } 934 | 935 | bool Watch::empty() const 936 | { 937 | return watches.empty(); 938 | } 939 | 940 | QueuedChangedItem::QueuedChangedItem(const Item &item) : 941 | item(item) 942 | { 943 | 944 | } 945 | 946 | std::chrono::seconds QueuedChangedItem::age() const 947 | { 948 | auto now = std::chrono::steady_clock::now(); 949 | auto c = now - created_at; 950 | auto duration = std::chrono::duration_cast(c); 951 | return duration; 952 | } 953 | 954 | bool BridgeConnectionState::operator==(const BridgeConnectionState &other) const 955 | { 956 | return msg == other.msg && connected == other.connected; 957 | } 958 | -------------------------------------------------------------------------------- /src/state.h: -------------------------------------------------------------------------------- 1 | #ifndef STATE_H 2 | #define STATE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "types.h" 12 | #include 13 | #include 14 | #include "serviceidentifier.h" 15 | #include "network.h" 16 | 17 | #define VRM_INTEREST_TIMEOUT_SECONDS 130 18 | #define KEEPALIVE_TOKENS 3 19 | #define ONE_SECOND_TIMER_INTERVAL 1000 20 | 21 | namespace dbus_flashmq 22 | { 23 | 24 | /** 25 | * @brief The Watch class is not an owner of the watches. DBus itself is. 26 | * 27 | * From a dbus dev: 28 | * 29 | * The short version is that libdbus does not guarantee that it will only 30 | * have one watch per fd, so lower-level code needs to be prepared to: 31 | * watch the fd for the union of all the flags of multiple watches; when 32 | * it is reported as readable, trigger the callbacks of all read-only or 33 | * rw watches; and when it is reported as writable, trigger the callbacks 34 | * of all write-only or rw watches. 35 | */ 36 | class Watch 37 | { 38 | 39 | std::vector watches; 40 | public: 41 | 42 | int fd = -1; 43 | 44 | ~Watch(); 45 | 46 | void add_watch(DBusWatch *watch); 47 | void remove_watch(DBusWatch *watch); 48 | const std::vector &get_watches() const; 49 | 50 | int get_combined_epoll_flags(); 51 | bool empty() const; 52 | 53 | }; 54 | 55 | struct QueuedChangedItem 56 | { 57 | Item item; 58 | std::chrono::time_point created_at = std::chrono::steady_clock::now(); 59 | 60 | QueuedChangedItem(const Item &item); 61 | std::chrono::seconds age() const; 62 | }; 63 | 64 | struct BridgeConnectionState 65 | { 66 | bool connected = false; 67 | std::string msg; 68 | 69 | bool operator==(const BridgeConnectionState &other) const; 70 | }; 71 | 72 | struct State 73 | { 74 | uint32_t register_pending_id = 0; 75 | std::map bridges_connected; 76 | std::unordered_map bridge_connection_states; 77 | std::unordered_map bridge_connection_states_last_written; 78 | bool do_online_registration = true; 79 | 80 | static std::atomic_int instance_counter; 81 | std::string unique_vrm_id; 82 | 83 | VrmPortalMode vrm_portal_mode = VrmPortalMode::Unknown; 84 | 85 | /* 86 | * TODO: Maybe implement the selective keep-alive mechanism like dbus-mqtt had. 87 | * 88 | * We're putting that on hold for now though. Apparently it back-fired, in terms of load. 89 | */ 90 | bool alive = false; 91 | 92 | uint32_t keep_alive_reset_task_id = 0; 93 | uint32_t heartbeat_task_id = 0; 94 | uint32_t period_task_id = 0; 95 | uint32_t write_all_bridge_states_task_id = 0; 96 | 97 | int dispatch_event_fd = -1; 98 | DBusConnection *con = nullptr; 99 | std::unordered_map> async_handlers; 100 | std::unordered_map> watches; 101 | std::unordered_map service_id_to_names; // like 1:31 to com.victronenergy.settings 102 | std::unordered_map service_type_and_instance_to_full_service; // like 'solarcharger/258' to 'com.victronenergy.solarcharger.ttyO2' 103 | std::unordered_map service_names_to_instance; // like 'com.victronenergy.solarcharger.ttyO2' to 258 104 | std::unordered_map> dbus_service_items; // keyed by service, then by dbus path, without instance. 105 | std::vector delayed_changed_values; 106 | std::chrono::time_point vrmBridgeInterestTime; 107 | int keepAliveTokens = KEEPALIVE_TOKENS; 108 | bool warningAboutNTopicsLogged = false; 109 | 110 | std::vector local_nets; 111 | 112 | State(); 113 | ~State(); 114 | void add_dbus_to_mqtt_mapping(const std::string &serivce, std::unordered_map &items, bool instance_must_be_known, bool force_publish=false); 115 | void add_dbus_to_mqtt_mapping(const std::string &service, ServiceIdentifier instance, Item &item, bool force_publish); 116 | const Item &find_item_by_mqtt_path(const std::string &topic) const; 117 | Item &find_matching_active_item(const Item &item); 118 | Item &find_by_service_and_dbus_path(const std::string &service, const std::string &dbus_path); 119 | void attempt_to_process_delayed_changes(); 120 | void get_unique_id(); 121 | void open(); 122 | void scan_all_dbus_services(); 123 | void get_value(const std::string &service, const std::string &path, bool force_publish=false); 124 | void scan_dbus_service(const std::string &service); 125 | void remove_dbus_service(const std::string &service); 126 | void setDispatchable(); 127 | dbus_uint32_t call_method(const std::string &service, const std::string &path, const std::string &interface, const std::string &method, 128 | const std::vector &args = std::vector(), bool wrap_arguments_in_variant=false); 129 | void write_to_dbus(const std::string &topic, const std::string &payload); 130 | ServiceIdentifier store_and_get_instance_from_service(const std::string &service, const std::unordered_map &items, bool instance_must_be_known); 131 | void handle_keepalive(const std::string &payload); 132 | void unset_keepalive(); 133 | void heartbeat(); 134 | void publish_all(const std::optional &payload_echo); 135 | void set_new_id_to_owner(const std::string &owner, const std::string &name); 136 | std::string get_named_owner(std::string sender) const; 137 | void remove_id_to_owner(const std::string &owner); 138 | void handle_read(const std::string &topic); 139 | void initiate_broker_registration(uint32_t delay); 140 | void per_second_action(); 141 | void start_one_second_timer(); 142 | bool match_local_net(const struct sockaddr *addr) const; 143 | void write_bridge_connection_state(const std::string &bridge, const std::optional connected, const std::string &msg); 144 | void write_all_bridge_connection_states_debounced(); 145 | }; 146 | 147 | } 148 | 149 | #endif // STATE_H 150 | -------------------------------------------------------------------------------- /src/testerglobals.cpp: -------------------------------------------------------------------------------- 1 | #include "testerglobals.h" 2 | #include "sys/epoll.h" 3 | #include 4 | #include "vendor/flashmq_plugin.h" 5 | 6 | int testCount; 7 | int failCount; 8 | 9 | bool fmq_assert(bool b, const char *failmsg, const char *actual, const char *expected, const char *file, int line) 10 | { 11 | testCount++; 12 | 13 | if (!b) 14 | { 15 | failCount++; 16 | printf("\033[01;31mFAIL\033[00m: '%s', %s != %s" 17 | "\n in %s, line %d\n", failmsg, actual, expected, file, line); 18 | } 19 | else 20 | { 21 | printf("\033[01;32mSUCCESS\033[00m: %s == %s\n", actual, expected); 22 | } 23 | 24 | return b; 25 | } 26 | 27 | using namespace dbus_flashmq; 28 | 29 | TesterGlobals::TesterGlobals() 30 | { 31 | 32 | } 33 | 34 | TesterGlobals *TesterGlobals::getInstance() 35 | { 36 | static TesterGlobals *instance = new TesterGlobals; 37 | return instance; 38 | } 39 | 40 | void TesterGlobals::pollExternalFd(int fd, uint32_t events, const std::weak_ptr &p) 41 | { 42 | int mode = EPOLL_CTL_MOD; 43 | auto pos = watchedFds.find(fd); 44 | if (pos == watchedFds.end()) 45 | { 46 | mode = EPOLL_CTL_ADD; 47 | } 48 | 49 | if (mode == EPOLL_CTL_ADD || !p.expired()) 50 | watchedFds[fd] = p; 51 | 52 | struct epoll_event ev; 53 | memset(&ev, 0, sizeof (struct epoll_event)); 54 | ev.data.fd = fd; 55 | ev.events = events; 56 | if (epoll_ctl(this->epoll_fd, mode, fd, &ev) == -1) 57 | { 58 | flashmq_logf(LOG_ERR, "Adding/changing externally watched fd %d to/from epoll produced error: %s", fd, strerror(errno)); 59 | } 60 | } 61 | 62 | void TesterGlobals::pollExternalRemove(int fd) 63 | { 64 | this->watchedFds.erase(fd); 65 | if (epoll_ctl(this->epoll_fd, EPOLL_CTL_DEL, fd, NULL) != 0) 66 | { 67 | flashmq_logf(LOG_ERR, "Removing externally watched fd %d from epoll produced error: %s", fd, strerror(errno)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/testerglobals.h: -------------------------------------------------------------------------------- 1 | #ifndef TESTERGLOBALS_H 2 | #define TESTERGLOBALS_H 3 | 4 | #include 5 | #include 6 | #include "queuedtasks.h" 7 | 8 | extern int testCount; 9 | extern int failCount; 10 | 11 | #define FMQ_COMPARE(actual, expected) \ 12 | if (!fmq_compare(actual, expected, #actual, #expected, __FILE__, __LINE__))\ 13 | return 1; \ 14 | 15 | bool fmq_assert(bool b, const char *failmsg, const char *actual, const char *expected, const char *file, int line); 16 | 17 | template 18 | inline bool fmq_compare(const T1 &t1, const T2 &t2, const char *actual, const char *expected, const char *file, int line) 19 | { 20 | return fmq_assert(t1 == t2, "Compared values are not the same", actual, expected, file, line); 21 | } 22 | 23 | namespace dbus_flashmq 24 | { 25 | 26 | class TesterGlobals 27 | { 28 | TesterGlobals(); 29 | 30 | 31 | public: 32 | std::unordered_map> watchedFds; 33 | int epoll_fd = -1; 34 | QueuedTasks delayedTasks; 35 | 36 | static TesterGlobals *getInstance(); 37 | void pollExternalFd(int fd, uint32_t events, const std::weak_ptr &p); 38 | void pollExternalRemove(int fd); 39 | }; 40 | 41 | } 42 | 43 | #endif // TESTERGLOBALS_H 44 | -------------------------------------------------------------------------------- /src/types.cpp: -------------------------------------------------------------------------------- 1 | #include "types.h" 2 | 3 | #include 4 | 5 | #include "exceptions.h" 6 | #include "vendor/flashmq_plugin.h" 7 | #include "vendor/json.hpp" 8 | 9 | using namespace dbus_flashmq; 10 | 11 | 12 | ValueMinMax &ValueMinMax::operator=(const ValueMinMax &other) 13 | { 14 | if (this == &other) 15 | return *this; 16 | 17 | // We don't update from uninitialized values. This proved necessary because there are signals that generate incomplete instantations of 18 | // this type, of which several are required to complete the picture. So, when we get a signal with only a 'max', we don't want to set 19 | // value back to nothing. 20 | if (other.value) 21 | this->value = other.value; 22 | 23 | // Because a ValueMinMax is also created by signals that may only contain a value dict item, we need to prevent erasing the min/max. 24 | if (other.min) 25 | this->min = other.min; 26 | if (other.max) 27 | this->max = other.max; 28 | 29 | return *this; 30 | } 31 | 32 | 33 | std::string Item::join_paths_with_slash(const std::string &a, const std::string &b) 34 | { 35 | std::string result = a + "/" + b; 36 | char prev = 0; 37 | result.erase(std::remove_if(result.begin(), result.end(), [&prev](char c) { 38 | if (c == prev && c == '/') 39 | return true; 40 | prev = c; 41 | return false; 42 | }), result.end()); 43 | return result; 44 | } 45 | 46 | /** 47 | * @brief Item::set_path stores the path with a leading slash. 48 | * @param path 49 | * 50 | * There are discrepancies between older and newer dbus methods wether the leading slash is used. 51 | */ 52 | std::string Item::prefix_path_with_slash(const std::string &path) 53 | { 54 | std::string new_path; 55 | if (path.find('/') != 0) 56 | new_path.push_back('/'); 57 | new_path.append(path); 58 | return new_path; 59 | } 60 | 61 | Item::Item(const std::string &path, const ValueMinMax &&value) : 62 | value(std::move(value)), 63 | path(prefix_path_with_slash(path)) 64 | { 65 | 66 | } 67 | 68 | Item::Item() 69 | { 70 | 71 | } 72 | 73 | /** 74 | * @brief Item::from_get_items constructs an item as returned by the GetItems method on Victron dbus services. 75 | * @param iter pointing at the first dict entry in below example. 76 | * @return 77 | * 78 | * GetItems() returns arrays like these: 79 | * 80 | * array [ 81 | * dict entry( 82 | * string "/Mgmt/ProcessName" 83 | * array [ 84 | * dict entry( 85 | * string "Value" 86 | * variant string "/opt/victronenergy/dbus-systemcalc-py/dbus_systemcalc.py" 87 | * ) 88 | * dict entry( 89 | * string "Text" 90 | * variant string "/opt/victronenergy/dbus-systemcalc-py/dbus_systemcalc.py" 91 | * ) 92 | * ] 93 | * ) 94 | * etc 95 | * ] 96 | * 97 | * Null entry looks like (empty array): 98 | * 99 | * dict entry( 100 | * string "/ProductId" 101 | * array [ 102 | * dict entry( 103 | * string "Value" 104 | * variant array [ 105 | * ] 106 | * ) 107 | * dict entry( 108 | * string "Text" 109 | * variant string "---" 110 | * ) 111 | * ] 112 | * ) 113 | * 114 | * This one parses one of those dict entries, containing a path as key and dict with text and value, to an Item. 115 | */ 116 | Item Item::from_get_items(DBusMessageIter *iter) 117 | { 118 | int type = dbus_message_iter_get_arg_type(iter); 119 | 120 | if (type != DBUS_TYPE_DICT_ENTRY) 121 | throw ValueError("Expected array from dbus when constructing item."); 122 | 123 | DBusMessageIter dict_iter; 124 | dbus_message_iter_recurse(iter, &dict_iter); 125 | 126 | if (dbus_message_iter_get_arg_type(&dict_iter) != DBUS_TYPE_STRING) 127 | throw ValueError("Dict key should be string."); 128 | 129 | DBusBasicValue key; 130 | dbus_message_iter_get_basic(&dict_iter, &key); 131 | const std::string path(key.str); 132 | 133 | dbus_message_iter_next(&dict_iter); 134 | 135 | if (dbus_message_iter_get_arg_type(&dict_iter) != DBUS_TYPE_ARRAY) 136 | throw ValueError("Dict value should be array."); 137 | 138 | DBusMessageIter array_iter; 139 | dbus_message_iter_recurse(&dict_iter, &array_iter); 140 | 141 | ValueMinMax value; 142 | 143 | int value_type = 0; 144 | while ((value_type = dbus_message_iter_get_arg_type(&array_iter)) != DBUS_TYPE_INVALID) 145 | { 146 | if (value_type != DBUS_TYPE_DICT_ENTRY) 147 | { 148 | throw ValueError("Item can only be created from dict entries."); 149 | } 150 | 151 | DBusMessageIter one_item_iter; 152 | dbus_message_iter_recurse(&array_iter, &one_item_iter); 153 | 154 | DBusBasicValue key_v; 155 | dbus_message_iter_get_basic(&one_item_iter, &key_v); 156 | 157 | std::string key(key_v.str); 158 | 159 | dbus_message_iter_next(&one_item_iter); 160 | 161 | if (dbus_message_iter_get_arg_type(&one_item_iter) != DBUS_TYPE_VARIANT) 162 | throw ValueError("Value/Text elements in dict must be variant."); 163 | 164 | VeVariant val(&one_item_iter); 165 | 166 | if (key == "Value") 167 | { 168 | value.value = std::move(val); 169 | } 170 | 171 | if (key == "Max") 172 | { 173 | value.max = std::move(val); 174 | } 175 | 176 | if (key == "Min") 177 | { 178 | value.min = std::move(val); 179 | } 180 | 181 | dbus_message_iter_next(&array_iter); 182 | } 183 | 184 | Item item(path, std::move(value)); 185 | return item; 186 | } 187 | 188 | /** 189 | * @brief Item::from_get_value 190 | * @param iter 191 | * @return Item 192 | * 193 | * GetValue() returns arrays like these: 194 | * 195 | * variant array [ 196 | * dict entry( 197 | * string "History/Daily/18/TimeInFloat" 198 | * variant double 0 199 | * ) 200 | * dict entry( 201 | * string "History/Daily/29/TimeInBulk" 202 | * variant double 0 203 | * ) 204 | * dict entry( 205 | * string "Yield/Power" 206 | * variant double 251 207 | * ) 208 | * etc 209 | * ] 210 | * 211 | * This one parses one of those inner dict entries to an Item. 212 | */ 213 | Item Item::from_get_value(DBusMessageIter *iter, const std::string &path_prefix) 214 | { 215 | const int type = dbus_message_iter_get_arg_type(iter); 216 | if (type != DBUS_TYPE_DICT_ENTRY) 217 | throw ValueError("Expected array from dbus when constructing item."); 218 | 219 | DBusMessageIter dict_iter; 220 | dbus_message_iter_recurse(iter, &dict_iter); 221 | 222 | if (dbus_message_iter_get_arg_type(&dict_iter) != DBUS_TYPE_STRING) 223 | throw ValueError("Dict key should be string."); 224 | 225 | DBusBasicValue key; 226 | dbus_message_iter_get_basic(&dict_iter, &key); 227 | const std::string path = join_paths_with_slash(path_prefix, key.str); 228 | 229 | dbus_message_iter_next(&dict_iter); 230 | 231 | const int value_type = dbus_message_iter_get_arg_type(&dict_iter); 232 | if (value_type != DBUS_TYPE_VARIANT) 233 | throw ValueError("Expected array from dbus when constructing item."); 234 | 235 | ValueMinMax val2; 236 | val2.value = VeVariant(&dict_iter); 237 | 238 | Item item(path, std::move(val2)); 239 | return item; 240 | } 241 | 242 | /** 243 | * @brief Item::from_properties_changed 244 | * @param msg 245 | * @return 246 | * 247 | * signal time=1691485092.671780 sender=:1.47 -> destination=(null destination) serial=8149 path=/Settings/Pump0/TankService; interface=com.victronenergy.BusItem; member=PropertiesChanged 248 | * array [ 249 | * dict entry( 250 | * string "Value" 251 | * variant string "notanksensor" 252 | * ) 253 | * dict entry( 254 | * string "Text" 255 | * variant string "notanksensor" 256 | * ) 257 | * dict entry( 258 | * string "Min" 259 | * variant int32 0 260 | * ) 261 | * dict entry( 262 | * string "Max" 263 | * variant int32 0 264 | * ) 265 | * dict entry( 266 | * string "Default" 267 | * variant string "notanksensor" 268 | * ) 269 | * ] 270 | */ 271 | Item Item::from_properties_changed(DBusMessage *msg) 272 | { 273 | int msg_type = dbus_message_get_type(msg); 274 | 275 | if (msg_type != DBUS_MESSAGE_TYPE_SIGNAL) 276 | throw std::runtime_error("In from_properties_changed: message is not a signal."); 277 | 278 | DBusMessageIter iter; 279 | dbus_message_iter_init(msg, &iter); 280 | 281 | const int type = dbus_message_iter_get_arg_type(&iter); 282 | if (type != DBUS_TYPE_ARRAY) 283 | throw ValueError("Expected array from dbus when constructing item."); 284 | 285 | const std::string path = dbus_message_get_path(msg); 286 | 287 | VeVariant v(&iter); 288 | 289 | ValueMinMax v2; 290 | v2.value = v.get_dict_val("Value"); 291 | 292 | Item item(path, std::move(v2)); 293 | return item; 294 | } 295 | 296 | std::string Item::as_json() 297 | { 298 | if (!cache_json.v.empty()) 299 | return cache_json.v; 300 | 301 | const bool mask = is_pincode(); 302 | nlohmann::json j { {"value", value.value.as_json_value(mask)} }; 303 | 304 | if (value.min) 305 | j["min"] = value.min.as_json_value(); 306 | if (value.max) 307 | j["max"] = value.max.as_json_value(); 308 | 309 | cache_json.v = j.dump(); 310 | return cache_json.v; 311 | } 312 | 313 | void Item::set_partial_mapping_details(const std::string &service) 314 | { 315 | this->service_name = service; 316 | } 317 | 318 | void Item::set_mapping_details(const std::string &vrm_id, const std::string &service, ServiceIdentifier instance) 319 | { 320 | ShortServiceName short_service_name(service, instance); 321 | 322 | this->vrm_id = vrm_id; 323 | this->short_service_name = short_service_name; 324 | this->service_name = service; 325 | 326 | std::ostringstream topic_stream; 327 | topic_stream << "N" << "/" << this->vrm_id.get() << "/" << short_service_name << path.get(); 328 | this->mqtt_publish_topic = topic_stream.str(); 329 | } 330 | 331 | void Item::publish(bool null_payload) 332 | { 333 | if (this->mqtt_publish_topic.get().empty()) 334 | return; 335 | 336 | // Blocked entries 337 | if ((short_service_name.service_type == "vebus" && path.get() == "/Interfaces/Mk2/Tunnel") || (short_service_name.service_type == "paygo" && path.get() == "/LVD/Threshold")) 338 | return; 339 | 340 | std::string payload; 341 | 342 | if (!null_payload) 343 | payload = as_json(); 344 | 345 | const bool retain = should_be_retained(); 346 | 347 | // Now that we use retain very selectively, never unpublish it. 348 | if (!(retain && payload.empty())) 349 | { 350 | // Note that FlashMQ merely appends the packet to the TCP client's output buffer as bytes, and once you return control 351 | // to the main loop, this buffer is flushed. This is a prerequisite to being fast. 352 | flashmq_publish_message(this->mqtt_publish_topic.get(), 0, retain, payload); 353 | } 354 | } 355 | 356 | /** 357 | * @brief Item::get_value is const and returns on purpose, because this value should only be set through Item::set_value(). 358 | * @return 359 | */ 360 | const ValueMinMax &Item::get_value() const 361 | { 362 | return this->value; 363 | } 364 | 365 | void Item::set_value(const ValueMinMax &val) 366 | { 367 | this->cache_json.v.clear(); 368 | this->value = val; 369 | } 370 | 371 | const std::string &Item::get_path() const 372 | { 373 | return this->path.get(); 374 | } 375 | 376 | const std::string &Item::get_service_name() const 377 | { 378 | const std::string &service = this->service_name.get(); 379 | 380 | assert(!service.empty()); 381 | 382 | if (service.empty()) 383 | { 384 | flashmq_logf(LOG_WARNING, "Requesting get_service_name() when it's empty. This means 'set_mapping_details()' has not been called and is a bug."); 385 | } 386 | 387 | return service; 388 | } 389 | 390 | /** 391 | * @brief Even though we don't use retained message anymore, some paths are still handy to have as retained. 392 | * @return 393 | */ 394 | bool Item::should_be_retained() const 395 | { 396 | return short_service_name.service_type == "system" && path.get() == "/Serial"; 397 | } 398 | 399 | bool Item::is_pincode() const 400 | { 401 | return short_service_name.service_type == "settings" && path.get() == "/Settings/Ble/Service/Pincode"; 402 | } 403 | 404 | bool Item::is_vrm_portal_mode() const 405 | { 406 | return short_service_name.service_type == "settings" && path.get() == "/Settings/Network/VrmPortal"; 407 | } 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | -------------------------------------------------------------------------------- /src/types.h: -------------------------------------------------------------------------------- 1 | #ifndef TYPES_H 2 | #define TYPES_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "vevariant.h" 8 | #include "shortservicename.h" 9 | #include "cachedstring.h" 10 | #include "boomstring.h" 11 | 12 | #define BRIDGE_DBUS "GXdbus" 13 | #define BRIDGE_RPC "GXrpc" 14 | #define BRIDGE_DEACTIVATED_STRING "deactivated" 15 | 16 | namespace dbus_flashmq 17 | { 18 | 19 | enum class VrmPortalMode 20 | { 21 | Unknown, 22 | Off, 23 | ReadOnly, 24 | Full 25 | }; 26 | 27 | struct ValueMinMax 28 | { 29 | VeVariant value; 30 | VeVariant min; 31 | VeVariant max; 32 | 33 | ValueMinMax &operator=(const ValueMinMax &other); 34 | }; 35 | 36 | class Item 37 | { 38 | ValueMinMax value; 39 | BoomString path; // without instance or service 40 | 41 | BoomString vrm_id; 42 | BoomString service_name; 43 | ShortServiceName short_service_name; 44 | BoomString mqtt_publish_topic; 45 | 46 | CachedString cache_json; 47 | 48 | static std::string join_paths_with_slash(const std::string &a, const std::string &b); 49 | static std::string prefix_path_with_slash(const std::string &s); 50 | Item(const std::string &path, const ValueMinMax &&value); 51 | public: 52 | Item(); 53 | 54 | static Item from_get_items(DBusMessageIter *iter); 55 | static Item from_get_value(DBusMessageIter *iter, const std::string &path_prefix); 56 | static Item from_properties_changed(DBusMessage *msg); 57 | 58 | std::string as_json(); 59 | void set_partial_mapping_details(const std::string &service); 60 | void set_mapping_details(const std::string &vrm_id, const std::string &service, ServiceIdentifier instance); 61 | void publish(bool null_payload=false); 62 | const ValueMinMax &get_value() const; 63 | void set_value(const ValueMinMax &val); 64 | const std::string &get_path() const; 65 | const std::string &get_service_name() const; 66 | bool should_be_retained() const; 67 | bool is_pincode() const; 68 | bool is_vrm_portal_mode() const; 69 | }; 70 | 71 | } 72 | 73 | #endif // TYPES_H 74 | -------------------------------------------------------------------------------- /src/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "exceptions.h" 14 | #include "fdguard.h" 15 | 16 | using namespace dbus_flashmq; 17 | 18 | int dbus_flashmq::dbus_watch_flags_to_epoll(int dbus_flags) 19 | { 20 | int epoll_flags = 0; 21 | 22 | if (dbus_flags & DBUS_WATCH_READABLE) 23 | { 24 | epoll_flags |= EPOLLIN; 25 | } 26 | if (dbus_flags & DBUS_WATCH_WRITABLE) 27 | { 28 | epoll_flags |= EPOLLOUT; 29 | } 30 | 31 | return epoll_flags; 32 | } 33 | 34 | int dbus_flashmq::epoll_flags_to_dbus_watch_flags(int epoll_flags) 35 | { 36 | int dbus_flags = 0; 37 | 38 | if (epoll_flags & EPOLLIN) 39 | dbus_flags |= DBusWatchFlags::DBUS_WATCH_READABLE; 40 | if (epoll_flags & EPOLLOUT) 41 | dbus_flags |= DBusWatchFlags::DBUS_WATCH_WRITABLE; 42 | if (epoll_flags & EPOLLERR) 43 | dbus_flags |= DBusWatchFlags::DBUS_WATCH_ERROR; 44 | if (epoll_flags & EPOLLHUP) 45 | dbus_flags |= DBusWatchFlags::DBUS_WATCH_HANGUP; 46 | 47 | return dbus_flags; 48 | } 49 | 50 | std::vector dbus_flashmq::splitToVector(const std::string &input, const char sep, size_t max, bool keep_empty_parts) 51 | { 52 | const auto substring_count = std::count(input.begin(), input.end(), sep) + 1; 53 | std::vector result; 54 | result.reserve(substring_count); 55 | 56 | size_t start = 0; 57 | size_t end; 58 | 59 | while (result.size() < max && (end = input.find(sep, start)) != std::string::npos) 60 | { 61 | if (start != end || keep_empty_parts) 62 | result.push_back(input.substr(start, end - start)); 63 | start = end + 1; // increase by length of seperator. 64 | } 65 | if (start != input.size() || keep_empty_parts) 66 | result.push_back(input.substr(start, std::string::npos)); 67 | 68 | return result; 69 | } 70 | 71 | std::string dbus_flashmq::get_service_type(const std::string &service) 72 | { 73 | if (service.find("com.victronenergy.") == std::string::npos) 74 | throw std::runtime_error("Not a victron service"); 75 | 76 | const std::vector parts = splitToVector(service, '.'); 77 | return parts.at(2); 78 | } 79 | 80 | std::string dbus_flashmq::get_uid_from_topic(const std::vector &subtopics) 81 | { 82 | return std::string(); 83 | } 84 | 85 | ServiceIdentifier dbus_flashmq::get_instance_from_items(const std::unordered_map &items) 86 | { 87 | uint32_t deviceInstance = 0; 88 | 89 | for (auto &p : items) 90 | { 91 | const Item &i = p.second; 92 | if (i.get_path() == "/DeviceInstance") 93 | { 94 | deviceInstance = i.get_value().value.as_int(); 95 | return deviceInstance; 96 | } 97 | } 98 | 99 | for (auto &p : items) 100 | { 101 | const Item &i = p.second; 102 | if (i.get_path() == "/Identifier") 103 | { 104 | const std::string s = i.get_value().value.as_text(); 105 | return s; 106 | } 107 | } 108 | 109 | return deviceInstance; 110 | } 111 | 112 | void dbus_flashmq::ltrim(std::string &s) 113 | { 114 | s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { 115 | return !std::isspace(ch); 116 | })); 117 | } 118 | 119 | void dbus_flashmq::rtrim(std::string &s) 120 | { 121 | s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { 122 | return !std::isspace(ch); 123 | }).base(), s.end()); 124 | } 125 | 126 | void dbus_flashmq::trim(std::string &s) 127 | { 128 | ltrim(s); 129 | rtrim(s); 130 | } 131 | 132 | std::string dbus_flashmq::get_stdout_from_process(const std::string &process) 133 | { 134 | pid_t p; 135 | return get_stdout_from_process(process, p); 136 | } 137 | 138 | std::string dbus_flashmq::get_stdout_from_process(const std::string &process, pid_t &out_pid) 139 | { 140 | int pipe_fds[2]; 141 | 142 | if (pipe(pipe_fds) < 0) 143 | throw std::runtime_error("Can't create pipes"); 144 | 145 | pid_t pid = fork(); 146 | 147 | if (pid == -1) 148 | throw std::runtime_error("What the fork?"); 149 | 150 | if (pid == 0) // the forked instance, which we are transforming with execlp 151 | { 152 | // Capture stdout on the write end of the pipe. 153 | while ((dup2(pipe_fds[1], STDOUT_FILENO) == -1) && (errno == EINTR)) {} 154 | 155 | // We duplicated our write fd, so we can close these. 156 | close(pipe_fds[1]); 157 | close(pipe_fds[0]); 158 | 159 | // Brute force close any open file/socket, because I don't want the child to see them. 160 | struct rlimit rlim; 161 | memset(&rlim, 0, sizeof (struct rlimit)); 162 | getrlimit(RLIMIT_NOFILE, &rlim); 163 | for (rlim_t i = 3; i < rlim.rlim_cur; ++i) close (i); 164 | 165 | execlp(process.c_str(), process.c_str(), nullptr); 166 | std::cerr << strerror(errno) << std::endl; 167 | exit(66); 168 | } 169 | 170 | out_pid = pid; 171 | 172 | close(pipe_fds[1]); // Close the write-end of the pipe, because we're only reading from it. 173 | FdGuard fd_guard = pipe_fds[0]; 174 | 175 | int status = 0; 176 | waitpid(pid, &status, 0); 177 | out_pid = -1; 178 | 179 | std::ostringstream o; 180 | 181 | if (!WIFEXITED(status)) 182 | { 183 | if (WIFSIGNALED(status)) 184 | { 185 | o << "Calling '" << process << "' signalled: " << WTERMSIG(status); 186 | throw std::runtime_error(o.str()); 187 | } 188 | 189 | o << "Process '" << process << "did not exit normally. Does it exist?"; 190 | throw std::runtime_error(o.str()); 191 | } 192 | else 193 | { 194 | int return_code = WEXITSTATUS(status); 195 | 196 | if (return_code != 0) 197 | { 198 | o << "Process '" << process << "' exitted with " << return_code; 199 | throw std::runtime_error(o.str()); 200 | } 201 | } 202 | 203 | char buf[256]; 204 | memset(&buf, 0, 256); 205 | 206 | ssize_t n; 207 | while ((n = read(fd_guard.get(), &buf, 128)) < 0) 208 | { 209 | if (n < 0 && errno == EINTR) 210 | continue; 211 | 212 | throw std::runtime_error(strerror(errno)); 213 | } 214 | 215 | std::string result(buf); 216 | return result; 217 | } 218 | 219 | int16_t dbus_flashmq::s_to_int16(const std::string &s) 220 | { 221 | int32_t x = std::stol(s); 222 | 223 | if (x > 32767 || x < -32768) 224 | throw ValueError("Value '" + s + "' too big for int16"); 225 | 226 | return x; 227 | } 228 | 229 | 230 | uint8_t dbus_flashmq::s_to_uint8(const std::string &s) 231 | { 232 | uint16_t x = std::stoi(s); 233 | 234 | if (x & 0xFF00) 235 | throw ValueError("Value '" + s + "' too big for uint8"); 236 | 237 | return x; 238 | } 239 | 240 | uint16_t dbus_flashmq::s_to_uint16(const std::string &s) 241 | { 242 | uint32_t x = std::stol(s); 243 | 244 | if (x & 0xFFFF0000) 245 | throw ValueError("Value '" + s + "' too big for uint16"); 246 | 247 | return x; 248 | } 249 | 250 | 251 | std::string dbus_flashmq::dbus_message_get_error_name_safe(DBusMessage *msg) 252 | { 253 | std::string result; 254 | const int msg_type = dbus_message_get_type(msg); 255 | 256 | if (msg_type == DBUS_MESSAGE_TYPE_ERROR) 257 | { 258 | const char *_msg = dbus_message_get_error_name(msg); 259 | 260 | if (_msg) 261 | result = _msg; 262 | } 263 | 264 | return result; 265 | } 266 | 267 | bool dbus_flashmq::client_id_is_bridge(const std::string &clientid) 268 | { 269 | return clientid.find("GXdbus_") == 0 || clientid.find("GXrpc_") == 0; 270 | } 271 | 272 | /** 273 | * @brief crypt_match uses the system crypt function to match hashed passwords. 274 | * @param phrase like 'hallo' 275 | * @param crypted like '$2a$08$LBfjL0PfMBbjWxCzLBfjLurkA7K0tuDn44rNUXDBvatSgSqHvwaHS' 276 | * @return 277 | * 278 | * Password 'hallo' yields this: 279 | * $2a$08$LBfjL0PfMBbjWxCzLBfjLurkA7K0tuDn44rNUXDBvatSgSqHvwaHS 280 | */ 281 | bool dbus_flashmq::crypt_match(const std::string &phrase, const std::string &crypted) 282 | { 283 | struct crypt_data data; 284 | memset(&data, 0, sizeof(struct crypt_data)); 285 | crypt_r(phrase.c_str(), crypted.c_str(), &data); 286 | 287 | const std::string new_crypt(data.output); 288 | return crypted == new_crypt; 289 | } 290 | 291 | 292 | VrmPortalMode dbus_flashmq::parseVrmPortalMode(int val) 293 | { 294 | switch(val) 295 | { 296 | case(0): 297 | return VrmPortalMode::Off; 298 | case(1): 299 | return VrmPortalMode::ReadOnly; 300 | case(2): 301 | return VrmPortalMode::Full; 302 | default: 303 | return VrmPortalMode::Unknown; 304 | } 305 | } 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_H 2 | #define UTILS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "types.h" 11 | #include "serviceidentifier.h" 12 | 13 | namespace dbus_flashmq 14 | { 15 | 16 | int dbus_watch_flags_to_epoll(int dbus_flags); 17 | int epoll_flags_to_dbus_watch_flags(int epoll_flags); 18 | std::vector splitToVector(const std::string &input, const char sep, size_t max = std::numeric_limits::max(), bool keep_empty_parts = true); 19 | std::string get_service_type(const std::string &service); 20 | std::string get_uid_from_topic(const std::vector &subtopics); 21 | ServiceIdentifier get_instance_from_items(const std::unordered_map &items); 22 | void ltrim(std::string &s); 23 | void rtrim(std::string &s); 24 | void trim(std::string &s); 25 | std::string get_stdout_from_process(const std::string &process); 26 | std::string get_stdout_from_process(const std::string &process, pid_t &out_pid); 27 | std::string dbus_message_get_error_name_safe(DBusMessage *msg); 28 | 29 | int16_t s_to_int16(const std::string &s); 30 | uint8_t s_to_uint8(const std::string &s); 31 | uint16_t s_to_uint16(const std::string &s); 32 | 33 | template 34 | T get_random() 35 | { 36 | std::vector buf(1); 37 | getrandom(buf.data(), sizeof(T), 0); 38 | T val = buf.at(0); 39 | return val; 40 | } 41 | 42 | bool client_id_is_bridge(const std::string &clientid); 43 | bool crypt_match(const std::string &phrase, const std::string &crypted); 44 | VrmPortalMode parseVrmPortalMode(int val); 45 | 46 | } 47 | 48 | #endif // UTILS_H 49 | -------------------------------------------------------------------------------- /src/vevariant.cpp: -------------------------------------------------------------------------------- 1 | #include "vevariant.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "vendor/flashmq_plugin.h" 7 | #include "exceptions.h" 8 | #include "dbusmessageiteropencontainerguard.h" 9 | #include "dbusmessageitersignature.h" 10 | 11 | using namespace dbus_flashmq; 12 | 13 | VeVariantArray::VeVariantArray(const VeVariantArray &other) 14 | { 15 | *this = other; 16 | } 17 | 18 | VeVariantArray &VeVariantArray::operator=(const VeVariantArray &other) 19 | { 20 | if (other.arr) 21 | { 22 | this->arr = std::make_unique>(); 23 | (*this->arr) = (*other.arr); 24 | } 25 | 26 | this->contained_dbus_type = other.contained_dbus_type; 27 | 28 | return *this; 29 | } 30 | 31 | VeVariant::VeVariant(DBusMessageIter *iter) 32 | { 33 | int dbus_type = dbus_message_iter_get_arg_type(iter); 34 | 35 | DBusMessageIter variant_iter; 36 | DBusMessageIter *_iter = iter; 37 | 38 | if (dbus_type == DBUS_TYPE_VARIANT) 39 | { 40 | dbus_message_iter_recurse(iter, &variant_iter); 41 | _iter = &variant_iter; 42 | dbus_type = dbus_message_iter_get_arg_type(&variant_iter); 43 | } 44 | 45 | DBusBasicValue value; 46 | 47 | // All the calls to dbus_message_iter_get_basic() looks like code duplication, but it's the safest way to avoid 48 | // a crash on trying to read a value from a non-basic type. In other words: there is no function to ask whether 49 | // the type is basic. 50 | switch (dbus_type) 51 | { 52 | case DBUS_TYPE_INT16: 53 | dbus_message_iter_get_basic(_iter, &value); 54 | this->i16 = value.i16; 55 | this->type = VeVariantType::IntegerSigned16; 56 | break; 57 | case DBUS_TYPE_UINT16: 58 | dbus_message_iter_get_basic(_iter, &value); 59 | this->u16 = value.u16; 60 | this->type = VeVariantType::IntegerUnsigned16; 61 | break; 62 | case DBUS_TYPE_INT32: 63 | dbus_message_iter_get_basic(_iter, &value); 64 | this->i32 = value.i32; 65 | this->type = VeVariantType::IntegerSigned32; 66 | break; 67 | case DBUS_TYPE_UINT32: 68 | dbus_message_iter_get_basic(_iter, &value); 69 | this->u32 = value.u32; 70 | this->type = VeVariantType::IntegerUnsigned32; 71 | break; 72 | case DBUS_TYPE_BOOLEAN: 73 | dbus_message_iter_get_basic(_iter, &value); 74 | this->bool_val = value.bool_val; 75 | this->type = VeVariantType::Boolean; 76 | break; 77 | case DBUS_TYPE_INT64: 78 | dbus_message_iter_get_basic(_iter, &value); 79 | this->i64 = value.i64; 80 | this->type = VeVariantType::IntegerSigned64; 81 | break; 82 | case DBUS_TYPE_UINT64: 83 | dbus_message_iter_get_basic(_iter, &value); 84 | this->u64 = value.u64; 85 | this->type = VeVariantType::IntegerUnsigned64; 86 | break; 87 | case DBUS_TYPE_DOUBLE: 88 | dbus_message_iter_get_basic(_iter, &value); 89 | this->d = value.dbl; 90 | this->type = VeVariantType::Double; 91 | break; 92 | case DBUS_TYPE_BYTE: 93 | dbus_message_iter_get_basic(_iter, &value); 94 | this->u8 = value.byt; 95 | this->type = VeVariantType::IntegerUnsigned8; 96 | break; 97 | case DBUS_TYPE_STRING: 98 | dbus_message_iter_get_basic(_iter, &value); 99 | this->str = value.str; 100 | this->type = VeVariantType::String; 101 | break; 102 | case DBUS_TYPE_STRUCT: 103 | flashmq_logf(LOG_WARNING, "Struct not implemented. In C++, it would have to be a map/array to be dynamic"); 104 | break; 105 | case DBUS_TYPE_ARRAY: 106 | { 107 | // You can't tell an array and dict apart from the outside, so we have to peek in. 108 | DBusMessageIter peek_iter; 109 | dbus_message_iter_recurse(_iter, &peek_iter); 110 | const int array_type = dbus_message_iter_get_arg_type(&peek_iter); 111 | 112 | DBusMessageIterSignature signature(&peek_iter); 113 | this->contained_array_type_as_string = signature.signature; 114 | 115 | if (array_type == DBUS_TYPE_DICT_ENTRY) 116 | { 117 | this->type = VeVariantType::Dict; 118 | this->dict = make_dict(_iter); 119 | } 120 | else 121 | { 122 | this->type = VeVariantType::Array; 123 | arr = make_array(_iter); 124 | } 125 | 126 | break; 127 | } 128 | case DBUS_TYPE_DICT_ENTRY: 129 | flashmq_logf(LOG_WARNING, "DBUS_TYPE_DICT_ENTRY in variant is not supported.."); 130 | break; 131 | case DBUS_TYPE_VARIANT: 132 | flashmq_logf(LOG_WARNING, "Variant in variant is not supported."); 133 | break; 134 | default: 135 | break; 136 | } 137 | } 138 | 139 | VeVariant::VeVariant(const std::string &v) : 140 | str(v), 141 | type(VeVariantType::String) 142 | { 143 | 144 | } 145 | 146 | VeVariant::VeVariant(const std::optional &v) : 147 | str(v.value_or(std::string())), 148 | type(v.has_value() ? VeVariantType::String : VeVariantType::Array), 149 | contained_array_type_as_string(v.has_value() ? std::string() : EMPTY_ARRAY_AS_NULL_VALUE_TYPE) 150 | { 151 | 152 | } 153 | 154 | VeVariant::VeVariant(const char *s) : 155 | str(s), 156 | type(VeVariantType::String) 157 | { 158 | 159 | } 160 | 161 | VeVariant::VeVariant(const std::optional b) : 162 | bool_val(static_cast(b.value_or(0))), 163 | type(b.has_value() ? VeVariantType::Boolean : VeVariantType::Array), 164 | contained_array_type_as_string(b.has_value() ? std::string() : EMPTY_ARRAY_AS_NULL_VALUE_TYPE) 165 | { 166 | 167 | } 168 | 169 | /** 170 | * @brief VeVariant::VeVariant 171 | * @param j 172 | * 173 | * It uses 32 bit ints if it fits, otherwise 64. Just like velib_python. 174 | */ 175 | VeVariant::VeVariant(const nlohmann::json &j) 176 | { 177 | if (j.is_number_integer()) 178 | { 179 | const dbus_int64_t v = j.get(); 180 | if (v > std::numeric_limits::max() || v < std::numeric_limits::min()) 181 | { 182 | this->i64 = v; 183 | this->type = VeVariantType::IntegerSigned64; 184 | } 185 | else 186 | { 187 | const dbus_int32_t v = j.get(); 188 | this->i32 = v; 189 | this->type = VeVariantType::IntegerSigned32; 190 | } 191 | } 192 | else if (j.is_string()) 193 | { 194 | this->str = j.get(); 195 | this->type = VeVariantType::String; 196 | } 197 | else if (j.is_number_float()) 198 | { 199 | this->d = j.get();; 200 | this->type = VeVariantType::Double; 201 | } 202 | else if (j.is_boolean()) 203 | { 204 | this->bool_val = j.get(); 205 | this->type = VeVariantType::Boolean; 206 | } 207 | else if (j.is_array()) 208 | { 209 | // We can't know here is someone is really trying to write an empty array, or null. We also support json null now; see below. 210 | 211 | this->arr = std::make_unique>(); 212 | type = VeVariantType::Array; 213 | contained_array_type_as_string = VALID_EMPTY_ARRAY_VALUE_TYPE; 214 | 215 | bool type_anchored = false; 216 | int last_type = 0; 217 | 218 | for (const nlohmann::json &v : j) 219 | { 220 | VeVariant v2(v); 221 | 222 | if (type_anchored && last_type != v2.get_dbus_type()) 223 | throw ValueError("Dbus doesn't support arrays of mixed type"); 224 | 225 | this->arr->push_back(std::move(v2)); 226 | 227 | contained_array_type_as_string = v2.get_dbus_type_as_string_flat(); 228 | 229 | type_anchored = true; 230 | last_type = v2.get_dbus_type(); 231 | } 232 | } 233 | else if (j.is_object()) 234 | { 235 | 236 | } 237 | else if (j.is_null()) 238 | { 239 | this->arr = std::make_unique>(); 240 | type = VeVariantType::Array; 241 | contained_array_type_as_string = EMPTY_ARRAY_AS_NULL_VALUE_TYPE; 242 | } 243 | else 244 | { 245 | const std::string type_name(j.type_name()); 246 | throw ValueError("Unsupported JSON value type: " + type_name); 247 | } 248 | } 249 | 250 | VeVariant::VeVariant(const VeVariant &other) 251 | { 252 | *this = other; 253 | } 254 | 255 | std::unique_ptr > VeVariant::make_array(DBusMessageIter *iter) 256 | { 257 | if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_ARRAY) 258 | throw ValueError("Calling make_array() on something other than array."); 259 | 260 | const int n = dbus_message_iter_get_element_count(iter); 261 | std::unique_ptr> result = std::make_unique>(); 262 | result->reserve(n); 263 | 264 | DBusMessageIter array_iter; 265 | dbus_message_iter_recurse(iter, &array_iter); 266 | iter = nullptr; 267 | 268 | while (dbus_message_iter_get_arg_type(&array_iter) != DBUS_TYPE_INVALID) 269 | { 270 | result->emplace_back(&array_iter); 271 | dbus_message_iter_next(&array_iter); 272 | } 273 | 274 | return result; 275 | } 276 | 277 | std::unique_ptr> VeVariant::make_dict(DBusMessageIter *iter) 278 | { 279 | if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_ARRAY) 280 | throw ValueError("Calling make_dict() on something other than array."); 281 | 282 | std::unique_ptr> result = std::make_unique>(); 283 | std::unordered_map &r = *result; 284 | 285 | DBusMessageIter array_iter; 286 | dbus_message_iter_recurse(iter, &array_iter); 287 | iter = nullptr; 288 | 289 | while (dbus_message_iter_get_arg_type(&array_iter) != DBUS_TYPE_INVALID) 290 | { 291 | if (dbus_message_iter_get_arg_type(&array_iter) != DBUS_TYPE_DICT_ENTRY) 292 | throw ValueError("Make_dict() needs dict entries in array."); 293 | 294 | DBusMessageIter dict_iter; 295 | dbus_message_iter_recurse(&array_iter, &dict_iter); 296 | 297 | VeVariant key(&dict_iter); 298 | dbus_message_iter_next(&dict_iter); 299 | VeVariant val(&dict_iter); 300 | 301 | r[key] = val; 302 | 303 | dbus_message_iter_next(&array_iter); 304 | } 305 | 306 | return result; 307 | } 308 | 309 | std::string VeVariant::as_text() const 310 | { 311 | std::ostringstream o; 312 | 313 | switch (this->type) 314 | { 315 | case VeVariantType::IntegerSigned16: 316 | o << i16; 317 | break; 318 | case VeVariantType::IntegerSigned32: 319 | o << i32; 320 | break; 321 | case VeVariantType::IntegerSigned64: 322 | o << i64; 323 | break; 324 | case VeVariantType::IntegerUnsigned8: 325 | o << u8; 326 | break; 327 | case VeVariantType::IntegerUnsigned16: 328 | o << u16; 329 | break; 330 | case VeVariantType::IntegerUnsigned32: 331 | o << u32; 332 | break; 333 | case VeVariantType::IntegerUnsigned64: 334 | o << u64; 335 | break; 336 | case VeVariantType::String: 337 | { 338 | o << str; 339 | break; 340 | } 341 | case VeVariantType::Double: 342 | o << d; 343 | break; 344 | case VeVariantType::Boolean: 345 | o << std::boolalpha << bool_val; 346 | break; 347 | case VeVariantType::Array: 348 | case VeVariantType::Dict: 349 | { 350 | auto v = as_json_value(); 351 | o << v.dump(); 352 | break; 353 | } 354 | default: 355 | return "unknown type"; 356 | } 357 | 358 | std::string result(o.str()); 359 | return result; 360 | } 361 | 362 | nlohmann::json VeVariant::as_json_value(bool mask) const 363 | { 364 | switch (this->type) 365 | { 366 | case VeVariantType::IntegerSigned16: 367 | return i16; 368 | case VeVariantType::IntegerSigned32: 369 | return i32; 370 | case VeVariantType::IntegerSigned64: 371 | return i64; 372 | case VeVariantType::IntegerUnsigned8: 373 | return u8; 374 | case VeVariantType::IntegerUnsigned16: 375 | return u16; 376 | case VeVariantType::IntegerUnsigned32: 377 | return u32; 378 | case VeVariantType::IntegerUnsigned64: 379 | return u64; 380 | case VeVariantType::String: 381 | { 382 | if (mask) 383 | { 384 | std::string val = str; 385 | std::transform(val.begin(), val.end(), val.begin(), [](char c) {return '*';}); 386 | return val; 387 | } 388 | return str; 389 | } 390 | case VeVariantType::Double: 391 | return d; 392 | case VeVariantType::Boolean: 393 | { 394 | // TODO: I don't actually know if the the current dbus-mqtt uses 1/0 or true/false. 395 | bool v = static_cast(bool_val); 396 | return v; 397 | } 398 | case VeVariantType::Array: 399 | { 400 | // The array being null when we think we are an array shouldn't happen, but playing it safe. 401 | if (!this->arr) 402 | return nlohmann::json(); 403 | 404 | nlohmann::json array = nlohmann::json::array({}); 405 | 406 | if (this->arr) 407 | { 408 | // I disabled the EMPTY_ARRAY_AS_NULL_VALUE_TYPE check, because there are dbus services that don't seem to stick to that rule. 409 | if (this->arr->empty()) // && this->contained_array_type_as_string == EMPTY_ARRAY_AS_NULL_VALUE_TYPE) 410 | return nlohmann::json(); 411 | 412 | for (VeVariant &v : *this->arr) 413 | { 414 | array.push_back(v.as_json_value()); 415 | } 416 | } 417 | 418 | return array; 419 | } 420 | case VeVariantType::Dict: 421 | { 422 | nlohmann::json obj = nlohmann::json::object(); 423 | 424 | if (this->dict) 425 | { 426 | for (auto &p : *this->dict) 427 | { 428 | const VeVariant &key = p.first; 429 | 430 | if (key.get_type() != VeVariantType::String) 431 | throw ValueError("JSON dict keys must be string."); 432 | 433 | obj[key.as_json_value()] = p.second.as_json_value(); 434 | } 435 | } 436 | 437 | return obj; 438 | } 439 | default: 440 | return "unknown_type"; 441 | } 442 | } 443 | 444 | // Kind of a placeholder. It may need to be templated if I use it more seriously. 445 | int VeVariant::as_int() const 446 | { 447 | switch (this->type) 448 | { 449 | case VeVariantType::IntegerSigned16: 450 | return i16; 451 | case VeVariantType::IntegerSigned32: 452 | return i32; 453 | case VeVariantType::IntegerSigned64: 454 | return i64; 455 | case VeVariantType::IntegerUnsigned8: 456 | return u8; 457 | case VeVariantType::IntegerUnsigned16: 458 | return u16; 459 | case VeVariantType::IntegerUnsigned32: 460 | return u32; 461 | case VeVariantType::IntegerUnsigned64: 462 | return u64; 463 | case VeVariantType::Double: 464 | return static_cast(d); 465 | case VeVariantType::Boolean: 466 | return static_cast(bool_val); 467 | break; 468 | default: 469 | return 0; 470 | } 471 | 472 | return 0; 473 | } 474 | 475 | int VeVariant::get_dbus_type() const 476 | { 477 | switch (this->type) 478 | { 479 | case VeVariantType::IntegerSigned16: 480 | return DBUS_TYPE_INT16; 481 | case VeVariantType::IntegerSigned32: 482 | return DBUS_TYPE_INT32; 483 | case VeVariantType::IntegerSigned64: 484 | return DBUS_TYPE_INT64; 485 | case VeVariantType::IntegerUnsigned8: 486 | return DBUS_TYPE_BYTE; 487 | case VeVariantType::IntegerUnsigned16: 488 | return DBUS_TYPE_UINT16; 489 | case VeVariantType::IntegerUnsigned32: 490 | return DBUS_TYPE_UINT32; 491 | case VeVariantType::IntegerUnsigned64: 492 | return DBUS_TYPE_UINT64; 493 | case VeVariantType::String: 494 | return DBUS_TYPE_STRING; 495 | case VeVariantType::Double: 496 | return DBUS_TYPE_DOUBLE; 497 | case VeVariantType::Boolean: 498 | return DBUS_TYPE_BOOLEAN; 499 | case VeVariantType::Array: 500 | return DBUS_TYPE_ARRAY; 501 | case VeVariantType::Dict: 502 | flashmq_logf(LOG_WARNING, "Using get_dbus_type() on a dict. This is not a dbus type, so what are you trying to do?"); 503 | return DBUS_TYPE_ARRAY; // TODO: array with dict types. Hmm. How? 504 | default: 505 | return DBUS_TYPE_INVALID; 506 | } 507 | } 508 | 509 | std::string VeVariant::get_dbus_type_as_string_flat() const 510 | { 511 | switch (this->type) 512 | { 513 | case VeVariantType::IntegerSigned16: 514 | return DBUS_TYPE_INT16_AS_STRING; 515 | case VeVariantType::IntegerSigned32: 516 | return DBUS_TYPE_INT32_AS_STRING; 517 | case VeVariantType::IntegerSigned64: 518 | return DBUS_TYPE_INT64_AS_STRING; 519 | case VeVariantType::IntegerUnsigned8: 520 | return DBUS_TYPE_BYTE_AS_STRING; 521 | case VeVariantType::IntegerUnsigned16: 522 | return DBUS_TYPE_UINT16_AS_STRING; 523 | case VeVariantType::IntegerUnsigned32: 524 | return DBUS_TYPE_UINT32_AS_STRING; 525 | case VeVariantType::IntegerUnsigned64: 526 | return DBUS_TYPE_UINT64_AS_STRING; 527 | case VeVariantType::String: 528 | return DBUS_TYPE_STRING_AS_STRING; 529 | case VeVariantType::Double: 530 | return DBUS_TYPE_DOUBLE_AS_STRING; 531 | case VeVariantType::Boolean: 532 | return DBUS_TYPE_BOOLEAN_AS_STRING; 533 | case VeVariantType::Array: 534 | return DBUS_TYPE_ARRAY_AS_STRING; 535 | case VeVariantType::Dict: 536 | return DBUS_TYPE_ARRAY_AS_STRING; // TODO: array with dict types. Hmm. How? 537 | default: 538 | return DBUS_TYPE_INVALID_AS_STRING; 539 | } 540 | } 541 | 542 | std::string VeVariant::get_dbus_type_as_string_recursive() const 543 | { 544 | std::string result = get_dbus_type_as_string_flat(); 545 | 546 | if (this->type == VeVariantType::Array) 547 | result += get_contained_type_as_string(); 548 | 549 | return result; 550 | } 551 | 552 | /** 553 | * @brief VeVariant::get_contained_type_as_string is only needed for arrays. 554 | * @return 555 | */ 556 | std::string VeVariant::get_contained_type_as_string() const 557 | { 558 | std::string recursive = this->contained_array_type_as_string; 559 | 560 | if (arr && !arr->empty()) 561 | { 562 | const VeVariant &first = arr->front(); 563 | recursive += first.get_contained_type_as_string(); 564 | } 565 | 566 | return recursive; 567 | } 568 | 569 | /** 570 | * @brief VeVariant::append_args_to_dbus_message is to be used for appending the VeVariant as argument to dbus messages, like method calls. 571 | * @param iter 572 | */ 573 | void VeVariant::append_args_to_dbus_message(DBusMessageIter *iter) const 574 | { 575 | switch (this->type) 576 | { 577 | case VeVariantType::IntegerSigned16: 578 | { 579 | dbus_message_iter_append_basic(iter, get_dbus_type(), &i16); 580 | break; 581 | } 582 | case VeVariantType::IntegerSigned32: 583 | { 584 | dbus_message_iter_append_basic(iter, get_dbus_type(), &i32); 585 | break; 586 | } 587 | case VeVariantType::IntegerSigned64: 588 | { 589 | dbus_message_iter_append_basic(iter, get_dbus_type(), &i64); 590 | break; 591 | } 592 | case VeVariantType::IntegerUnsigned8: 593 | { 594 | dbus_message_iter_append_basic(iter, get_dbus_type(), &u8); 595 | break; 596 | } 597 | case VeVariantType::IntegerUnsigned16: 598 | { 599 | dbus_message_iter_append_basic(iter, get_dbus_type(), &u16); 600 | break; 601 | } 602 | case VeVariantType::IntegerUnsigned32: 603 | { 604 | dbus_message_iter_append_basic(iter, get_dbus_type(), &u32); 605 | break; 606 | } 607 | case VeVariantType::IntegerUnsigned64: 608 | { 609 | dbus_message_iter_append_basic(iter, get_dbus_type(), &u64); 610 | break; 611 | } 612 | case VeVariantType::String: 613 | { 614 | const char *mystr = str.c_str(); 615 | dbus_message_iter_append_basic(iter, get_dbus_type(), &mystr); 616 | break; 617 | } 618 | case VeVariantType::Double: 619 | { 620 | dbus_message_iter_append_basic(iter, get_dbus_type(), &d); 621 | break; 622 | } 623 | case VeVariantType::Boolean: 624 | { 625 | dbus_message_iter_append_basic(iter, get_dbus_type(), &bool_val); 626 | break; 627 | } 628 | case VeVariantType::Array: 629 | { 630 | if (!arr) // Empty pointer shouldn't happen, but just in case. 631 | { 632 | DBusMessageIterOpenContainerGuard array_iter(iter, DBUS_TYPE_ARRAY, VALID_EMPTY_ARRAY_VALUE_TYPE); 633 | } 634 | else 635 | { 636 | DBusMessageIterOpenContainerGuard array_iter(iter, DBUS_TYPE_ARRAY, get_contained_type_as_string().c_str()); 637 | int last_type = 0; 638 | bool type_anchored = false; 639 | for(const VeVariant &v : *arr) 640 | { 641 | if (type_anchored && v.get_dbus_type() != last_type) 642 | throw ValueError("You can't put different types in a dbus array."); 643 | 644 | v.append_args_to_dbus_message(array_iter.get_array_iter()); 645 | last_type = v.get_dbus_type(); 646 | type_anchored = true; 647 | } 648 | } 649 | 650 | break; 651 | } 652 | case VeVariantType::Dict: 653 | // TODO: do we need to be able to set dicts? 654 | throw ValueError("append_args() for dict is not supported. Yet?"); 655 | default: 656 | throw ValueError("Default case in append_args(). You shouldn't get here."); 657 | } 658 | } 659 | 660 | VeVariant &VeVariant::operator=(const VeVariant &other) 661 | { 662 | if (other.arr) 663 | { 664 | this->arr = std::make_unique>(); 665 | (*this->arr) = (*other.arr); 666 | } 667 | else 668 | { 669 | this->arr.reset(); 670 | } 671 | 672 | if (other.dict) 673 | { 674 | this->dict = std::make_unique>(); 675 | (*this->dict) = (*other.dict); 676 | } 677 | else 678 | { 679 | this->dict.reset(); 680 | } 681 | 682 | this->u8 = other.u8; 683 | this->i16 = other.i16; 684 | this->u16 = other.u16; 685 | this->i32 = other.i32; 686 | this->u32 = other.u32; 687 | this->bool_val = other.bool_val; 688 | this->i64 = other.i64; 689 | this->u64 = other.u64; 690 | this->str = other.str; 691 | this->d = other.d; 692 | this->type = other.type; 693 | this->contained_array_type_as_string = other.contained_array_type_as_string; 694 | 695 | return *this; 696 | } 697 | 698 | bool VeVariant::operator==(const VeVariant &other) const 699 | { 700 | if (this->type != other.type) 701 | return false; 702 | 703 | switch (type) 704 | { 705 | case VeVariantType::Unknown: 706 | return true; 707 | case VeVariantType::IntegerSigned16: 708 | return this->i16 == other.i16; 709 | case VeVariantType::IntegerSigned32: 710 | return this->i32 == other.i32; 711 | case VeVariantType::IntegerSigned64: 712 | return this->i64 == other.i64; 713 | case VeVariantType::IntegerUnsigned8: 714 | return this->u8 == other.u8; 715 | case VeVariantType::IntegerUnsigned16: 716 | return this->u16 == other.u16; 717 | case VeVariantType::IntegerUnsigned32: 718 | return this->u32 == other.u32; 719 | case VeVariantType::IntegerUnsigned64: 720 | return this->u64 == other.u64; 721 | case VeVariantType::String: 722 | return this->str == other.str; 723 | case VeVariantType::Double: 724 | return this->d == other.d; 725 | case VeVariantType::Boolean: 726 | return this->bool_val == other.bool_val; 727 | case VeVariantType::Array: 728 | { 729 | if (!this->arr && !other.arr) 730 | return true; 731 | 732 | if (!this->arr || !other.arr) 733 | return false; 734 | 735 | return *this->arr == *other.arr; 736 | } 737 | case VeVariantType::Dict: 738 | { 739 | if (!this->dict && !other.dict) 740 | return true; 741 | 742 | if (!this->dict || !other.dict) 743 | return false; 744 | 745 | return *this->dict == *other.dict; 746 | } 747 | default: 748 | throw ValueError("Shouldn't end up here."); 749 | } 750 | } 751 | 752 | VeVariant::operator bool() const 753 | { 754 | return type != VeVariantType::Unknown; 755 | } 756 | 757 | VeVariantType VeVariant::get_type() const 758 | { 759 | return type; 760 | } 761 | 762 | std::size_t VeVariant::hash() const 763 | { 764 | switch (type) 765 | { 766 | case VeVariantType::Unknown: 767 | return std::hash()(0); 768 | case VeVariantType::IntegerSigned16: 769 | return std::hash()(0); 770 | case VeVariantType::IntegerSigned32: 771 | return std::hash()(0); 772 | case VeVariantType::IntegerSigned64: 773 | return std::hash()(0); 774 | case VeVariantType::IntegerUnsigned8: 775 | return std::hash()(0); 776 | case VeVariantType::IntegerUnsigned16: 777 | return std::hash()(0); 778 | case VeVariantType::IntegerUnsigned32: 779 | return std::hash()(0); 780 | case VeVariantType::IntegerUnsigned64: 781 | return std::hash()(0); 782 | case VeVariantType::String: 783 | return std::hash()(str); 784 | case VeVariantType::Double: 785 | return std::hash()(d); 786 | case VeVariantType::Boolean: 787 | return std::hash()(bool_val); 788 | case VeVariantType::Array: 789 | { 790 | if (!this->arr) 791 | return std::hash()(0); 792 | 793 | size_t hash = 0; 794 | 795 | for (const VeVariant &v : *this->arr) 796 | { 797 | hash ^= v.hash(); 798 | } 799 | 800 | return hash; 801 | } 802 | default: 803 | throw ValueError("Can't calculcate hash more complicated than array."); 804 | } 805 | } 806 | 807 | VeVariant &VeVariant::operator[](const VeVariant &v) 808 | { 809 | if (type != VeVariantType::Dict || !dict) 810 | throw ValueError("VeVariant is not a dict or the dict is null."); 811 | std::unordered_map &d = *dict; 812 | return d[v]; 813 | } 814 | 815 | VeVariant &VeVariant::get_dict_val(const VeVariant &key) 816 | { 817 | if (type != VeVariantType::Dict || !dict) 818 | throw ValueError("VeVariant is not a dict or the dict is null."); 819 | 820 | auto pos = dict->find(key); 821 | if (pos == dict->end()) 822 | throw ValueError("Key '" + key.as_text() + "' not found."); 823 | 824 | return pos->second; 825 | } 826 | 827 | 828 | -------------------------------------------------------------------------------- /src/vevariant.h: -------------------------------------------------------------------------------- 1 | #ifndef VEVARIANT_H 2 | #define VEVARIANT_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "vendor/json.hpp" 11 | 12 | // Explicitely documented in velib_python 13 | #define EMPTY_ARRAY_AS_NULL_VALUE_TYPE "i" 14 | #define VALID_EMPTY_ARRAY_VALUE_TYPE "u" 15 | 16 | namespace dbus_flashmq 17 | { 18 | 19 | enum class VeVariantType 20 | { 21 | Unknown, 22 | IntegerSigned16, 23 | IntegerSigned32, 24 | IntegerSigned64, 25 | IntegerUnsigned8, 26 | IntegerUnsigned16, 27 | IntegerUnsigned32, 28 | IntegerUnsigned64, 29 | String, 30 | Double, 31 | Boolean, 32 | Array, 33 | Dict 34 | }; 35 | 36 | class VeVariant; 37 | 38 | // TODO: finish? Or remove? 39 | class VeVariantArray 40 | { 41 | std::unique_ptr> arr; 42 | char contained_dbus_type = 0; 43 | 44 | public: 45 | VeVariantArray() = default; 46 | VeVariantArray(const VeVariantArray &other); 47 | VeVariantArray& operator=(const VeVariantArray &other); 48 | }; 49 | 50 | /** 51 | * @brief The VeVariant class is a little less uniony and prone to type confusion than a normal variant, and is recursive. 52 | */ 53 | class VeVariant 54 | { 55 | // TODO: put these unique pointers in separate class, so I can remove the custom assignment operator and such. 56 | //VeVariantArray arr2; 57 | std::unique_ptr> arr; 58 | std::unique_ptr> dict; 59 | 60 | std::string str; 61 | 62 | // Not using DBusBasicValues because a C union is not default constructed, has no operators, etc. 63 | uint8_t u8 = 0; 64 | dbus_int16_t i16 = 0; 65 | dbus_uint16_t u16 = 0; 66 | dbus_int32_t i32 = 0; 67 | dbus_uint32_t u32 = 0; 68 | dbus_bool_t bool_val = 0; // must be 32 bit in dbus 69 | dbus_int64_t i64 = 0; 70 | dbus_uint64_t u64 = 0; 71 | double d = 0.0; 72 | 73 | VeVariantType type = VeVariantType::Unknown; 74 | std::string contained_array_type_as_string; 75 | 76 | static std::unique_ptr> make_array(DBusMessageIter *iter); 77 | static std::unique_ptr> make_dict(DBusMessageIter *iter); 78 | 79 | public: 80 | VeVariant() = default; 81 | VeVariant(VeVariant &&other) = default; 82 | VeVariant(const VeVariant &other); 83 | VeVariant(DBusMessageIter *iter); 84 | VeVariant(const std::string &v); 85 | explicit VeVariant(const std::optional &v); 86 | VeVariant(const char *s); 87 | VeVariant(const nlohmann::json &j); 88 | explicit VeVariant(const std::optional b); 89 | std::string as_text() const; 90 | nlohmann::json as_json_value(bool mask=false) const; 91 | int as_int() const; 92 | int get_dbus_type() const; 93 | std::string get_dbus_type_as_string_flat() const; 94 | std::string get_dbus_type_as_string_recursive() const; 95 | std::string get_contained_type_as_string() const; 96 | void append_args_to_dbus_message(DBusMessageIter *iter) const; 97 | 98 | VeVariant& operator=(const VeVariant &other); 99 | bool operator==(const VeVariant &other) const; 100 | operator bool() const; 101 | VeVariantType get_type() const; 102 | std::size_t hash() const; 103 | VeVariant &operator[](const VeVariant &v); 104 | VeVariant &get_dict_val(const VeVariant &key); 105 | }; 106 | 107 | } 108 | 109 | namespace std { 110 | 111 | template <> 112 | struct hash 113 | { 114 | std::size_t operator()(const dbus_flashmq::VeVariant& k) const 115 | { 116 | return k.hash(); 117 | } 118 | }; 119 | 120 | } 121 | 122 | #endif // VEVARIANT_H 123 | -------------------------------------------------------------------------------- /vendor/flashmq_plugin.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of FlashMQ (https://www.flashmq.org). It defines the 3 | * plugin interface. 4 | * 5 | * This interface definition is public domain and you are encouraged 6 | * to copy it to your plugin project, for portability. Including 7 | * this file in your project does not make it a 'derivative work'. 8 | * 9 | * Compile like: gcc -fPIC -shared plugin.cpp -o plugin.so 10 | * 11 | * It's best practice to build your plugin with the same library versions of the 12 | * build of FlashMQ you're using. In practice, this means building on the OS 13 | * version you're running on. This also means using the AppImage build of FlashMQ 14 | * is not really compatible with plugins, because that includes older, and fixed, 15 | * versions of various libraries. 16 | * 17 | * For instance, if you use OpenSSL: by the time your plugin is loaded, FlashMQ will 18 | * have already dynamically linked OpenSSL. If you then try to call OpenSSL 19 | * functions, you'll run into ABI incompatibilities. 20 | */ 21 | 22 | #ifndef FLASHMQ_PLUGIN_H 23 | #define FLASHMQ_PLUGIN_H 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | #define FLASHMQ_PLUGIN_VERSION 4 35 | 36 | // Compatible with Mosquitto, for (auth) plugin compatability. 37 | #define LOG_NONE 0x00 38 | #define LOG_INFO 0x01 39 | #define LOG_NOTICE 0x02 40 | #define LOG_WARNING 0x04 41 | #define LOG_ERR 0x08 42 | #define LOG_ERROR 0x08 43 | #define LOG_DEBUG 0x10 44 | #define LOG_SUBSCRIBE 0x20 45 | #define LOG_UNSUBSCRIBE 0x40 46 | 47 | extern "C" 48 | { 49 | 50 | class Client; 51 | class Session; 52 | 53 | /** 54 | * @brief The AclAccess enum's numbers are compatible with Mosquitto's 'int access'. 55 | * 56 | * read = reading a publish published by someone else. 57 | * write = doing a publish. 58 | * subscribe = subscribing. 59 | */ 60 | enum class AclAccess 61 | { 62 | none = 0, 63 | read = 1, 64 | write = 2, 65 | subscribe = 4, 66 | register_will = 100 67 | }; 68 | 69 | /** 70 | * @brief The AuthResult enum's numbers are compatible with Mosquitto's auth result. 71 | * 72 | * async = defer the decision until you have the result from an async call, which can be submitted with flashmq_continue_async_authentication(). 73 | * 74 | * auth_continue = part of MQTT5 extended authentication, which can be a back-and-forth between server and client. 75 | * 76 | * success_without_retained_delivery = allow the subscription action, but don't try to give client the matching retained messages. This 77 | * can be used prevent load on the server. For instance, if there are many retained messages and clients subscribe to '#'. This value 78 | * is only valid for AclAccess::subscribe, and requires FlashMQ version 1.9.0 or newer. 79 | * 80 | * success_without_setting_retained = allow the write action, but don't set the retained message (if it was set to retain to begin 81 | * with). MQTT5 subscribers with 'retain as published' will still see the retain flag set. This value is only valid for 82 | * AclAccess::write and requires a FlashMQ version 1.16.0 or higher. 83 | * 84 | * success_but_drop_publish / success_but_drop = send success SUBACK or PUBACK back to client (if QoS) but don't process the packet. This 85 | * may be useful in combination with flashmq_publish_message() or flashmq_plugin_add_subscription() if you need to publish or subscribe 86 | * something new entirely (different topic(s) or payload for instance). This only works with AclAccess::write (FlashMQ 1.20.0 or higher) 87 | * and AclAccess::subscribe (FlashMQ 1.21.0 or higher). 88 | * 89 | * server_not_available = to be used as log-in result, for when you don't have auth data yet, for instance. MQTT3 and MQTT5 both support 90 | * sending 'ServerUnavailble' in their CONNACK, when this result is used. Requires FlashMQ 1.17.0 or newer. 91 | */ 92 | enum class AuthResult 93 | { 94 | success = 0, 95 | auth_method_not_supported = 10, 96 | acl_denied = 12, 97 | login_denied = 11, 98 | error = 13, 99 | server_not_available = 14, 100 | async = 50, 101 | success_without_retained_delivery = 51, 102 | success_without_setting_retained = 52, 103 | success_but_drop_publish = 53, 104 | success_but_drop = 53, 105 | auth_continue = -4 106 | }; 107 | 108 | enum class ExtendedAuthStage 109 | { 110 | None = 0, 111 | Auth = 10, 112 | Reauth = 20, 113 | Continue = 30 114 | }; 115 | 116 | /** 117 | * @brief flashmq_logf calls the internal logger of FlashMQ. The logger mutexes all access, so is thread-safe, and writes to disk 118 | * asynchronously, so it won't hold you up. 119 | * @param level is any of the levels defined above, starting with LOG_. 120 | * @param str 121 | * 122 | * FlashMQ makes no distinction between INFO and NOTICE. 123 | */ 124 | void flashmq_logf(int level, const char *str, ...); 125 | 126 | /** 127 | * @brief The ServerDisconnectReasons enum lists the possible values to initiate client disconnect with. 128 | * 129 | * This is a subset of all MQTT5 reason codes that are allowed in a disconnect packet, and that make sense for plugin use. 130 | */ 131 | enum class ServerDisconnectReasons 132 | { 133 | NormalDisconnect = 0, 134 | UnspecifiedError = 128, 135 | ProtocolError = 130, 136 | ImplementationSpecificError = 131, 137 | NotAuthorized = 135, 138 | ServerBusy = 137, 139 | MessageRateTooHigh = 150 140 | }; 141 | 142 | /** 143 | * @brief flashmq_plugin_remove_client queues a removal of a client in the proper thread, including session if required. It can be called by 144 | * plugin code (meaning this function does not need to be implemented). 145 | * @param session Can be obtained with flashmq_get_session_pointer(). 146 | * @param alsoSession also remove the session if it would otherwise remain. 147 | * @param reasonCode is only for MQTT5, because MQTT3 doesn't have server-initiated disconnect packets. 148 | * 149 | * Many clients will automatically reconnect, so you'll have to also remove permissions of the client in question, probably. 150 | * 151 | * Can be called from any thread: the action will be queued properly. 152 | * 153 | * [Function provided by FlashMQ; new version since plugin version 4] 154 | */ 155 | void flashmq_plugin_remove_client_v4(const std::weak_ptr &session, bool alsoSession, ServerDisconnectReasons reasonCode); 156 | 157 | /** 158 | * @brief flashmq_plugin_remove_subscription removes a client's subscription from the central store. It can be called by plugin code (meaning 159 | * this function does not need to be implemented). 160 | * @param session Can be obtained with flashmq_get_session_pointer(). 161 | * @param topicFilter Like 'one/two/three' or '$share/myshare/one/two/three'. 162 | * 163 | * It matches only literal filters. So removing '#' would only remove an active subscription on '#', not 'everything'. 164 | * 165 | * Will throw exceptions on certain errors. 166 | * 167 | * Can be called from any thread, because the global subscription store is mutexed. 168 | * 169 | * [Function provided by FlashMQ; new version since plugin version 4] 170 | */ 171 | void flashmq_plugin_remove_subscription_v4(const std::weak_ptr &session, const std::string &topicFilter); 172 | 173 | /** 174 | * @brief flashmq_plugin_add_subscription 175 | * @param session Can be obtained with flashmq_get_session_pointer(). 176 | * @param topicFilter Like 'one/two/three' or '$share/myshare/one/two/three'. 177 | * @return boolean True when session found and subscription actually added. 178 | * 179 | * Will throw exceptions on certain errors. 180 | * 181 | * Can be called from any thread, because the global subscription store is mutexed. 182 | * 183 | * [Function provided by FlashMQ] 184 | */ 185 | bool flashmq_plugin_add_subscription( 186 | const std::weak_ptr &session, const std::string &topicFilter, uint8_t qos, bool noLocal, bool retainAsPublished, 187 | const uint32_t subscriptionIdentifier); 188 | 189 | /** 190 | * @brief flashmq_continue_async_authentication is to continue/finish async authentication. 191 | * @param client 192 | * @param result 193 | * 194 | * When you've previously returned AuthResult::async in the authentication check, because you need to perform a network call for instance, 195 | * you can submit the final result back to FlashMQ with this function. The action will be queued in the proper thread. 196 | * 197 | * It uses a weak pointer to Client instead of client id, because clients are in limbo at this point, and a client id isn't necessarily 198 | * correct (anymore). The login functions also give this weak pointer so you can store it with the async operation, to be used again later for 199 | * a call to this function. 200 | * 201 | * [Function provided by FlashMQ] 202 | */ 203 | void flashmq_continue_async_authentication(const std::weak_ptr &client, AuthResult result, const std::string &authMethod, const std::string &returnData); 204 | 205 | /** 206 | * @brief flashmq_publish_message Publish a message from the plugin. 207 | * 208 | * Can be called from any thread. 209 | * 210 | * [Function provided by FlashMQ] 211 | */ 212 | void flashmq_publish_message(const std::string &topic, const uint8_t qos, const bool retain, const std::string &payload, uint32_t expiryInterval=0, 213 | const std::vector> *userProperties = nullptr, 214 | const std::string *responseTopic=nullptr, const std::string *correlationData=nullptr, const std::string *contentType=nullptr); 215 | 216 | /** 217 | * @brief The FlashMQSockAddr class is a simple helper for the C-style polymorfism of all the sockaddr structs. 218 | * 219 | * Primary struct is an sockaddr_in6 because that is the biggest one. 220 | */ 221 | class FlashMQSockAddr 222 | { 223 | struct sockaddr_in6 addr_in6; 224 | 225 | public: 226 | struct sockaddr *getAddr(); 227 | static constexpr int getLen(); 228 | }; 229 | 230 | /** 231 | * @brief flashmq_get_client_address 232 | * @param client A client pointer as provided by 'flashmq_plugin_login_check'. 233 | * @param text If not nullptr, will be assigned the address in text form, like 192.168.1.1 or "2001:0db8:85a3:0000:1319:8a2e:0370:7344". 234 | * @param addr If not nullptr, will provide a sockaddr struct, for low level operations. 235 | * 236 | * The text and addr can be pointers to local variables in the calling context. 237 | * 238 | * [Function provided by FlashMQ] 239 | */ 240 | void flashmq_get_client_address(const std::weak_ptr &client, std::string *text, FlashMQSockAddr *addr); 241 | 242 | /** 243 | * @brief flashmq_get_session_pointer Get reference counted weak pointer of a session. 244 | * @param clientid The client ID of the session you're retrieving. 245 | * @param username The username is used for verification, as a security measure. 246 | * @param sessionOut The result (has to be an output parameter because we can't return it). 247 | * 248 | * The weak pointer will acurately reflect the original session. If it has been replaced with a new one with 249 | * the same client ID, this weak pointer will be 'expired'. 250 | * 251 | * [Function provided by FlashMQ] 252 | */ 253 | void flashmq_get_session_pointer(const std::string &clientid, const std::string &username, std::weak_ptr &sessionOut); 254 | 255 | /** 256 | * @brief flashmq_get_client_pointer Get reference counted client pointer of a session. 257 | * @param clientOut The result (has to be an output parameter because we can't return it). 258 | * 259 | * Can we used to feed to other functions, or to check if the client is still online. 260 | * 261 | * [Function provided by FlashMQ] 262 | */ 263 | void flashmq_get_client_pointer(const std::weak_ptr &session, std::weak_ptr &clientOut); 264 | 265 | /** 266 | * @brief Allows async operation of outgoing connections you may need to make. It adds the file descriptor to 267 | * the epoll listener. 268 | * @param fd 269 | * @param events epoll events, typically EPOLLIN (ready read) and EPOLLOUT (ready write). Should be or'ed together, 270 | * like 'EPOLLOUT | EPOLLIN'. See 'man epoll'. 271 | * @param p weak pointer. Can be a weak copy of a shared pointer with proper type. Like p = std::make_share(). 272 | * You'll get it back in 'flashmq_plugin_poll_event_received()'. Use is optional. For libcurl multi socket, 273 | * you don't need it. 274 | * 275 | * You can do this once you have a connection with something external. 276 | * 277 | * You can also call it again with different events, in which case it will modify the existing entry. If you specify 278 | * a non-expired p, it will overwrite the original data associated with the fd. 279 | * 280 | * Will throw exceptions on error, so be sure to handle them. 281 | * 282 | * [Function provided by FlashMQ] 283 | */ 284 | void flashmq_poll_add_fd(int fd, uint32_t events, const std::weak_ptr &p); 285 | 286 | /** 287 | * @brief Remove the fd from the event polling system. 288 | * @param fd 289 | * 290 | * Closing a socket will also remove it from the epoll system, but if you don't call this function on close, you may get stray 291 | * events once the fd number is reused. There is protection against it, but you may end up with unpredictable behavior. 292 | * 293 | * Will throw exceptions on error, so be sure to handle them. 294 | * 295 | * [Function provided by FlashMQ] 296 | */ 297 | void flashmq_poll_remove_fd(uint32_t fd); 298 | 299 | /** 300 | * @brief Is called when the socket watched by 'flashmq_poll_add_fd()' has an event. 301 | * @param thread_data is memory allocated in flashmq_plugin_allocate_thread_memory(). 302 | * @param fd 303 | * @param events contains the events as a bit flags. See 'man epoll'. 304 | * @param p can be made back into your type with 'std::shared_ptr sp = std::static_pointer_cast(b.lock())'. 305 | * This allows you to properly lend pointers to the event system that you can actually check for expiration. 306 | * 307 | * [Can optionally be implemented by plugin] 308 | */ 309 | void flashmq_plugin_poll_event_received(void *thread_data, int fd, uint32_t events, const std::weak_ptr &p); 310 | 311 | /** 312 | * @brief call a task later, once. 313 | * @param f Function, that can be created with std::bind, for instance. 314 | * @param delay_in_ms 315 | * @return id of the timer, which can be used to remove it. 316 | * 317 | * The task queue is local to the thread, including the id returned. 318 | * 319 | * This can be necessary for asynchronous interfaces, like libcurl. 320 | * 321 | * Can throw an exceptions. 322 | * 323 | * [Function provided by FlashMQ] 324 | */ 325 | uint32_t flashmq_add_task(std::function f, uint32_t delay_in_ms); 326 | 327 | /** 328 | * @brief Remove a task with id as given by 'flashmq_add_task()'. 329 | * @param id 330 | * 331 | * The task queue is local to the thread, including the id returned. 332 | * 333 | * [Function provided by FlashMQ] 334 | */ 335 | void flashmq_remove_task(uint32_t id); 336 | 337 | /** 338 | * @brief flashmq_plugin_version must return FLASHMQ_PLUGIN_VERSION. 339 | * @return FLASHMQ_PLUGIN_VERSION. 340 | * 341 | * [Must be implemented by plugin] 342 | */ 343 | int flashmq_plugin_version(); 344 | 345 | /** 346 | * @brief flashmq_plugin_main_init is called once before the event loops start. 347 | * @param plugin_opts 348 | * 349 | * [Can optionally be implemented by plugin] 350 | */ 351 | void flashmq_plugin_main_init(std::unordered_map &plugin_opts); 352 | 353 | /** 354 | * @brief flashmq_plugin_main_deinit is the complementary pair of flashmq_plugin_main_init(). It's called after the threads have stopped. 355 | * @param plugin_opts 356 | */ 357 | void flashmq_plugin_main_deinit(std::unordered_map &plugin_opts); 358 | 359 | /** 360 | * @brief flashmq_plugin_allocate_thread_memory is called once by each thread. Never again. 361 | * @param thread_data. Create a memory structure and assign it to *thread_data. 362 | * @param plugin_opts. Map of flashmq_plugin_opt_* from the config file. 363 | * 364 | * Only allocate the plugin's memory here, or other things that you really only have to do once. Don't open connections, etc. That's 365 | * because the reload mechanism doesn't call this function. 366 | * 367 | * Because of the multi-core design of FlashMQ, you should treat each thread as its own domain with its own data. You can use static 368 | * variables for global scope if you must, or even create threads, but do provide proper locking where necessary. 369 | * 370 | * You can throw exceptions on errors. 371 | * 372 | * [Must be implemented by plugin] 373 | */ 374 | void flashmq_plugin_allocate_thread_memory(void **thread_data, std::unordered_map &plugin_opts); 375 | 376 | /** 377 | * @brief flashmq_plugin_deallocate_thread_memory is called once by each thread. Never again. 378 | * @param thread_data. Delete this memory. 379 | * @param plugin_opts. Map of flashmq_plugin_opt_* from the config file. 380 | * 381 | * You can throw exceptions on errors. 382 | * 383 | * [Must be implemented by plugin] 384 | */ 385 | void flashmq_plugin_deallocate_thread_memory(void *thread_data, std::unordered_map &plugin_opts); 386 | 387 | /** 388 | * @brief flashmq_plugin_init is called on thread start and config reload. It is the main place to initialize the plugin. 389 | * @param thread_data is memory allocated in flashmq_plugin_allocate_thread_memory(). 390 | * @param plugin_opts. Map of flashmq_plugin_opt_* from the config file. 391 | * @param reloading. 392 | * 393 | * The best approach to state keeping is doing everything per thread. You can initialize connections to database servers, load encryption keys, 394 | * create maps, etc. However, remember that for instance with libcurl, initing a 'multi handle' is best not done here, or at least not done 395 | * AGAIN on reload, because it will distrupt your ongoing transfers. Memory structures that you really only need to init once are best 396 | * done in 'flashmq_plugin_allocate_thread_memory()' or even 'flashmq_plugin_main_init()'. 397 | * 398 | * Keep in mind that libraries you use may not be thread safe (by default). Sometimes they use global scope in treacherous ways. As a random 399 | * example: Qt's QSqlDatabase needs a unique name for each connection, otherwise it is not thread safe and will crash. It will also hide away 400 | * libmysqlclient's requirement to do a global one-time init, that would be best done in 'flashmq_plugin_main_init()'. 401 | * 402 | * There is the option to set 'plugin_serialize_init true' in the config file, which allows some mitigation in 403 | * case you run into problems. 404 | * 405 | * You can throw exceptions on errors. 406 | * 407 | * [Must be implemented by plugin] 408 | */ 409 | void flashmq_plugin_init(void *thread_data, std::unordered_map &plugin_opts, bool reloading); 410 | 411 | /** 412 | * @brief flashmq_plugin_deinit is called on thread stop and config reload. It is the precursor to initializing. 413 | * @param thread_data is memory allocated in flashmq_plugin_allocate_thread_memory(). 414 | * @param plugin_opts. Map of flashmq_plugin_opt_* from the config file. 415 | * @param reloading 416 | * 417 | * You can throw exceptions on errors. 418 | * 419 | * [Must be implemented by plugin] 420 | */ 421 | void flashmq_plugin_deinit(void *thread_data, std::unordered_map &plugin_opts, bool reloading); 422 | 423 | /** 424 | * @brief flashmq_plugin_periodic is called every x seconds as defined in the config file. 425 | * @param thread_data is memory allocated in flashmq_plugin_allocate_thread_memory(). 426 | * 427 | * You may need to periodically refresh data from a database, post stats, etc. You can do that from here. It's queued 428 | * in each thread at the same time, so you can perform somewhat synchronized events in all threads. 429 | * 430 | * Note that it's executed in the event loop, so it blocks the thread if you block here. If you need asynchronous operation, 431 | * you can make threads yourself. Be sure to synchronize data access properly in that case. 432 | * 433 | * The setting plugin_timer_period sets this interval in seconds. 434 | * 435 | * You can throw exceptions on errors. 436 | * 437 | * [Can optionally be implemented by plugin] 438 | */ 439 | void flashmq_plugin_periodic_event(void *thread_data); 440 | 441 | /** 442 | * @brief flashmq_plugin_alter_subscription can optionally be implemented if you want to be able to change incoming subscriptions. 443 | * @param thread_data 444 | * @param clientid 445 | * @param topic non-const reference which can be changed. 446 | * @param subtopics 447 | * @param qos non-const reference which can be changed. 448 | * @param userProperties 449 | * @return boolean indicating whether the subscription was changed. Not returning the truth here results in unpredictable behavior. 450 | * 451 | * In case of shared subscriptions, you will see the original subscription path, like '$share/myshare/battery/voltage'. You have the 452 | * chance to change every aspect of it, like make it non-shared. 453 | * 454 | * [Can optionally be implemented by plugin] 455 | */ 456 | bool flashmq_plugin_alter_subscription(void *thread_data, const std::string &clientid, std::string &topic, const std::vector &subtopics, 457 | uint8_t &qos, const std::vector> *userProperties); 458 | 459 | /** 460 | * @brief flashmq_plugin_alter_publish allows changing of the non-const arguments. 461 | * @param thread_data is memory allocated in flashmq_plugin_allocate_thread_memory(). 462 | * @return boolean indicating whether the packet was changed. It saves FlashMQ from having to do a full compare. Not returning the truth here 463 | * results in unpredictable behavior. Note: this only applies to the topic, because FlashMQ has to know whether to resplit 464 | * the topic string. You can change other flags and still return false. 465 | * 466 | * Be aware that changing publishes may incur a (slight) reduction in performance. 467 | * 468 | * [Can optionally be implemented by plugin] 469 | */ 470 | bool flashmq_plugin_alter_publish(void *thread_data, const std::string &clientid, std::string &topic, const std::vector &subtopics, 471 | std::string_view payload, uint8_t &qos, bool &retain, const std::optional &correlationData, 472 | const std::optional &responseTopic, std::vector> *userProperties); 473 | 474 | /** 475 | * @brief flashmq_plugin_login_check is called on login of a client. 476 | * @param thread_data is memory allocated in flashmq_plugin_allocate_thread_memory(). 477 | * @param username 478 | * @param password 479 | * @param client Example use is for storing in a async operation and passing to flashmq_continue_async_authentication. 480 | * @return 481 | * 482 | * You could throw exceptions here, but that will be slow and pointless. It will just get converted into AuthResult::error, 483 | * because there's nothing else to do: the state of FlashMQ won't change. 484 | * 485 | * Note that there is a setting 'plugin_serialize_auth_checks'. Use only as a last resort if your plugin is not 486 | * thread-safe. It will negate much of FlashMQ's multi-core model. 487 | * 488 | * The AuthResult::async can be used if your auth check causes blocking IO (like network). You can save the weak pointer to the client 489 | * and do the auth in a thread or any kind of async way. FlashMQ's event loop will then continue. You can call flashmq_continue_async_authentication 490 | * later with the result. 491 | * 492 | * [Must be implemented by plugin] 493 | */ 494 | AuthResult flashmq_plugin_login_check(void *thread_data, const std::string &clientid, const std::string &username, const std::string &password, 495 | const std::vector> *userProperties, const std::weak_ptr &client); 496 | 497 | /** 498 | * @brief flashmq_plugin_client_disconnected Called when clients disconnect or their keep-alive expire. 499 | * @param thread_data 500 | * @param clientid 501 | * 502 | * Is only called for authenticated clients, to avoid spoofing. 503 | * 504 | * [Can optionally be implemented by plugin] 505 | */ 506 | void flashmq_plugin_client_disconnected(void *thread_data, const std::string &clientid); 507 | 508 | /** 509 | * @brief flashmq_plugin_on_unsubscribe is called after unsubscribe. Unsubscribe actions can't be manipulated or blocked. 510 | * @param topic Does not contain the share name. 511 | * @param subtopics Does not contain the share name. 512 | * 513 | * [Can optionally be implemented by plugin] 514 | */ 515 | void flashmq_plugin_on_unsubscribe(void *thread_data, const std::weak_ptr &session, const std::string &clientid, 516 | const std::string &username, const std::string &topic, const std::vector &subtopics, 517 | const std::string &shareName, const std::vector> *userProperties); 518 | 519 | /** 520 | * @brief flashmq_plugin_acl_check is called on publish, deliver and subscribe. 521 | * @param thread_data is memory allocated in flashmq_plugin_allocate_thread_memory(). 522 | * @param shareName The shared subscription name in a filter like '$share/my_share_name/one/two'. Is only present on AclAccess::subscribe. 523 | * @return 524 | * 525 | * You could throw exceptions here, but that will be slow and pointless. It will just get converted into AuthResult::error, 526 | * because there's nothing else to do: the state of FlashMQ won't change. 527 | * 528 | * Controlling subscribe access can have several benefits. For instance, you may want to avoid subscriptions that cause 529 | * a lot of server load. If clients pester you with many subscriptions like '+/+/+/+/+/+/+/+/+/', that causes a lot 530 | * of tree walking. Similarly, if all clients subscribe to '#' because it's easy, every single message passing through 531 | * the server will have to be ACL checked for every subscriber. 532 | * 533 | * Note that only MQTT 3.1.1 or higher has a 'failed' return code for subscribing, so older clients will see a normal 534 | * ack and won't know it failed. 535 | * 536 | * Note that there is a setting 'plugin_serialize_auth_checks'. Use only as a last resort if your plugin is not 537 | * thread-safe. It will negate much of FlashMQ's multi-core model. 538 | * 539 | * When the 'access' is 'subscribe' and it's a shared subscription (like '$share/myshare/one/two/three'), you only get 540 | * the effective topic filter (like 'one/two/three'). However, since plugin version 4, there is the argument 'shareName' for that. 541 | * 542 | * [Must be implemented by plugin] 543 | */ 544 | AuthResult flashmq_plugin_acl_check(void *thread_data, const AclAccess access, const std::string &clientid, const std::string &username, 545 | const std::string &topic, const std::vector &subtopics, const std::string &shareName, 546 | std::string_view payload, const uint8_t qos, const bool retain, 547 | const std::optional &correlationData, const std::optional &responseTopic, 548 | const std::vector> *userProperties); 549 | 550 | /** 551 | * @brief flashmq_plugin_extended_auth can be used to implement MQTT 5 extended auth. This is optional. 552 | * @param thread_data is the memory you allocated in flashmq_plugin_allocate_thread_memory. 553 | * @param clientid 554 | * @param stage 555 | * @param authMethod 556 | * @param authData 557 | * @param userProperties are optional (and are nullptr in that case) 558 | * @param returnData is a non-const string, that you can set to include data back to the client in an AUTH packet. 559 | * @param username is a non-const string. You can set it, which will then apply to ACL checking and show in the logs. 560 | * @param client Use this for AuthResult::async. See flashmq_plugin_login_check(). 561 | * @return an AuthResult enum class value 562 | * 563 | * [Can optionally be implemented by plugin] 564 | */ 565 | AuthResult flashmq_plugin_extended_auth(void *thread_data, const std::string &clientid, ExtendedAuthStage stage, const std::string &authMethod, 566 | const std::string &authData, const std::vector> *userProperties, std::string &returnData, 567 | std::string &username, const std::weak_ptr &client); 568 | 569 | } 570 | 571 | #endif // FLASHMQ_PLUGIN_H 572 | -------------------------------------------------------------------------------- /venus-ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIECTCCAvGgAwIBAgIJAM+t3iC8ybEHMA0GCSqGSIb3DQEBCwUAMIGZMQswCQYD 3 | VQQGEwJOTDESMBAGA1UECAwJR3JvbmluZ2VuMRIwEAYDVQQHDAlHcm9uaW5nZW4x 4 | HDAaBgNVBAoME1ZpY3Ryb24gRW5lcmd5IEIuVi4xIzAhBgNVBAsMGkNDR1ggQ2Vy 5 | dGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYJKoZIhvcNAQkBFhBzeXNhZG1pbkB5dGVj 6 | Lm5sMCAXDTE0MDkxNzExNTQxOVoYDzIxMTQwODI0MTE1NDE5WjCBmTELMAkGA1UE 7 | BhMCTkwxEjAQBgNVBAgMCUdyb25pbmdlbjESMBAGA1UEBwwJR3JvbmluZ2VuMRww 8 | GgYDVQQKDBNWaWN0cm9uIEVuZXJneSBCLlYuMSMwIQYDVQQLDBpDQ0dYIENlcnRp 9 | ZmljYXRlIEF1dGhvcml0eTEfMB0GCSqGSIb3DQEJARYQc3lzYWRtaW5AeXRlYy5u 10 | bDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKVdbAUAElbX+Sh0FATX 11 | yhlJ6zqYMHbqCXawgsOe09zHynDCT4GTXuSuoH2kR/1jE8zvWNLHORXa/eahzWJP 12 | V4WpXuYsFEyU3r8hxA6y+SR06IT7WHdfN6LXN+qt5KLQbmQLxeb1zElMKW4io/WE 13 | N+SWpo5dklXAS6vnq+VRTNwRYnPOUIXKduhvTQp6hEHnLBjYC/Ot8SkC8KtL88cW 14 | pH6d7UmeW3333/vNMEMOTLWlOWrR30P6R+gTjbvzasaB6tlcYqW+jO1YDlBwhSEV 15 | 4As4ziQysuy4qvm41KY/o4Q6P6npsh8MaZuRmi/UTxU2DHAbs/on7qaRi6IkVgvg 16 | o6kCAwEAAaNQME4wHQYDVR0OBBYEFPjmM5NYXMw7Wc/TgbLtwPnMAfewMB8GA1Ud 17 | IwQYMBaAFPjmM5NYXMw7Wc/TgbLtwPnMAfewMAwGA1UdEwQFMAMBAf8wDQYJKoZI 18 | hvcNAQELBQADggEBAEFTeGcmxzzXJIfgUrfKLki+hi2mR9g7qomvw6IB1JQHefIw 19 | iKXe14gdp0ytjYL6QoTeEbS2A8VI2FvSbusAzn0JqXdZI+Gwt/CuH0XH40QHpaQ5 20 | UAB5d7EGvbB2op7AA/IyR5TwF/cDb1fRbTaTmwDOIo3kuFGEyNCc+PFrN2MvtPHn 21 | hHH7fo7joY7mUKdP573bJXFsLwZxlqiycJreroLPFqYwgChaMTStQ71rP5i1eGtg 22 | ealQ7kPVtlHmX89tCkfkK77ojm48qgl4gwsI01SikstaPP9fr4ck+U/qIKhSg+Bg 23 | nc9OImY9ubQxe+/GQP4KFme2PPqthEWys7ut2HM= 24 | -----END CERTIFICATE----- 25 | --------------------------------------------------------------------------------