├── .flake8 ├── .gitattributes ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dbus-mqtt-temperature ├── config.sample.ini ├── dbus-mqtt-temperature.py ├── ext │ ├── paho │ │ ├── __init__.py │ │ └── mqtt │ │ │ ├── __init__.py │ │ │ ├── client.py │ │ │ ├── enums.py │ │ │ ├── matcher.py │ │ │ ├── packettypes.py │ │ │ ├── properties.py │ │ │ ├── publish.py │ │ │ ├── py.typed │ │ │ ├── reasoncodes.py │ │ │ ├── subscribe.py │ │ │ └── subscribeoptions.py │ └── velib_python │ │ ├── LICENSE │ │ ├── README.md │ │ ├── ve_utils.py │ │ └── vedbus.py ├── install.sh ├── restart.sh ├── service │ ├── log │ │ └── run │ └── run └── uninstall.sh ├── download.sh ├── pyproject.toml └── screenshots ├── temperature_device-list.png ├── temperature_device-list_mqtt-temperature-1.png ├── temperature_device-list_mqtt-temperature-2.png └── temperature_pages_guimods.png /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 216 3 | exclude = 4 | ./dbus-mqtt-temperature/ext 5 | extend-ignore: 6 | # E203 whitespace before ':' conflicts with black code formatting. Will be ignored in flake8 7 | E203 8 | # E402 module level import not at top of file 9 | E402 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behaviour, in case users don't have core.autocrlf set. 2 | text eol=lf 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.ini text 7 | *.md text 8 | *.py text 9 | *.sh text 10 | run text 11 | 12 | # Denote all files that are truly binary and should not be modified. 13 | *.gif binary 14 | *.jpg binary 15 | *.png binary 16 | *.zip binary -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*/config.ini 2 | /data 3 | /logs 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.extraPaths": [ 3 | "./dbus-mqtt-temperature/ext/velib_python" 4 | ], 5 | "[python]": { "editor.formatOnSave": true } 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.0.8-dev 4 | * Changed: Fix restart issue 5 | 6 | ## 0.0.7 7 | ⚠️ This version is required for Venus OS v3.60~27 or later, but it is also compatible with older versions. 8 | * Added: paho-mqtt module to driver 9 | 10 | ## v0.0.6 11 | * Changed: Allow to use customized JSON property names by @hoone123 12 | 13 | ## v0.0.5 14 | * Added: MQTT message can now also be only a float number 15 | * Added: New temperature types room, outdoor, waterheater and freezer 16 | * Changed: Broker port missing on reconnect 17 | * Changed: Fixed service not starting sometimes 18 | 19 | ## v0.0.4 20 | * Changed: Add VRM ID to MQTT client name 21 | * Changed: Fix registration to dbus https://github.com/victronenergy/velib_python/commit/494f9aef38f46d6cfcddd8b1242336a0a3a79563 22 | 23 | ## v0.0.3 24 | * Changed: Fixed problems when timeout was set to `0`. 25 | 26 | ## v0.0.2 27 | * Added: Timeout on driver startup. Prevents problems, if the MQTT broker is not reachable on driver startup 28 | 29 | ## v0.0.1 30 | Initial release 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Manuel 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-temperature - Emulates a temperature sensor from MQTT data 2 | 3 | GitHub repository: [mr-manuel/venus-os_dbus-mqtt-temperature](https://github.com/mr-manuel/venus-os_dbus-mqtt-temperature) 4 | 5 | ## Index 6 | 7 | 1. [Disclaimer](#disclaimer) 8 | 1. [Supporting/Sponsoring this project](#supportingsponsoring-this-project) 9 | 1. [Purpose](#purpose) 10 | 1. [Config](#config) 11 | 1. [JSON structure](#json-structure) 12 | 1. [Install / Update](#install--update) 13 | 1. [Uninstall](#uninstall) 14 | 1. [Restart](#restart) 15 | 1. [Debugging](#debugging) 16 | 1. [Compatibility](#compatibility) 17 | 1. [Screenshots](#screenshots) 18 | 19 | 20 | ## Disclaimer 21 | 22 | I wrote this script for myself. I'm not responsible, if you damage something using my script. 23 | 24 | 25 | ## Supporting/Sponsoring this project 26 | 27 | You like the project and you want to support me? 28 | 29 | [](https://www.paypal.com/donate/?hosted_button_id=3NEVZBDM5KABW) 30 | 31 | 32 | ## Purpose 33 | 34 | The script emulates a temperature sensor in Venus OS. It gets the MQTT data from a subscribed topic and publishes the information on the dbus as the service `com.victronenergy.temperature.mqtt_temperature` with the VRM instance `100`. 35 | 36 | 37 | ## Config 38 | 39 | Copy or rename the `config.sample.ini` to `config.ini` in the `dbus-mqtt-temperature` folder and change it as you need it. 40 | 41 | 42 | ## JSON structure 43 | 44 |
Minimum required 45 | 46 | ```json 47 | { 48 | "temperature": 22.0 49 | } 50 | ``` 51 | 52 | OR 53 | 54 | ```json 55 | 22.0 56 | ``` 57 | 58 | 59 |
60 | 61 |
Full 62 | 63 | ```json 64 | { 65 | "temperature": 22.0, 66 | "humidity": 62.927, 67 | "pressure": 102.104 68 | } 69 | ``` 70 |
71 | 72 | You can customize the names used in the MQTT message in the config. 73 | 74 | ## Install / Update 75 | 76 | 1. Login to your Venus OS device via SSH. See [Venus OS:Root Access](https://www.victronenergy.com/live/ccgx:root_access#root_access) for more details. 77 | 78 | 2. Execute this commands to download and copy the files: 79 | 80 | ```bash 81 | wget -O /tmp/download_dbus-mqtt-temperature.sh https://raw.githubusercontent.com/mr-manuel/venus-os_dbus-mqtt-temperature/master/download.sh 82 | 83 | bash /tmp/download_dbus-mqtt-temperature.sh 84 | ``` 85 | 86 | 3. Select the version you want to install. 87 | 88 | 4. Press enter for a single instance. For multiple instances, enter a number and press enter. 89 | 90 | Example: 91 | 92 | - Pressing enter or entering `1` will install the driver to `/data/etc/dbus-mqtt-temperature`. 93 | - Entering `2` will install the driver to `/data/etc/dbus-mqtt-temperature-2`. 94 | 95 | ### Extra steps for your first installation 96 | 97 | 5. Edit the config file to fit your needs. The correct command for your installation is shown after the installation. 98 | 99 | - If you pressed enter or entered `1` during installation: 100 | ```bash 101 | nano /data/etc/dbus-mqtt-temperature/config.ini 102 | ``` 103 | 104 | - If you entered `2` during installation: 105 | ```bash 106 | nano /data/etc/dbus-mqtt-temperature-2/config.ini 107 | ``` 108 | 109 | 6. Install the driver as a service. The correct command for your installation is shown after the installation. 110 | 111 | - If you pressed enter or entered `1` during installation: 112 | ```bash 113 | bash /data/etc/dbus-mqtt-temperature/install.sh 114 | ``` 115 | 116 | - If you entered `2` during installation: 117 | ```bash 118 | bash /data/etc/dbus-mqtt-temperature-2/install.sh 119 | ``` 120 | 121 | The daemon-tools should start this service automatically within seconds. 122 | 123 | ## Uninstall 124 | 125 | ⚠️ If you have multiple instances, ensure you choose the correct one. For example: 126 | 127 | - To uninstall the default instance: 128 | ```bash 129 | bash /data/etc/dbus-mqtt-temperature/uninstall.sh 130 | ``` 131 | 132 | - To uninstall the second instance: 133 | ```bash 134 | bash /data/etc/dbus-mqtt-temperature-2/uninstall.sh 135 | ``` 136 | 137 | ## Restart 138 | 139 | ⚠️ If you have multiple instances, ensure you choose the correct one. For example: 140 | 141 | - To restart the default instance: 142 | ```bash 143 | bash /data/etc/dbus-mqtt-temperature/restart.sh 144 | ``` 145 | 146 | - To restart the second instance: 147 | ```bash 148 | bash /data/etc/dbus-mqtt-temperature-2/restart.sh 149 | ``` 150 | 151 | ## Debugging 152 | 153 | ⚠️ If you have multiple instances, ensure you choose the correct one. 154 | 155 | - To check the logs of the default instance: 156 | ```bash 157 | tail -n 100 -F /data/log/dbus-mqtt-temperature/current | tai64nlocal 158 | ``` 159 | 160 | - To check the logs of the second instance: 161 | ```bash 162 | tail -n 100 -F /data/log/dbus-mqtt-temperature-2/current | tai64nlocal 163 | ``` 164 | 165 | The service status can be checked with svstat `svstat /service/dbus-mqtt-temperature` 166 | 167 | This will output somethink like `/service/dbus-mqtt-temperature: up (pid 5845) 185 seconds` 168 | 169 | If the seconds are under 5 then the service crashes and gets restarted all the time. If you do not see anything in the logs you can increase the log level in `/data/etc/dbus-mqtt-temperature/dbus-mqtt-temperature.py` by changing `level=logging.WARNING` to `level=logging.INFO` or `level=logging.DEBUG` 170 | 171 | If the script stops with the message `dbus.exceptions.NameExistsException: Bus name already exists: com.victronenergy.temperatureinverter.mqtt_temperature"` it means that the service is still running or another service is using that bus name. 172 | 173 | ## Compatibility 174 | 175 | This software supports the latest three stable versions of Venus OS. It may also work on older versions, but this is not guaranteed. 176 | 177 | ## Screenshots 178 | 179 |
MQTT Temperature 180 | 181 | ![Temperature - device list](/screenshots/temperature_device-list.png) 182 | ![Temperature - device list - mqtt temperature 1](/screenshots/temperature_device-list_mqtt-temperature-1.png) 183 | ![Temperature - device list - mqtt temperature 2](/screenshots/temperature_device-list_mqtt-temperature-2.png) 184 | 185 | ## GuiMods 186 | 187 | ![Temperature - pages](/screenshots/temperature_pages_guimods.png) 188 | 189 |
190 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/config.sample.ini: -------------------------------------------------------------------------------- 1 | ; CONFIG FILE 2 | ; GitHub reporitory: https://github.com/mr-manuel/venus-os_dbus-mqtt-temperature 3 | ; remove semicolon ; to enable desired setting 4 | 5 | [DEFAULT] 6 | ; Set logging level 7 | ; ERROR = shows errors only 8 | ; WARNING = shows ERROR and warnings 9 | ; INFO = shows WARNING and running functions 10 | ; DEBUG = shows INFO and data/values 11 | ; default: WARNING 12 | logging = WARNING 13 | 14 | ; Device name 15 | ; default: MQTT Temperature 16 | device_name = MQTT Temperature 17 | 18 | ; Device VRM instance 19 | ; default: 100 20 | device_instance = 100 21 | 22 | ; Specify after how many seconds the driver should exit (disconnect), if no new MQTT message was received 23 | ; default: 60 24 | ; value to disable timeout: 0 25 | timeout = 60 26 | 27 | ; Temperature type 28 | ; 0 = battery 29 | ; 1 = fridge 30 | ; 2 = generic 31 | ; 3 = room 32 | ; 4 = outdoor 33 | ; 5 = waterheater 34 | ; 6 = freezer 35 | ; default: 2 36 | type = 2 37 | 38 | 39 | [MQTT] 40 | ; IP addess or FQDN from MQTT server 41 | broker_address = IP_ADDR_OR_FQDN 42 | 43 | ; Port of the MQTT server 44 | ; default plaintext: 1883 45 | ; default TLS port: 8883 46 | broker_port = 1883 47 | 48 | ; Enables TLS 49 | ; 0 = Disabled 50 | ; 1 = Enabled 51 | ;tls_enabled = 1 52 | 53 | ; Absolute path to the Certificate Authority certificate file that is to be treated as trusted by this client 54 | ;tls_path_to_ca = /data/keys/mosquitto.crt 55 | 56 | ; Disables verification of the server hostname in the server certificate 57 | ; 0 = Disabled 58 | ; 1 = Enabled 59 | ;tls_insecure = 1 60 | 61 | ; Username used for connection 62 | ;username = myuser 63 | 64 | ; Password used for connection 65 | ;password = mypassword 66 | 67 | ; Topic where the temperature data as JSON string is published 68 | ; minimum required JSON payload: {"temperature": 22.0 } 69 | topic = N/aa00aa00aa00/battery/1/System/Temperature2 70 | 71 | ; Temperature 72 | ; Set the name for temperature in the MQTT message 73 | ; default: temperature 74 | temperature_name = temperature 75 | 76 | ; Humidity 77 | ; Set the name for humidity in the MQTT message 78 | ; default: humidity 79 | humidity_name = humidity 80 | 81 | ; Pressure 82 | ; Set the name for pressure in the MQTT message 83 | ; default: pressure 84 | pressure_name = pressure -------------------------------------------------------------------------------- /dbus-mqtt-temperature/dbus-mqtt-temperature.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from gi.repository import GLib # pyright: ignore[reportMissingImports] 4 | import platform 5 | import logging 6 | import sys 7 | import os 8 | from time import sleep, time 9 | import json 10 | import configparser # for config/ini file 11 | import _thread 12 | import re 13 | 14 | # import external packages 15 | sys.path.insert(1, os.path.join(os.path.dirname(__file__), "ext")) 16 | import paho.mqtt.client as mqtt 17 | 18 | # import Victron Energy packages 19 | sys.path.insert(1, os.path.join(os.path.dirname(__file__), "ext", "velib_python")) 20 | from vedbus import VeDbusService # noqa: E402 21 | from ve_utils import get_vrm_portal_id # noqa: E402 22 | 23 | 24 | # get values from config.ini file 25 | try: 26 | config_file = (os.path.dirname(os.path.realpath(__file__))) + "/config.ini" 27 | if os.path.exists(config_file): 28 | config = configparser.ConfigParser() 29 | config.read(config_file) 30 | if config["MQTT"]["broker_address"] == "IP_ADDR_OR_FQDN": 31 | print('ERROR:The "config.ini" is using invalid default values like IP_ADDR_OR_FQDN. The driver restarts in 60 seconds.') 32 | sleep(60) 33 | sys.exit() 34 | else: 35 | print('ERROR:The "' + config_file + '" is not found. Did you copy or rename the "config.sample.ini" to "config.ini"? The driver restarts in 60 seconds.') 36 | sleep(60) 37 | sys.exit() 38 | 39 | except Exception: 40 | exception_type, exception_object, exception_traceback = sys.exc_info() 41 | file = exception_traceback.tb_frame.f_code.co_filename 42 | line = exception_traceback.tb_lineno 43 | print(f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}") 44 | print("ERROR:The driver restarts in 60 seconds.") 45 | sleep(60) 46 | sys.exit() 47 | 48 | 49 | # Get logging level from config.ini 50 | # ERROR = shows errors only 51 | # WARNING = shows ERROR and warnings 52 | # INFO = shows WARNING and running functions 53 | # DEBUG = shows INFO and data/values 54 | if "DEFAULT" in config and "logging" in config["DEFAULT"]: 55 | if config["DEFAULT"]["logging"] == "DEBUG": 56 | logging.basicConfig(level=logging.DEBUG) 57 | elif config["DEFAULT"]["logging"] == "INFO": 58 | logging.basicConfig(level=logging.INFO) 59 | elif config["DEFAULT"]["logging"] == "ERROR": 60 | logging.basicConfig(level=logging.ERROR) 61 | else: 62 | logging.basicConfig(level=logging.WARNING) 63 | else: 64 | logging.basicConfig(level=logging.WARNING) 65 | 66 | 67 | # get timeout 68 | if "DEFAULT" in config and "timeout" in config["DEFAULT"]: 69 | timeout = int(config["DEFAULT"]["timeout"]) 70 | else: 71 | timeout = 60 72 | 73 | 74 | # get type 75 | if "DEFAULT" in config and "type" in config["DEFAULT"]: 76 | type = int(config["DEFAULT"]["type"]) 77 | else: 78 | type = 2 79 | 80 | # get names used in the MQTT messages 81 | temperature_name = str(config["MQTT"].get("temperature_name", "temperature")) 82 | humidity_name = str(config["MQTT"].get("humidity_name", "humidity")) 83 | pressure_name = str(config["MQTT"].get("pressure_name", "pressure")) 84 | 85 | # set variables 86 | connected = 0 87 | last_changed = 0 88 | last_updated = 0 89 | 90 | temperature = -999 91 | humidity = None 92 | pressure = None 93 | 94 | 95 | # MQTT requests 96 | def on_disconnect(client, userdata, flags, reason_code, properties): 97 | global connected 98 | logging.warning("MQTT client: Got disconnected") 99 | if reason_code != 0: 100 | logging.warning("MQTT client: Unexpected MQTT disconnection. Will auto-reconnect") 101 | else: 102 | logging.warning("MQTT client: reason_code value:" + str(reason_code)) 103 | 104 | while connected == 0: 105 | try: 106 | logging.warning(f"MQTT client: Trying to reconnect to broker {config['MQTT']['broker_address']} on port {config['MQTT']['broker_port']}") 107 | client.connect(host=config["MQTT"]["broker_address"], port=int(config["MQTT"]["broker_port"])) 108 | connected = 1 109 | except Exception as err: 110 | logging.error(f"MQTT client: Error in retrying to connect with broker ({config['MQTT']['broker_address']}:{config['MQTT']['broker_port']}): {err}") 111 | logging.error("MQTT client: Retrying in 15 seconds") 112 | connected = 0 113 | sleep(15) 114 | 115 | 116 | def on_connect(client, userdata, flags, reason_code, properties): 117 | global connected 118 | if reason_code == 0: 119 | logging.info("MQTT client: Connected to MQTT broker!") 120 | connected = 1 121 | client.subscribe(config["MQTT"]["topic"]) 122 | else: 123 | logging.error("MQTT client: Failed to connect, return code %d\n", reason_code) 124 | 125 | 126 | def on_message(client, userdata, msg): 127 | try: 128 | global last_changed, temperature, humidity, pressure 129 | 130 | # get JSON from topic 131 | if msg.topic == config["MQTT"]["topic"]: 132 | if msg.payload != "" and msg.payload != b"": 133 | 134 | # Regex to check if payload is a number 135 | number_regex = re.compile(r"^-?\d+(\.\d+)?$") 136 | 137 | if number_regex.match(msg.payload.decode("utf-8")): 138 | temperature = float(msg.payload) 139 | 140 | last_changed = int(time()) 141 | else: 142 | jsonpayload = json.loads(msg.payload) 143 | 144 | last_changed = int(time()) 145 | 146 | if temperature_name in jsonpayload or "value" in jsonpayload: 147 | if temperature_name in jsonpayload: 148 | temperature = float(jsonpayload["temperature"]) 149 | elif "value" in jsonpayload: 150 | temperature = float(jsonpayload["value"]) 151 | 152 | # check if humidity exists 153 | if humidity_name in jsonpayload: 154 | humidity = float(jsonpayload[humidity_name]) 155 | 156 | # check if pressure exists 157 | if pressure_name in jsonpayload: 158 | pressure = float(jsonpayload[pressure_name]) 159 | 160 | else: 161 | logging.error(f'Received JSON MQTT message does not include a temperature object. Expected at least: {{"{temperature_name}": 22.0}}') 162 | logging.debug("MQTT payload: " + str(msg.payload)[1:]) 163 | 164 | else: 165 | logging.warning(f'Received JSON MQTT message was empty and therefore it was ignored. Expected at least: {{"{temperature_name}": 22.0}} or 22.0') 166 | logging.debug("MQTT payload: " + str(msg.payload)[1:]) 167 | 168 | except TypeError as e: 169 | logging.error("Received message is not valid. Check the README and sample payload. %s" % e) 170 | logging.debug("MQTT payload: " + str(msg.payload)[1:]) 171 | 172 | except ValueError as e: 173 | logging.error("Received message is not a valid JSON. Check the README and sample payload. %s" % e) 174 | logging.debug("MQTT payload: " + str(msg.payload)[1:]) 175 | 176 | except Exception as e: 177 | logging.error("Exception occurred: %s" % e) 178 | logging.debug("MQTT payload: " + str(msg.payload)[1:]) 179 | 180 | 181 | class DbusMqttTemperatureService: 182 | def __init__( 183 | self, 184 | servicename, 185 | deviceinstance, 186 | paths, 187 | productname="MQTT Temperature", 188 | customname="MQTT Temperature", 189 | connection="MQTT Temperature service", 190 | ): 191 | self._dbusservice = VeDbusService(servicename, register=False) 192 | self._paths = paths 193 | 194 | logging.debug("%s /DeviceInstance = %d" % (servicename, deviceinstance)) 195 | 196 | # Create the management objects, as specified in the ccgx dbus-api document 197 | self._dbusservice.add_path("/Mgmt/ProcessName", __file__) 198 | self._dbusservice.add_path( 199 | "/Mgmt/ProcessVersion", 200 | "Unkown version, and running on Python " + platform.python_version(), 201 | ) 202 | self._dbusservice.add_path("/Mgmt/Connection", connection) 203 | 204 | # Create the mandatory objects 205 | self._dbusservice.add_path("/DeviceInstance", deviceinstance) 206 | self._dbusservice.add_path("/ProductId", 0xFFFF) 207 | self._dbusservice.add_path("/ProductName", productname) 208 | self._dbusservice.add_path("/CustomName", customname) 209 | self._dbusservice.add_path("/FirmwareVersion", "0.0.8-dev (20250217)") 210 | # self._dbusservice.add_path('/HardwareVersion', '') 211 | self._dbusservice.add_path("/Connected", 1) 212 | 213 | self._dbusservice.add_path("/Status", 0) 214 | self._dbusservice.add_path("/TemperatureType", type) 215 | 216 | for path, settings in self._paths.items(): 217 | self._dbusservice.add_path( 218 | path, 219 | settings["initial"], 220 | gettextcallback=settings["textformat"], 221 | writeable=True, 222 | onchangecallback=self._handlechangedvalue, 223 | ) 224 | 225 | # register VeDbusService after all paths where added 226 | self._dbusservice.register() 227 | 228 | GLib.timeout_add(1000, self._update) # pause 1000ms before the next request 229 | 230 | def _update(self): 231 | global last_changed, last_updated 232 | 233 | now = int(time()) 234 | 235 | if last_changed != last_updated: 236 | self._dbusservice["/Temperature"] = round(temperature, 2) if temperature is not None else None 237 | self._dbusservice["/Humidity"] = round(humidity, 2) if humidity is not None else None 238 | self._dbusservice["/Pressure"] = round(pressure, 2) if pressure is not None else None 239 | 240 | log_message = "Temperature: {:.1f} °C".format(temperature) 241 | log_message += " - Humidity: {:.1f} %".format(humidity) if humidity is not None else "" 242 | log_message += " - Pressure: {:.1f} hPa".format(pressure) if pressure is not None else "" 243 | logging.debug(log_message) 244 | 245 | last_updated = last_changed 246 | 247 | # quit driver if timeout is exceeded 248 | if timeout != 0 and (now - last_changed) > timeout: 249 | logging.error("Driver stopped. Timeout of %i seconds exceeded, since no new MQTT message was received in this time." % timeout) 250 | sys.exit() 251 | 252 | # increment UpdateIndex - to show that new data is available 253 | index = self._dbusservice["/UpdateIndex"] + 1 # increment index 254 | if index > 255: # maximum value of the index 255 | index = 0 # overflow from 255 to 0 256 | self._dbusservice["/UpdateIndex"] = index 257 | return True 258 | 259 | def _handlechangedvalue(self, path, value): 260 | logging.debug("someone else updated %s to %s" % (path, value)) 261 | return True # accept the change 262 | 263 | 264 | def main(): 265 | _thread.daemon = True # allow the program to quit 266 | 267 | from dbus.mainloop.glib import ( # pyright: ignore[reportMissingImports] 268 | DBusGMainLoop, 269 | ) 270 | 271 | # Have a mainloop, so we can send/receive asynchronous calls to and from dbus 272 | DBusGMainLoop(set_as_default=True) 273 | 274 | # MQTT setup 275 | client = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION2, client_id="MqttTemperature_" + get_vrm_portal_id() + "_" + str(config["DEFAULT"]["device_instance"])) 276 | client.on_disconnect = on_disconnect 277 | client.on_connect = on_connect 278 | client.on_message = on_message 279 | 280 | # check tls and use settings, if provided 281 | if "tls_enabled" in config["MQTT"] and config["MQTT"]["tls_enabled"] == "1": 282 | logging.info("MQTT client: TLS is enabled") 283 | 284 | if "tls_path_to_ca" in config["MQTT"] and config["MQTT"]["tls_path_to_ca"] != "": 285 | logging.info('MQTT client: TLS: custom ca "%s" used' % config["MQTT"]["tls_path_to_ca"]) 286 | client.tls_set(config["MQTT"]["tls_path_to_ca"], tls_version=2) 287 | else: 288 | client.tls_set(tls_version=2) 289 | 290 | if "tls_insecure" in config["MQTT"] and config["MQTT"]["tls_insecure"] != "": 291 | logging.info("MQTT client: TLS certificate server hostname verification disabled") 292 | client.tls_insecure_set(True) 293 | 294 | # check if username and password are set 295 | if "username" in config["MQTT"] and "password" in config["MQTT"] and config["MQTT"]["username"] != "" and config["MQTT"]["password"] != "": 296 | logging.info('MQTT client: Using username "%s" and password to connect' % config["MQTT"]["username"]) 297 | client.username_pw_set(username=config["MQTT"]["username"], password=config["MQTT"]["password"]) 298 | 299 | # connect to broker 300 | logging.info(f"MQTT client: Connecting to broker {config['MQTT']['broker_address']} on port {config['MQTT']['broker_port']}") 301 | client.connect(host=config["MQTT"]["broker_address"], port=int(config["MQTT"]["broker_port"])) 302 | client.loop_start() 303 | 304 | # wait to receive first data, else the JSON is empty and phase setup won't work 305 | i = 0 306 | while temperature == -999: 307 | if i % 12 != 0 or i == 0: 308 | logging.info("Waiting 5 seconds for receiving first data...") 309 | else: 310 | logging.warning("Waiting since %s seconds for receiving first data..." % str(i * 5)) 311 | 312 | # check if timeout was exceeded 313 | if timeout != 0 and timeout <= (i * 5): 314 | logging.error("Driver stopped. Timeout of %i seconds exceeded, since no new MQTT message was received in this time." % timeout) 315 | sys.exit() 316 | 317 | sleep(5) 318 | i += 1 319 | 320 | # formatting 321 | def _celsius(p, v): 322 | return str("%.2f" % v) + "°C" 323 | 324 | def _percent(p, v): 325 | return str("%.1f" % v) + "%" 326 | 327 | def _pressure(p, v): 328 | return str("%i" % v) + "hPa" 329 | 330 | def _n(p, v): 331 | return str("%i" % v) 332 | 333 | paths_dbus = { 334 | "/Temperature": {"initial": None, "textformat": _celsius}, 335 | "/Humidity": {"initial": None, "textformat": _percent}, 336 | "/Pressure": {"initial": None, "textformat": _pressure}, 337 | "/UpdateIndex": {"initial": 0, "textformat": _n}, 338 | } 339 | 340 | DbusMqttTemperatureService( 341 | servicename="com.victronenergy.temperature.mqtt_temperature_" + str(config["DEFAULT"]["device_instance"]), 342 | deviceinstance=int(config["DEFAULT"]["device_instance"]), 343 | customname=config["DEFAULT"]["device_name"], 344 | paths=paths_dbus, 345 | ) 346 | 347 | logging.info("Connected to dbus and switching over to GLib.MainLoop() (= event based)") 348 | mainloop = GLib.MainLoop() 349 | mainloop.run() 350 | 351 | 352 | if __name__ == "__main__": 353 | main() 354 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/ext/paho/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-manuel/venus-os_dbus-mqtt-temperature/08f0e716d99659e4db88eef27e90ac9bd6328e9c/dbus-mqtt-temperature/ext/paho/__init__.py -------------------------------------------------------------------------------- /dbus-mqtt-temperature/ext/paho/mqtt/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.1.0" 2 | 3 | 4 | class MQTTException(Exception): 5 | pass 6 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/ext/paho/mqtt/enums.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class MQTTErrorCode(enum.IntEnum): 5 | MQTT_ERR_AGAIN = -1 6 | MQTT_ERR_SUCCESS = 0 7 | MQTT_ERR_NOMEM = 1 8 | MQTT_ERR_PROTOCOL = 2 9 | MQTT_ERR_INVAL = 3 10 | MQTT_ERR_NO_CONN = 4 11 | MQTT_ERR_CONN_REFUSED = 5 12 | MQTT_ERR_NOT_FOUND = 6 13 | MQTT_ERR_CONN_LOST = 7 14 | MQTT_ERR_TLS = 8 15 | MQTT_ERR_PAYLOAD_SIZE = 9 16 | MQTT_ERR_NOT_SUPPORTED = 10 17 | MQTT_ERR_AUTH = 11 18 | MQTT_ERR_ACL_DENIED = 12 19 | MQTT_ERR_UNKNOWN = 13 20 | MQTT_ERR_ERRNO = 14 21 | MQTT_ERR_QUEUE_SIZE = 15 22 | MQTT_ERR_KEEPALIVE = 16 23 | 24 | 25 | class MQTTProtocolVersion(enum.IntEnum): 26 | MQTTv31 = 3 27 | MQTTv311 = 4 28 | MQTTv5 = 5 29 | 30 | 31 | class CallbackAPIVersion(enum.Enum): 32 | """Defined the arguments passed to all user-callback. 33 | 34 | See each callbacks for details: `on_connect`, `on_connect_fail`, `on_disconnect`, `on_message`, `on_publish`, 35 | `on_subscribe`, `on_unsubscribe`, `on_log`, `on_socket_open`, `on_socket_close`, 36 | `on_socket_register_write`, `on_socket_unregister_write` 37 | """ 38 | VERSION1 = 1 39 | """The version used with paho-mqtt 1.x before introducing CallbackAPIVersion. 40 | 41 | This version had different arguments depending if MQTTv5 or MQTTv3 was used. `Properties` & `ReasonCode` were missing 42 | on some callback (apply only to MQTTv5). 43 | 44 | This version is deprecated and will be removed in version 3.0. 45 | """ 46 | VERSION2 = 2 47 | """ This version fix some of the shortcoming of previous version. 48 | 49 | Callback have the same signature if using MQTTv5 or MQTTv3. `ReasonCode` are used in MQTTv3. 50 | """ 51 | 52 | 53 | class MessageType(enum.IntEnum): 54 | CONNECT = 0x10 55 | CONNACK = 0x20 56 | PUBLISH = 0x30 57 | PUBACK = 0x40 58 | PUBREC = 0x50 59 | PUBREL = 0x60 60 | PUBCOMP = 0x70 61 | SUBSCRIBE = 0x80 62 | SUBACK = 0x90 63 | UNSUBSCRIBE = 0xA0 64 | UNSUBACK = 0xB0 65 | PINGREQ = 0xC0 66 | PINGRESP = 0xD0 67 | DISCONNECT = 0xE0 68 | AUTH = 0xF0 69 | 70 | 71 | class LogLevel(enum.IntEnum): 72 | MQTT_LOG_INFO = 0x01 73 | MQTT_LOG_NOTICE = 0x02 74 | MQTT_LOG_WARNING = 0x04 75 | MQTT_LOG_ERR = 0x08 76 | MQTT_LOG_DEBUG = 0x10 77 | 78 | 79 | class ConnackCode(enum.IntEnum): 80 | CONNACK_ACCEPTED = 0 81 | CONNACK_REFUSED_PROTOCOL_VERSION = 1 82 | CONNACK_REFUSED_IDENTIFIER_REJECTED = 2 83 | CONNACK_REFUSED_SERVER_UNAVAILABLE = 3 84 | CONNACK_REFUSED_BAD_USERNAME_PASSWORD = 4 85 | CONNACK_REFUSED_NOT_AUTHORIZED = 5 86 | 87 | 88 | class _ConnectionState(enum.Enum): 89 | MQTT_CS_NEW = enum.auto() 90 | MQTT_CS_CONNECT_ASYNC = enum.auto() 91 | MQTT_CS_CONNECTING = enum.auto() 92 | MQTT_CS_CONNECTED = enum.auto() 93 | MQTT_CS_CONNECTION_LOST = enum.auto() 94 | MQTT_CS_DISCONNECTING = enum.auto() 95 | MQTT_CS_DISCONNECTED = enum.auto() 96 | 97 | 98 | class MessageState(enum.IntEnum): 99 | MQTT_MS_INVALID = 0 100 | MQTT_MS_PUBLISH = 1 101 | MQTT_MS_WAIT_FOR_PUBACK = 2 102 | MQTT_MS_WAIT_FOR_PUBREC = 3 103 | MQTT_MS_RESEND_PUBREL = 4 104 | MQTT_MS_WAIT_FOR_PUBREL = 5 105 | MQTT_MS_RESEND_PUBCOMP = 6 106 | MQTT_MS_WAIT_FOR_PUBCOMP = 7 107 | MQTT_MS_SEND_PUBREC = 8 108 | MQTT_MS_QUEUED = 9 109 | 110 | 111 | class PahoClientMode(enum.IntEnum): 112 | MQTT_CLIENT = 0 113 | MQTT_BRIDGE = 1 114 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/ext/paho/mqtt/matcher.py: -------------------------------------------------------------------------------- 1 | class MQTTMatcher: 2 | """Intended to manage topic filters including wildcards. 3 | 4 | Internally, MQTTMatcher use a prefix tree (trie) to store 5 | values associated with filters, and has an iter_match() 6 | method to iterate efficiently over all filters that match 7 | some topic name.""" 8 | 9 | class Node: 10 | __slots__ = '_children', '_content' 11 | 12 | def __init__(self): 13 | self._children = {} 14 | self._content = None 15 | 16 | def __init__(self): 17 | self._root = self.Node() 18 | 19 | def __setitem__(self, key, value): 20 | """Add a topic filter :key to the prefix tree 21 | and associate it to :value""" 22 | node = self._root 23 | for sym in key.split('/'): 24 | node = node._children.setdefault(sym, self.Node()) 25 | node._content = value 26 | 27 | def __getitem__(self, key): 28 | """Retrieve the value associated with some topic filter :key""" 29 | try: 30 | node = self._root 31 | for sym in key.split('/'): 32 | node = node._children[sym] 33 | if node._content is None: 34 | raise KeyError(key) 35 | return node._content 36 | except KeyError as ke: 37 | raise KeyError(key) from ke 38 | 39 | def __delitem__(self, key): 40 | """Delete the value associated with some topic filter :key""" 41 | lst = [] 42 | try: 43 | parent, node = None, self._root 44 | for k in key.split('/'): 45 | parent, node = node, node._children[k] 46 | lst.append((parent, k, node)) 47 | # TODO 48 | node._content = None 49 | except KeyError as ke: 50 | raise KeyError(key) from ke 51 | else: # cleanup 52 | for parent, k, node in reversed(lst): 53 | if node._children or node._content is not None: 54 | break 55 | del parent._children[k] 56 | 57 | def iter_match(self, topic): 58 | """Return an iterator on all values associated with filters 59 | that match the :topic""" 60 | lst = topic.split('/') 61 | normal = not topic.startswith('$') 62 | def rec(node, i=0): 63 | if i == len(lst): 64 | if node._content is not None: 65 | yield node._content 66 | else: 67 | part = lst[i] 68 | if part in node._children: 69 | for content in rec(node._children[part], i + 1): 70 | yield content 71 | if '+' in node._children and (normal or i > 0): 72 | for content in rec(node._children['+'], i + 1): 73 | yield content 74 | if '#' in node._children and (normal or i > 0): 75 | content = node._children['#']._content 76 | if content is not None: 77 | yield content 78 | return rec(self._root) 79 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/ext/paho/mqtt/packettypes.py: -------------------------------------------------------------------------------- 1 | """ 2 | ******************************************************************* 3 | Copyright (c) 2017, 2019 IBM Corp. 4 | 5 | All rights reserved. This program and the accompanying materials 6 | are made available under the terms of the Eclipse Public License v2.0 7 | and Eclipse Distribution License v1.0 which accompany this distribution. 8 | 9 | The Eclipse Public License is available at 10 | http://www.eclipse.org/legal/epl-v20.html 11 | and the Eclipse Distribution License is available at 12 | http://www.eclipse.org/org/documents/edl-v10.php. 13 | 14 | Contributors: 15 | Ian Craggs - initial implementation and/or documentation 16 | ******************************************************************* 17 | """ 18 | 19 | 20 | class PacketTypes: 21 | 22 | """ 23 | Packet types class. Includes the AUTH packet for MQTT v5.0. 24 | 25 | Holds constants for each packet type such as PacketTypes.PUBLISH 26 | and packet name strings: PacketTypes.Names[PacketTypes.PUBLISH]. 27 | 28 | """ 29 | 30 | indexes = range(1, 16) 31 | 32 | # Packet types 33 | CONNECT, CONNACK, PUBLISH, PUBACK, PUBREC, PUBREL, \ 34 | PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, \ 35 | PINGREQ, PINGRESP, DISCONNECT, AUTH = indexes 36 | 37 | # Dummy packet type for properties use - will delay only applies to will 38 | WILLMESSAGE = 99 39 | 40 | Names = ( "reserved", \ 41 | "Connect", "Connack", "Publish", "Puback", "Pubrec", "Pubrel", \ 42 | "Pubcomp", "Subscribe", "Suback", "Unsubscribe", "Unsuback", \ 43 | "Pingreq", "Pingresp", "Disconnect", "Auth") 44 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/ext/paho/mqtt/properties.py: -------------------------------------------------------------------------------- 1 | # ******************************************************************* 2 | # Copyright (c) 2017, 2019 IBM Corp. 3 | # 4 | # All rights reserved. This program and the accompanying materials 5 | # are made available under the terms of the Eclipse Public License v2.0 6 | # and Eclipse Distribution License v1.0 which accompany this distribution. 7 | # 8 | # The Eclipse Public License is available at 9 | # http://www.eclipse.org/legal/epl-v20.html 10 | # and the Eclipse Distribution License is available at 11 | # http://www.eclipse.org/org/documents/edl-v10.php. 12 | # 13 | # Contributors: 14 | # Ian Craggs - initial implementation and/or documentation 15 | # ******************************************************************* 16 | 17 | import struct 18 | 19 | from .packettypes import PacketTypes 20 | 21 | 22 | class MQTTException(Exception): 23 | pass 24 | 25 | 26 | class MalformedPacket(MQTTException): 27 | pass 28 | 29 | 30 | def writeInt16(length): 31 | # serialize a 16 bit integer to network format 32 | return bytearray(struct.pack("!H", length)) 33 | 34 | 35 | def readInt16(buf): 36 | # deserialize a 16 bit integer from network format 37 | return struct.unpack("!H", buf[:2])[0] 38 | 39 | 40 | def writeInt32(length): 41 | # serialize a 32 bit integer to network format 42 | return bytearray(struct.pack("!L", length)) 43 | 44 | 45 | def readInt32(buf): 46 | # deserialize a 32 bit integer from network format 47 | return struct.unpack("!L", buf[:4])[0] 48 | 49 | 50 | def writeUTF(data): 51 | # data could be a string, or bytes. If string, encode into bytes with utf-8 52 | if not isinstance(data, bytes): 53 | data = bytes(data, "utf-8") 54 | return writeInt16(len(data)) + data 55 | 56 | 57 | def readUTF(buffer, maxlen): 58 | if maxlen >= 2: 59 | length = readInt16(buffer) 60 | else: 61 | raise MalformedPacket("Not enough data to read string length") 62 | maxlen -= 2 63 | if length > maxlen: 64 | raise MalformedPacket("Length delimited string too long") 65 | buf = buffer[2:2+length].decode("utf-8") 66 | # look for chars which are invalid for MQTT 67 | for c in buf: # look for D800-DFFF in the UTF string 68 | ord_c = ord(c) 69 | if ord_c >= 0xD800 and ord_c <= 0xDFFF: 70 | raise MalformedPacket("[MQTT-1.5.4-1] D800-DFFF found in UTF-8 data") 71 | if ord_c == 0x00: # look for null in the UTF string 72 | raise MalformedPacket("[MQTT-1.5.4-2] Null found in UTF-8 data") 73 | if ord_c == 0xFEFF: 74 | raise MalformedPacket("[MQTT-1.5.4-3] U+FEFF in UTF-8 data") 75 | return buf, length+2 76 | 77 | 78 | def writeBytes(buffer): 79 | return writeInt16(len(buffer)) + buffer 80 | 81 | 82 | def readBytes(buffer): 83 | length = readInt16(buffer) 84 | return buffer[2:2+length], length+2 85 | 86 | 87 | class VariableByteIntegers: # Variable Byte Integer 88 | """ 89 | MQTT variable byte integer helper class. Used 90 | in several places in MQTT v5.0 properties. 91 | 92 | """ 93 | 94 | @staticmethod 95 | def encode(x): 96 | """ 97 | Convert an integer 0 <= x <= 268435455 into multi-byte format. 98 | Returns the buffer converted from the integer. 99 | """ 100 | if not 0 <= x <= 268435455: 101 | raise ValueError(f"Value {x!r} must be in range 0-268435455") 102 | buffer = b'' 103 | while 1: 104 | digit = x % 128 105 | x //= 128 106 | if x > 0: 107 | digit |= 0x80 108 | buffer += bytes([digit]) 109 | if x == 0: 110 | break 111 | return buffer 112 | 113 | @staticmethod 114 | def decode(buffer): 115 | """ 116 | Get the value of a multi-byte integer from a buffer 117 | Return the value, and the number of bytes used. 118 | 119 | [MQTT-1.5.5-1] the encoded value MUST use the minimum number of bytes necessary to represent the value 120 | """ 121 | multiplier = 1 122 | value = 0 123 | bytes = 0 124 | while 1: 125 | bytes += 1 126 | digit = buffer[0] 127 | buffer = buffer[1:] 128 | value += (digit & 127) * multiplier 129 | if digit & 128 == 0: 130 | break 131 | multiplier *= 128 132 | return (value, bytes) 133 | 134 | 135 | class Properties: 136 | """MQTT v5.0 properties class. 137 | 138 | See Properties.names for a list of accepted property names along with their numeric values. 139 | 140 | See Properties.properties for the data type of each property. 141 | 142 | Example of use:: 143 | 144 | publish_properties = Properties(PacketTypes.PUBLISH) 145 | publish_properties.UserProperty = ("a", "2") 146 | publish_properties.UserProperty = ("c", "3") 147 | 148 | First the object is created with packet type as argument, no properties will be present at 149 | this point. Then properties are added as attributes, the name of which is the string property 150 | name without the spaces. 151 | 152 | """ 153 | 154 | def __init__(self, packetType): 155 | self.packetType = packetType 156 | self.types = ["Byte", "Two Byte Integer", "Four Byte Integer", "Variable Byte Integer", 157 | "Binary Data", "UTF-8 Encoded String", "UTF-8 String Pair"] 158 | 159 | self.names = { 160 | "Payload Format Indicator": 1, 161 | "Message Expiry Interval": 2, 162 | "Content Type": 3, 163 | "Response Topic": 8, 164 | "Correlation Data": 9, 165 | "Subscription Identifier": 11, 166 | "Session Expiry Interval": 17, 167 | "Assigned Client Identifier": 18, 168 | "Server Keep Alive": 19, 169 | "Authentication Method": 21, 170 | "Authentication Data": 22, 171 | "Request Problem Information": 23, 172 | "Will Delay Interval": 24, 173 | "Request Response Information": 25, 174 | "Response Information": 26, 175 | "Server Reference": 28, 176 | "Reason String": 31, 177 | "Receive Maximum": 33, 178 | "Topic Alias Maximum": 34, 179 | "Topic Alias": 35, 180 | "Maximum QoS": 36, 181 | "Retain Available": 37, 182 | "User Property": 38, 183 | "Maximum Packet Size": 39, 184 | "Wildcard Subscription Available": 40, 185 | "Subscription Identifier Available": 41, 186 | "Shared Subscription Available": 42 187 | } 188 | 189 | self.properties = { 190 | # id: type, packets 191 | # payload format indicator 192 | 1: (self.types.index("Byte"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), 193 | 2: (self.types.index("Four Byte Integer"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), 194 | 3: (self.types.index("UTF-8 Encoded String"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), 195 | 8: (self.types.index("UTF-8 Encoded String"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), 196 | 9: (self.types.index("Binary Data"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), 197 | 11: (self.types.index("Variable Byte Integer"), 198 | [PacketTypes.PUBLISH, PacketTypes.SUBSCRIBE]), 199 | 17: (self.types.index("Four Byte Integer"), 200 | [PacketTypes.CONNECT, PacketTypes.CONNACK, PacketTypes.DISCONNECT]), 201 | 18: (self.types.index("UTF-8 Encoded String"), [PacketTypes.CONNACK]), 202 | 19: (self.types.index("Two Byte Integer"), [PacketTypes.CONNACK]), 203 | 21: (self.types.index("UTF-8 Encoded String"), 204 | [PacketTypes.CONNECT, PacketTypes.CONNACK, PacketTypes.AUTH]), 205 | 22: (self.types.index("Binary Data"), 206 | [PacketTypes.CONNECT, PacketTypes.CONNACK, PacketTypes.AUTH]), 207 | 23: (self.types.index("Byte"), 208 | [PacketTypes.CONNECT]), 209 | 24: (self.types.index("Four Byte Integer"), [PacketTypes.WILLMESSAGE]), 210 | 25: (self.types.index("Byte"), [PacketTypes.CONNECT]), 211 | 26: (self.types.index("UTF-8 Encoded String"), [PacketTypes.CONNACK]), 212 | 28: (self.types.index("UTF-8 Encoded String"), 213 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]), 214 | 31: (self.types.index("UTF-8 Encoded String"), 215 | [PacketTypes.CONNACK, PacketTypes.PUBACK, PacketTypes.PUBREC, 216 | PacketTypes.PUBREL, PacketTypes.PUBCOMP, PacketTypes.SUBACK, 217 | PacketTypes.UNSUBACK, PacketTypes.DISCONNECT, PacketTypes.AUTH]), 218 | 33: (self.types.index("Two Byte Integer"), 219 | [PacketTypes.CONNECT, PacketTypes.CONNACK]), 220 | 34: (self.types.index("Two Byte Integer"), 221 | [PacketTypes.CONNECT, PacketTypes.CONNACK]), 222 | 35: (self.types.index("Two Byte Integer"), [PacketTypes.PUBLISH]), 223 | 36: (self.types.index("Byte"), [PacketTypes.CONNACK]), 224 | 37: (self.types.index("Byte"), [PacketTypes.CONNACK]), 225 | 38: (self.types.index("UTF-8 String Pair"), 226 | [PacketTypes.CONNECT, PacketTypes.CONNACK, 227 | PacketTypes.PUBLISH, PacketTypes.PUBACK, 228 | PacketTypes.PUBREC, PacketTypes.PUBREL, PacketTypes.PUBCOMP, 229 | PacketTypes.SUBSCRIBE, PacketTypes.SUBACK, 230 | PacketTypes.UNSUBSCRIBE, PacketTypes.UNSUBACK, 231 | PacketTypes.DISCONNECT, PacketTypes.AUTH, PacketTypes.WILLMESSAGE]), 232 | 39: (self.types.index("Four Byte Integer"), 233 | [PacketTypes.CONNECT, PacketTypes.CONNACK]), 234 | 40: (self.types.index("Byte"), [PacketTypes.CONNACK]), 235 | 41: (self.types.index("Byte"), [PacketTypes.CONNACK]), 236 | 42: (self.types.index("Byte"), [PacketTypes.CONNACK]), 237 | } 238 | 239 | def allowsMultiple(self, compressedName): 240 | return self.getIdentFromName(compressedName) in [11, 38] 241 | 242 | def getIdentFromName(self, compressedName): 243 | # return the identifier corresponding to the property name 244 | result = -1 245 | for name in self.names.keys(): 246 | if compressedName == name.replace(' ', ''): 247 | result = self.names[name] 248 | break 249 | return result 250 | 251 | def __setattr__(self, name, value): 252 | name = name.replace(' ', '') 253 | privateVars = ["packetType", "types", "names", "properties"] 254 | if name in privateVars: 255 | object.__setattr__(self, name, value) 256 | else: 257 | # the name could have spaces in, or not. Remove spaces before assignment 258 | if name not in [aname.replace(' ', '') for aname in self.names.keys()]: 259 | raise MQTTException( 260 | f"Property name must be one of {self.names.keys()}") 261 | # check that this attribute applies to the packet type 262 | if self.packetType not in self.properties[self.getIdentFromName(name)][1]: 263 | raise MQTTException(f"Property {name} does not apply to packet type {PacketTypes.Names[self.packetType]}") 264 | 265 | # Check for forbidden values 266 | if not isinstance(value, list): 267 | if name in ["ReceiveMaximum", "TopicAlias"] \ 268 | and (value < 1 or value > 65535): 269 | 270 | raise MQTTException(f"{name} property value must be in the range 1-65535") 271 | elif name in ["TopicAliasMaximum"] \ 272 | and (value < 0 or value > 65535): 273 | 274 | raise MQTTException(f"{name} property value must be in the range 0-65535") 275 | elif name in ["MaximumPacketSize", "SubscriptionIdentifier"] \ 276 | and (value < 1 or value > 268435455): 277 | 278 | raise MQTTException(f"{name} property value must be in the range 1-268435455") 279 | elif name in ["RequestResponseInformation", "RequestProblemInformation", "PayloadFormatIndicator"] \ 280 | and (value != 0 and value != 1): 281 | 282 | raise MQTTException( 283 | f"{name} property value must be 0 or 1") 284 | 285 | if self.allowsMultiple(name): 286 | if not isinstance(value, list): 287 | value = [value] 288 | if hasattr(self, name): 289 | value = object.__getattribute__(self, name) + value 290 | object.__setattr__(self, name, value) 291 | 292 | def __str__(self): 293 | buffer = "[" 294 | first = True 295 | for name in self.names.keys(): 296 | compressedName = name.replace(' ', '') 297 | if hasattr(self, compressedName): 298 | if not first: 299 | buffer += ", " 300 | buffer += f"{compressedName} : {getattr(self, compressedName)}" 301 | first = False 302 | buffer += "]" 303 | return buffer 304 | 305 | def json(self): 306 | data = {} 307 | for name in self.names.keys(): 308 | compressedName = name.replace(' ', '') 309 | if hasattr(self, compressedName): 310 | val = getattr(self, compressedName) 311 | if compressedName == 'CorrelationData' and isinstance(val, bytes): 312 | data[compressedName] = val.hex() 313 | else: 314 | data[compressedName] = val 315 | return data 316 | 317 | def isEmpty(self): 318 | rc = True 319 | for name in self.names.keys(): 320 | compressedName = name.replace(' ', '') 321 | if hasattr(self, compressedName): 322 | rc = False 323 | break 324 | return rc 325 | 326 | def clear(self): 327 | for name in self.names.keys(): 328 | compressedName = name.replace(' ', '') 329 | if hasattr(self, compressedName): 330 | delattr(self, compressedName) 331 | 332 | def writeProperty(self, identifier, type, value): 333 | buffer = b"" 334 | buffer += VariableByteIntegers.encode(identifier) # identifier 335 | if type == self.types.index("Byte"): # value 336 | buffer += bytes([value]) 337 | elif type == self.types.index("Two Byte Integer"): 338 | buffer += writeInt16(value) 339 | elif type == self.types.index("Four Byte Integer"): 340 | buffer += writeInt32(value) 341 | elif type == self.types.index("Variable Byte Integer"): 342 | buffer += VariableByteIntegers.encode(value) 343 | elif type == self.types.index("Binary Data"): 344 | buffer += writeBytes(value) 345 | elif type == self.types.index("UTF-8 Encoded String"): 346 | buffer += writeUTF(value) 347 | elif type == self.types.index("UTF-8 String Pair"): 348 | buffer += writeUTF(value[0]) + writeUTF(value[1]) 349 | return buffer 350 | 351 | def pack(self): 352 | # serialize properties into buffer for sending over network 353 | buffer = b"" 354 | for name in self.names.keys(): 355 | compressedName = name.replace(' ', '') 356 | if hasattr(self, compressedName): 357 | identifier = self.getIdentFromName(compressedName) 358 | attr_type = self.properties[identifier][0] 359 | if self.allowsMultiple(compressedName): 360 | for prop in getattr(self, compressedName): 361 | buffer += self.writeProperty(identifier, 362 | attr_type, prop) 363 | else: 364 | buffer += self.writeProperty(identifier, attr_type, 365 | getattr(self, compressedName)) 366 | return VariableByteIntegers.encode(len(buffer)) + buffer 367 | 368 | def readProperty(self, buffer, type, propslen): 369 | if type == self.types.index("Byte"): 370 | value = buffer[0] 371 | valuelen = 1 372 | elif type == self.types.index("Two Byte Integer"): 373 | value = readInt16(buffer) 374 | valuelen = 2 375 | elif type == self.types.index("Four Byte Integer"): 376 | value = readInt32(buffer) 377 | valuelen = 4 378 | elif type == self.types.index("Variable Byte Integer"): 379 | value, valuelen = VariableByteIntegers.decode(buffer) 380 | elif type == self.types.index("Binary Data"): 381 | value, valuelen = readBytes(buffer) 382 | elif type == self.types.index("UTF-8 Encoded String"): 383 | value, valuelen = readUTF(buffer, propslen) 384 | elif type == self.types.index("UTF-8 String Pair"): 385 | value, valuelen = readUTF(buffer, propslen) 386 | buffer = buffer[valuelen:] # strip the bytes used by the value 387 | value1, valuelen1 = readUTF(buffer, propslen - valuelen) 388 | value = (value, value1) 389 | valuelen += valuelen1 390 | return value, valuelen 391 | 392 | def getNameFromIdent(self, identifier): 393 | rc = None 394 | for name in self.names: 395 | if self.names[name] == identifier: 396 | rc = name 397 | return rc 398 | 399 | def unpack(self, buffer): 400 | self.clear() 401 | # deserialize properties into attributes from buffer received from network 402 | propslen, VBIlen = VariableByteIntegers.decode(buffer) 403 | buffer = buffer[VBIlen:] # strip the bytes used by the VBI 404 | propslenleft = propslen 405 | while propslenleft > 0: # properties length is 0 if there are none 406 | identifier, VBIlen2 = VariableByteIntegers.decode( 407 | buffer) # property identifier 408 | buffer = buffer[VBIlen2:] # strip the bytes used by the VBI 409 | propslenleft -= VBIlen2 410 | attr_type = self.properties[identifier][0] 411 | value, valuelen = self.readProperty( 412 | buffer, attr_type, propslenleft) 413 | buffer = buffer[valuelen:] # strip the bytes used by the value 414 | propslenleft -= valuelen 415 | propname = self.getNameFromIdent(identifier) 416 | compressedName = propname.replace(' ', '') 417 | if not self.allowsMultiple(compressedName) and hasattr(self, compressedName): 418 | raise MQTTException( 419 | f"Property '{property}' must not exist more than once") 420 | setattr(self, propname, value) 421 | return self, propslen + VBIlen 422 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/ext/paho/mqtt/publish.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Roger Light 2 | # 3 | # All rights reserved. This program and the accompanying materials 4 | # are made available under the terms of the Eclipse Public License v2.0 5 | # and Eclipse Distribution License v1.0 which accompany this distribution. 6 | # 7 | # The Eclipse Public License is available at 8 | # http://www.eclipse.org/legal/epl-v20.html 9 | # and the Eclipse Distribution License is available at 10 | # http://www.eclipse.org/org/documents/edl-v10.php. 11 | # 12 | # Contributors: 13 | # Roger Light - initial API and implementation 14 | 15 | """ 16 | This module provides some helper functions to allow straightforward publishing 17 | of messages in a one-shot manner. In other words, they are useful for the 18 | situation where you have a single/multiple messages you want to publish to a 19 | broker, then disconnect and nothing else is required. 20 | """ 21 | from __future__ import annotations 22 | 23 | import collections 24 | from collections.abc import Iterable 25 | from typing import TYPE_CHECKING, Any, List, Tuple, Union 26 | 27 | from paho.mqtt.enums import CallbackAPIVersion, MQTTProtocolVersion 28 | from paho.mqtt.properties import Properties 29 | from paho.mqtt.reasoncodes import ReasonCode 30 | 31 | from .. import mqtt 32 | from . import client as paho 33 | 34 | if TYPE_CHECKING: 35 | try: 36 | from typing import NotRequired, Required, TypedDict # type: ignore 37 | except ImportError: 38 | from typing_extensions import NotRequired, Required, TypedDict 39 | 40 | try: 41 | from typing import Literal 42 | except ImportError: 43 | from typing_extensions import Literal # type: ignore 44 | 45 | 46 | 47 | class AuthParameter(TypedDict, total=False): 48 | username: Required[str] 49 | password: NotRequired[str] 50 | 51 | 52 | class TLSParameter(TypedDict, total=False): 53 | ca_certs: Required[str] 54 | certfile: NotRequired[str] 55 | keyfile: NotRequired[str] 56 | tls_version: NotRequired[int] 57 | ciphers: NotRequired[str] 58 | insecure: NotRequired[bool] 59 | 60 | 61 | class MessageDict(TypedDict, total=False): 62 | topic: Required[str] 63 | payload: NotRequired[paho.PayloadType] 64 | qos: NotRequired[int] 65 | retain: NotRequired[bool] 66 | 67 | MessageTuple = Tuple[str, paho.PayloadType, int, bool] 68 | 69 | MessagesList = List[Union[MessageDict, MessageTuple]] 70 | 71 | 72 | def _do_publish(client: paho.Client): 73 | """Internal function""" 74 | 75 | message = client._userdata.popleft() 76 | 77 | if isinstance(message, dict): 78 | client.publish(**message) 79 | elif isinstance(message, (tuple, list)): 80 | client.publish(*message) 81 | else: 82 | raise TypeError('message must be a dict, tuple, or list') 83 | 84 | 85 | def _on_connect(client: paho.Client, userdata: MessagesList, flags, reason_code, properties): 86 | """Internal v5 callback""" 87 | if reason_code == 0: 88 | if len(userdata) > 0: 89 | _do_publish(client) 90 | else: 91 | raise mqtt.MQTTException(paho.connack_string(reason_code)) 92 | 93 | 94 | def _on_publish( 95 | client: paho.Client, userdata: collections.deque[MessagesList], mid: int, reason_codes: ReasonCode, properties: Properties, 96 | ) -> None: 97 | """Internal callback""" 98 | #pylint: disable=unused-argument 99 | 100 | if len(userdata) == 0: 101 | client.disconnect() 102 | else: 103 | _do_publish(client) 104 | 105 | 106 | def multiple( 107 | msgs: MessagesList, 108 | hostname: str = "localhost", 109 | port: int = 1883, 110 | client_id: str = "", 111 | keepalive: int = 60, 112 | will: MessageDict | None = None, 113 | auth: AuthParameter | None = None, 114 | tls: TLSParameter | None = None, 115 | protocol: MQTTProtocolVersion = paho.MQTTv311, 116 | transport: Literal["tcp", "websockets"] = "tcp", 117 | proxy_args: Any | None = None, 118 | ) -> None: 119 | """Publish multiple messages to a broker, then disconnect cleanly. 120 | 121 | This function creates an MQTT client, connects to a broker and publishes a 122 | list of messages. Once the messages have been delivered, it disconnects 123 | cleanly from the broker. 124 | 125 | :param msgs: a list of messages to publish. Each message is either a dict or a 126 | tuple. 127 | 128 | If a dict, only the topic must be present. Default values will be 129 | used for any missing arguments. The dict must be of the form: 130 | 131 | msg = {'topic':"", 'payload':"", 'qos':, 132 | 'retain':} 133 | topic must be present and may not be empty. 134 | If payload is "", None or not present then a zero length payload 135 | will be published. 136 | If qos is not present, the default of 0 is used. 137 | If retain is not present, the default of False is used. 138 | 139 | If a tuple, then it must be of the form: 140 | ("", "", qos, retain) 141 | 142 | :param str hostname: the address of the broker to connect to. 143 | Defaults to localhost. 144 | 145 | :param int port: the port to connect to the broker on. Defaults to 1883. 146 | 147 | :param str client_id: the MQTT client id to use. If "" or None, the Paho library will 148 | generate a client id automatically. 149 | 150 | :param int keepalive: the keepalive timeout value for the client. Defaults to 60 151 | seconds. 152 | 153 | :param will: a dict containing will parameters for the client: will = {'topic': 154 | "", 'payload':", 'qos':, 'retain':}. 155 | Topic is required, all other parameters are optional and will 156 | default to None, 0 and False respectively. 157 | Defaults to None, which indicates no will should be used. 158 | 159 | :param auth: a dict containing authentication parameters for the client: 160 | auth = {'username':"", 'password':""} 161 | Username is required, password is optional and will default to None 162 | if not provided. 163 | Defaults to None, which indicates no authentication is to be used. 164 | 165 | :param tls: a dict containing TLS configuration parameters for the client: 166 | dict = {'ca_certs':"", 'certfile':"", 167 | 'keyfile':"", 'tls_version':"", 168 | 'ciphers':", 'insecure':""} 169 | ca_certs is required, all other parameters are optional and will 170 | default to None if not provided, which results in the client using 171 | the default behaviour - see the paho.mqtt.client documentation. 172 | Alternatively, tls input can be an SSLContext object, which will be 173 | processed using the tls_set_context method. 174 | Defaults to None, which indicates that TLS should not be used. 175 | 176 | :param str transport: set to "tcp" to use the default setting of transport which is 177 | raw TCP. Set to "websockets" to use WebSockets as the transport. 178 | 179 | :param proxy_args: a dictionary that will be given to the client. 180 | """ 181 | 182 | if not isinstance(msgs, Iterable): 183 | raise TypeError('msgs must be an iterable') 184 | if len(msgs) == 0: 185 | raise ValueError('msgs is empty') 186 | 187 | client = paho.Client( 188 | CallbackAPIVersion.VERSION2, 189 | client_id=client_id, 190 | userdata=collections.deque(msgs), 191 | protocol=protocol, 192 | transport=transport, 193 | ) 194 | 195 | client.enable_logger() 196 | client.on_publish = _on_publish 197 | client.on_connect = _on_connect # type: ignore 198 | 199 | if proxy_args is not None: 200 | client.proxy_set(**proxy_args) 201 | 202 | if auth: 203 | username = auth.get('username') 204 | if username: 205 | password = auth.get('password') 206 | client.username_pw_set(username, password) 207 | else: 208 | raise KeyError("The 'username' key was not found, this is " 209 | "required for auth") 210 | 211 | if will is not None: 212 | client.will_set(**will) 213 | 214 | if tls is not None: 215 | if isinstance(tls, dict): 216 | insecure = tls.pop('insecure', False) 217 | # mypy don't get that tls no longer contains the key insecure 218 | client.tls_set(**tls) # type: ignore[misc] 219 | if insecure: 220 | # Must be set *after* the `client.tls_set()` call since it sets 221 | # up the SSL context that `client.tls_insecure_set` alters. 222 | client.tls_insecure_set(insecure) 223 | else: 224 | # Assume input is SSLContext object 225 | client.tls_set_context(tls) 226 | 227 | client.connect(hostname, port, keepalive) 228 | client.loop_forever() 229 | 230 | 231 | def single( 232 | topic: str, 233 | payload: paho.PayloadType = None, 234 | qos: int = 0, 235 | retain: bool = False, 236 | hostname: str = "localhost", 237 | port: int = 1883, 238 | client_id: str = "", 239 | keepalive: int = 60, 240 | will: MessageDict | None = None, 241 | auth: AuthParameter | None = None, 242 | tls: TLSParameter | None = None, 243 | protocol: MQTTProtocolVersion = paho.MQTTv311, 244 | transport: Literal["tcp", "websockets"] = "tcp", 245 | proxy_args: Any | None = None, 246 | ) -> None: 247 | """Publish a single message to a broker, then disconnect cleanly. 248 | 249 | This function creates an MQTT client, connects to a broker and publishes a 250 | single message. Once the message has been delivered, it disconnects cleanly 251 | from the broker. 252 | 253 | :param str topic: the only required argument must be the topic string to which the 254 | payload will be published. 255 | 256 | :param payload: the payload to be published. If "" or None, a zero length payload 257 | will be published. 258 | 259 | :param int qos: the qos to use when publishing, default to 0. 260 | 261 | :param bool retain: set the message to be retained (True) or not (False). 262 | 263 | :param str hostname: the address of the broker to connect to. 264 | Defaults to localhost. 265 | 266 | :param int port: the port to connect to the broker on. Defaults to 1883. 267 | 268 | :param str client_id: the MQTT client id to use. If "" or None, the Paho library will 269 | generate a client id automatically. 270 | 271 | :param int keepalive: the keepalive timeout value for the client. Defaults to 60 272 | seconds. 273 | 274 | :param will: a dict containing will parameters for the client: will = {'topic': 275 | "", 'payload':", 'qos':, 'retain':}. 276 | Topic is required, all other parameters are optional and will 277 | default to None, 0 and False respectively. 278 | Defaults to None, which indicates no will should be used. 279 | 280 | :param auth: a dict containing authentication parameters for the client: 281 | Username is required, password is optional and will default to None 282 | auth = {'username':"", 'password':""} 283 | if not provided. 284 | Defaults to None, which indicates no authentication is to be used. 285 | 286 | :param tls: a dict containing TLS configuration parameters for the client: 287 | dict = {'ca_certs':"", 'certfile':"", 288 | 'keyfile':"", 'tls_version':"", 289 | 'ciphers':", 'insecure':""} 290 | ca_certs is required, all other parameters are optional and will 291 | default to None if not provided, which results in the client using 292 | the default behaviour - see the paho.mqtt.client documentation. 293 | Defaults to None, which indicates that TLS should not be used. 294 | Alternatively, tls input can be an SSLContext object, which will be 295 | processed using the tls_set_context method. 296 | 297 | :param transport: set to "tcp" to use the default setting of transport which is 298 | raw TCP. Set to "websockets" to use WebSockets as the transport. 299 | 300 | :param proxy_args: a dictionary that will be given to the client. 301 | """ 302 | 303 | msg: MessageDict = {'topic':topic, 'payload':payload, 'qos':qos, 'retain':retain} 304 | 305 | multiple([msg], hostname, port, client_id, keepalive, will, auth, tls, 306 | protocol, transport, proxy_args) 307 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/ext/paho/mqtt/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-manuel/venus-os_dbus-mqtt-temperature/08f0e716d99659e4db88eef27e90ac9bd6328e9c/dbus-mqtt-temperature/ext/paho/mqtt/py.typed -------------------------------------------------------------------------------- /dbus-mqtt-temperature/ext/paho/mqtt/reasoncodes.py: -------------------------------------------------------------------------------- 1 | # ******************************************************************* 2 | # Copyright (c) 2017, 2019 IBM Corp. 3 | # 4 | # All rights reserved. This program and the accompanying materials 5 | # are made available under the terms of the Eclipse Public License v2.0 6 | # and Eclipse Distribution License v1.0 which accompany this distribution. 7 | # 8 | # The Eclipse Public License is available at 9 | # http://www.eclipse.org/legal/epl-v20.html 10 | # and the Eclipse Distribution License is available at 11 | # http://www.eclipse.org/org/documents/edl-v10.php. 12 | # 13 | # Contributors: 14 | # Ian Craggs - initial implementation and/or documentation 15 | # ******************************************************************* 16 | 17 | import functools 18 | import warnings 19 | from typing import Any 20 | 21 | from .packettypes import PacketTypes 22 | 23 | 24 | @functools.total_ordering 25 | class ReasonCode: 26 | """MQTT version 5.0 reason codes class. 27 | 28 | See ReasonCode.names for a list of possible numeric values along with their 29 | names and the packets to which they apply. 30 | 31 | """ 32 | 33 | def __init__(self, packetType: int, aName: str ="Success", identifier: int =-1): 34 | """ 35 | packetType: the type of the packet, such as PacketTypes.CONNECT that 36 | this reason code will be used with. Some reason codes have different 37 | names for the same identifier when used a different packet type. 38 | 39 | aName: the String name of the reason code to be created. Ignored 40 | if the identifier is set. 41 | 42 | identifier: an integer value of the reason code to be created. 43 | 44 | """ 45 | 46 | self.packetType = packetType 47 | self.names = { 48 | 0: {"Success": [PacketTypes.CONNACK, PacketTypes.PUBACK, 49 | PacketTypes.PUBREC, PacketTypes.PUBREL, PacketTypes.PUBCOMP, 50 | PacketTypes.UNSUBACK, PacketTypes.AUTH], 51 | "Normal disconnection": [PacketTypes.DISCONNECT], 52 | "Granted QoS 0": [PacketTypes.SUBACK]}, 53 | 1: {"Granted QoS 1": [PacketTypes.SUBACK]}, 54 | 2: {"Granted QoS 2": [PacketTypes.SUBACK]}, 55 | 4: {"Disconnect with will message": [PacketTypes.DISCONNECT]}, 56 | 16: {"No matching subscribers": 57 | [PacketTypes.PUBACK, PacketTypes.PUBREC]}, 58 | 17: {"No subscription found": [PacketTypes.UNSUBACK]}, 59 | 24: {"Continue authentication": [PacketTypes.AUTH]}, 60 | 25: {"Re-authenticate": [PacketTypes.AUTH]}, 61 | 128: {"Unspecified error": [PacketTypes.CONNACK, PacketTypes.PUBACK, 62 | PacketTypes.PUBREC, PacketTypes.SUBACK, PacketTypes.UNSUBACK, 63 | PacketTypes.DISCONNECT], }, 64 | 129: {"Malformed packet": 65 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 66 | 130: {"Protocol error": 67 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 68 | 131: {"Implementation specific error": [PacketTypes.CONNACK, 69 | PacketTypes.PUBACK, PacketTypes.PUBREC, PacketTypes.SUBACK, 70 | PacketTypes.UNSUBACK, PacketTypes.DISCONNECT], }, 71 | 132: {"Unsupported protocol version": [PacketTypes.CONNACK]}, 72 | 133: {"Client identifier not valid": [PacketTypes.CONNACK]}, 73 | 134: {"Bad user name or password": [PacketTypes.CONNACK]}, 74 | 135: {"Not authorized": [PacketTypes.CONNACK, PacketTypes.PUBACK, 75 | PacketTypes.PUBREC, PacketTypes.SUBACK, PacketTypes.UNSUBACK, 76 | PacketTypes.DISCONNECT], }, 77 | 136: {"Server unavailable": [PacketTypes.CONNACK]}, 78 | 137: {"Server busy": [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 79 | 138: {"Banned": [PacketTypes.CONNACK]}, 80 | 139: {"Server shutting down": [PacketTypes.DISCONNECT]}, 81 | 140: {"Bad authentication method": 82 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 83 | 141: {"Keep alive timeout": [PacketTypes.DISCONNECT]}, 84 | 142: {"Session taken over": [PacketTypes.DISCONNECT]}, 85 | 143: {"Topic filter invalid": 86 | [PacketTypes.SUBACK, PacketTypes.UNSUBACK, PacketTypes.DISCONNECT]}, 87 | 144: {"Topic name invalid": 88 | [PacketTypes.CONNACK, PacketTypes.PUBACK, 89 | PacketTypes.PUBREC, PacketTypes.DISCONNECT]}, 90 | 145: {"Packet identifier in use": 91 | [PacketTypes.PUBACK, PacketTypes.PUBREC, 92 | PacketTypes.SUBACK, PacketTypes.UNSUBACK]}, 93 | 146: {"Packet identifier not found": 94 | [PacketTypes.PUBREL, PacketTypes.PUBCOMP]}, 95 | 147: {"Receive maximum exceeded": [PacketTypes.DISCONNECT]}, 96 | 148: {"Topic alias invalid": [PacketTypes.DISCONNECT]}, 97 | 149: {"Packet too large": [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 98 | 150: {"Message rate too high": [PacketTypes.DISCONNECT]}, 99 | 151: {"Quota exceeded": [PacketTypes.CONNACK, PacketTypes.PUBACK, 100 | PacketTypes.PUBREC, PacketTypes.SUBACK, PacketTypes.DISCONNECT], }, 101 | 152: {"Administrative action": [PacketTypes.DISCONNECT]}, 102 | 153: {"Payload format invalid": 103 | [PacketTypes.PUBACK, PacketTypes.PUBREC, PacketTypes.DISCONNECT]}, 104 | 154: {"Retain not supported": 105 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 106 | 155: {"QoS not supported": 107 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 108 | 156: {"Use another server": 109 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 110 | 157: {"Server moved": 111 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 112 | 158: {"Shared subscription not supported": 113 | [PacketTypes.SUBACK, PacketTypes.DISCONNECT]}, 114 | 159: {"Connection rate exceeded": 115 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 116 | 160: {"Maximum connect time": 117 | [PacketTypes.DISCONNECT]}, 118 | 161: {"Subscription identifiers not supported": 119 | [PacketTypes.SUBACK, PacketTypes.DISCONNECT]}, 120 | 162: {"Wildcard subscription not supported": 121 | [PacketTypes.SUBACK, PacketTypes.DISCONNECT]}, 122 | } 123 | if identifier == -1: 124 | if packetType == PacketTypes.DISCONNECT and aName == "Success": 125 | aName = "Normal disconnection" 126 | self.set(aName) 127 | else: 128 | self.value = identifier 129 | self.getName() # check it's good 130 | 131 | def __getName__(self, packetType, identifier): 132 | """ 133 | Get the reason code string name for a specific identifier. 134 | The name can vary by packet type for the same identifier, which 135 | is why the packet type is also required. 136 | 137 | Used when displaying the reason code. 138 | """ 139 | if identifier not in self.names: 140 | raise KeyError(identifier) 141 | names = self.names[identifier] 142 | namelist = [name for name in names.keys() if packetType in names[name]] 143 | if len(namelist) != 1: 144 | raise ValueError(f"Expected exactly one name, found {namelist!r}") 145 | return namelist[0] 146 | 147 | def getId(self, name): 148 | """ 149 | Get the numeric id corresponding to a reason code name. 150 | 151 | Used when setting the reason code for a packetType 152 | check that only valid codes for the packet are set. 153 | """ 154 | for code in self.names.keys(): 155 | if name in self.names[code].keys(): 156 | if self.packetType in self.names[code][name]: 157 | return code 158 | raise KeyError(f"Reason code name not found: {name}") 159 | 160 | def set(self, name): 161 | self.value = self.getId(name) 162 | 163 | def unpack(self, buffer): 164 | c = buffer[0] 165 | name = self.__getName__(self.packetType, c) 166 | self.value = self.getId(name) 167 | return 1 168 | 169 | def getName(self): 170 | """Returns the reason code name corresponding to the numeric value which is set. 171 | """ 172 | return self.__getName__(self.packetType, self.value) 173 | 174 | def __eq__(self, other): 175 | if isinstance(other, int): 176 | return self.value == other 177 | if isinstance(other, str): 178 | return other == str(self) 179 | if isinstance(other, ReasonCode): 180 | return self.value == other.value 181 | return False 182 | 183 | def __lt__(self, other): 184 | if isinstance(other, int): 185 | return self.value < other 186 | if isinstance(other, ReasonCode): 187 | return self.value < other.value 188 | return NotImplemented 189 | 190 | def __repr__(self): 191 | try: 192 | packet_name = PacketTypes.Names[self.packetType] 193 | except IndexError: 194 | packet_name = "Unknown" 195 | 196 | return f"ReasonCode({packet_name}, {self.getName()!r})" 197 | 198 | def __str__(self): 199 | return self.getName() 200 | 201 | def json(self): 202 | return self.getName() 203 | 204 | def pack(self): 205 | return bytearray([self.value]) 206 | 207 | @property 208 | def is_failure(self) -> bool: 209 | return self.value >= 0x80 210 | 211 | 212 | class _CompatibilityIsInstance(type): 213 | def __instancecheck__(self, other: Any) -> bool: 214 | return isinstance(other, ReasonCode) 215 | 216 | 217 | class ReasonCodes(ReasonCode, metaclass=_CompatibilityIsInstance): 218 | def __init__(self, *args, **kwargs): 219 | warnings.warn("ReasonCodes is deprecated, use ReasonCode (singular) instead", 220 | category=DeprecationWarning, 221 | stacklevel=2, 222 | ) 223 | super().__init__(*args, **kwargs) 224 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/ext/paho/mqtt/subscribe.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Roger Light 2 | # 3 | # All rights reserved. This program and the accompanying materials 4 | # are made available under the terms of the Eclipse Public License v2.0 5 | # and Eclipse Distribution License v1.0 which accompany this distribution. 6 | # 7 | # The Eclipse Public License is available at 8 | # http://www.eclipse.org/legal/epl-v20.html 9 | # and the Eclipse Distribution License is available at 10 | # http://www.eclipse.org/org/documents/edl-v10.php. 11 | # 12 | # Contributors: 13 | # Roger Light - initial API and implementation 14 | 15 | """ 16 | This module provides some helper functions to allow straightforward subscribing 17 | to topics and retrieving messages. The two functions are simple(), which 18 | returns one or messages matching a set of topics, and callback() which allows 19 | you to pass a callback for processing of messages. 20 | """ 21 | 22 | from .. import mqtt 23 | from . import client as paho 24 | 25 | 26 | def _on_connect(client, userdata, flags, reason_code, properties): 27 | """Internal callback""" 28 | if reason_code != 0: 29 | raise mqtt.MQTTException(paho.connack_string(reason_code)) 30 | 31 | if isinstance(userdata['topics'], list): 32 | for topic in userdata['topics']: 33 | client.subscribe(topic, userdata['qos']) 34 | else: 35 | client.subscribe(userdata['topics'], userdata['qos']) 36 | 37 | 38 | def _on_message_callback(client, userdata, message): 39 | """Internal callback""" 40 | userdata['callback'](client, userdata['userdata'], message) 41 | 42 | 43 | def _on_message_simple(client, userdata, message): 44 | """Internal callback""" 45 | 46 | if userdata['msg_count'] == 0: 47 | return 48 | 49 | # Don't process stale retained messages if 'retained' was false 50 | if message.retain and not userdata['retained']: 51 | return 52 | 53 | userdata['msg_count'] = userdata['msg_count'] - 1 54 | 55 | if userdata['messages'] is None and userdata['msg_count'] == 0: 56 | userdata['messages'] = message 57 | client.disconnect() 58 | return 59 | 60 | userdata['messages'].append(message) 61 | if userdata['msg_count'] == 0: 62 | client.disconnect() 63 | 64 | 65 | def callback(callback, topics, qos=0, userdata=None, hostname="localhost", 66 | port=1883, client_id="", keepalive=60, will=None, auth=None, 67 | tls=None, protocol=paho.MQTTv311, transport="tcp", 68 | clean_session=True, proxy_args=None): 69 | """Subscribe to a list of topics and process them in a callback function. 70 | 71 | This function creates an MQTT client, connects to a broker and subscribes 72 | to a list of topics. Incoming messages are processed by the user provided 73 | callback. This is a blocking function and will never return. 74 | 75 | :param callback: function with the same signature as `on_message` for 76 | processing the messages received. 77 | 78 | :param topics: either a string containing a single topic to subscribe to, or a 79 | list of topics to subscribe to. 80 | 81 | :param int qos: the qos to use when subscribing. This is applied to all topics. 82 | 83 | :param userdata: passed to the callback 84 | 85 | :param str hostname: the address of the broker to connect to. 86 | Defaults to localhost. 87 | 88 | :param int port: the port to connect to the broker on. Defaults to 1883. 89 | 90 | :param str client_id: the MQTT client id to use. If "" or None, the Paho library will 91 | generate a client id automatically. 92 | 93 | :param int keepalive: the keepalive timeout value for the client. Defaults to 60 94 | seconds. 95 | 96 | :param will: a dict containing will parameters for the client: will = {'topic': 97 | "", 'payload':", 'qos':, 'retain':}. 98 | Topic is required, all other parameters are optional and will 99 | default to None, 0 and False respectively. 100 | 101 | Defaults to None, which indicates no will should be used. 102 | 103 | :param auth: a dict containing authentication parameters for the client: 104 | auth = {'username':"", 'password':""} 105 | Username is required, password is optional and will default to None 106 | if not provided. 107 | Defaults to None, which indicates no authentication is to be used. 108 | 109 | :param tls: a dict containing TLS configuration parameters for the client: 110 | dict = {'ca_certs':"", 'certfile':"", 111 | 'keyfile':"", 'tls_version':"", 112 | 'ciphers':", 'insecure':""} 113 | ca_certs is required, all other parameters are optional and will 114 | default to None if not provided, which results in the client using 115 | the default behaviour - see the paho.mqtt.client documentation. 116 | Alternatively, tls input can be an SSLContext object, which will be 117 | processed using the tls_set_context method. 118 | Defaults to None, which indicates that TLS should not be used. 119 | 120 | :param str transport: set to "tcp" to use the default setting of transport which is 121 | raw TCP. Set to "websockets" to use WebSockets as the transport. 122 | 123 | :param clean_session: a boolean that determines the client type. If True, 124 | the broker will remove all information about this client 125 | when it disconnects. If False, the client is a persistent 126 | client and subscription information and queued messages 127 | will be retained when the client disconnects. 128 | Defaults to True. 129 | 130 | :param proxy_args: a dictionary that will be given to the client. 131 | """ 132 | 133 | if qos < 0 or qos > 2: 134 | raise ValueError('qos must be in the range 0-2') 135 | 136 | callback_userdata = { 137 | 'callback':callback, 138 | 'topics':topics, 139 | 'qos':qos, 140 | 'userdata':userdata} 141 | 142 | client = paho.Client( 143 | paho.CallbackAPIVersion.VERSION2, 144 | client_id=client_id, 145 | userdata=callback_userdata, 146 | protocol=protocol, 147 | transport=transport, 148 | clean_session=clean_session, 149 | ) 150 | client.enable_logger() 151 | 152 | client.on_message = _on_message_callback 153 | client.on_connect = _on_connect 154 | 155 | if proxy_args is not None: 156 | client.proxy_set(**proxy_args) 157 | 158 | if auth: 159 | username = auth.get('username') 160 | if username: 161 | password = auth.get('password') 162 | client.username_pw_set(username, password) 163 | else: 164 | raise KeyError("The 'username' key was not found, this is " 165 | "required for auth") 166 | 167 | if will is not None: 168 | client.will_set(**will) 169 | 170 | if tls is not None: 171 | if isinstance(tls, dict): 172 | insecure = tls.pop('insecure', False) 173 | client.tls_set(**tls) 174 | if insecure: 175 | # Must be set *after* the `client.tls_set()` call since it sets 176 | # up the SSL context that `client.tls_insecure_set` alters. 177 | client.tls_insecure_set(insecure) 178 | else: 179 | # Assume input is SSLContext object 180 | client.tls_set_context(tls) 181 | 182 | client.connect(hostname, port, keepalive) 183 | client.loop_forever() 184 | 185 | 186 | def simple(topics, qos=0, msg_count=1, retained=True, hostname="localhost", 187 | port=1883, client_id="", keepalive=60, will=None, auth=None, 188 | tls=None, protocol=paho.MQTTv311, transport="tcp", 189 | clean_session=True, proxy_args=None): 190 | """Subscribe to a list of topics and return msg_count messages. 191 | 192 | This function creates an MQTT client, connects to a broker and subscribes 193 | to a list of topics. Once "msg_count" messages have been received, it 194 | disconnects cleanly from the broker and returns the messages. 195 | 196 | :param topics: either a string containing a single topic to subscribe to, or a 197 | list of topics to subscribe to. 198 | 199 | :param int qos: the qos to use when subscribing. This is applied to all topics. 200 | 201 | :param int msg_count: the number of messages to retrieve from the broker. 202 | if msg_count == 1 then a single MQTTMessage will be returned. 203 | if msg_count > 1 then a list of MQTTMessages will be returned. 204 | 205 | :param bool retained: If set to True, retained messages will be processed the same as 206 | non-retained messages. If set to False, retained messages will 207 | be ignored. This means that with retained=False and msg_count=1, 208 | the function will return the first message received that does 209 | not have the retained flag set. 210 | 211 | :param str hostname: the address of the broker to connect to. 212 | Defaults to localhost. 213 | 214 | :param int port: the port to connect to the broker on. Defaults to 1883. 215 | 216 | :param str client_id: the MQTT client id to use. If "" or None, the Paho library will 217 | generate a client id automatically. 218 | 219 | :param int keepalive: the keepalive timeout value for the client. Defaults to 60 220 | seconds. 221 | 222 | :param will: a dict containing will parameters for the client: will = {'topic': 223 | "", 'payload':", 'qos':, 'retain':}. 224 | Topic is required, all other parameters are optional and will 225 | default to None, 0 and False respectively. 226 | Defaults to None, which indicates no will should be used. 227 | 228 | :param auth: a dict containing authentication parameters for the client: 229 | auth = {'username':"", 'password':""} 230 | Username is required, password is optional and will default to None 231 | if not provided. 232 | Defaults to None, which indicates no authentication is to be used. 233 | 234 | :param tls: a dict containing TLS configuration parameters for the client: 235 | dict = {'ca_certs':"", 'certfile':"", 236 | 'keyfile':"", 'tls_version':"", 237 | 'ciphers':", 'insecure':""} 238 | ca_certs is required, all other parameters are optional and will 239 | default to None if not provided, which results in the client using 240 | the default behaviour - see the paho.mqtt.client documentation. 241 | Alternatively, tls input can be an SSLContext object, which will be 242 | processed using the tls_set_context method. 243 | Defaults to None, which indicates that TLS should not be used. 244 | 245 | :param protocol: the MQTT protocol version to use. Defaults to MQTTv311. 246 | 247 | :param transport: set to "tcp" to use the default setting of transport which is 248 | raw TCP. Set to "websockets" to use WebSockets as the transport. 249 | 250 | :param clean_session: a boolean that determines the client type. If True, 251 | the broker will remove all information about this client 252 | when it disconnects. If False, the client is a persistent 253 | client and subscription information and queued messages 254 | will be retained when the client disconnects. 255 | Defaults to True. If protocol is MQTTv50, clean_session 256 | is ignored. 257 | 258 | :param proxy_args: a dictionary that will be given to the client. 259 | """ 260 | 261 | if msg_count < 1: 262 | raise ValueError('msg_count must be > 0') 263 | 264 | # Set ourselves up to return a single message if msg_count == 1, or a list 265 | # if > 1. 266 | if msg_count == 1: 267 | messages = None 268 | else: 269 | messages = [] 270 | 271 | # Ignore clean_session if protocol is MQTTv50, otherwise Client will raise 272 | if protocol == paho.MQTTv5: 273 | clean_session = None 274 | 275 | userdata = {'retained':retained, 'msg_count':msg_count, 'messages':messages} 276 | 277 | callback(_on_message_simple, topics, qos, userdata, hostname, port, 278 | client_id, keepalive, will, auth, tls, protocol, transport, 279 | clean_session, proxy_args) 280 | 281 | return userdata['messages'] 282 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/ext/paho/mqtt/subscribeoptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | ******************************************************************* 3 | Copyright (c) 2017, 2019 IBM Corp. 4 | 5 | All rights reserved. This program and the accompanying materials 6 | are made available under the terms of the Eclipse Public License v2.0 7 | and Eclipse Distribution License v1.0 which accompany this distribution. 8 | 9 | The Eclipse Public License is available at 10 | http://www.eclipse.org/legal/epl-v20.html 11 | and the Eclipse Distribution License is available at 12 | http://www.eclipse.org/org/documents/edl-v10.php. 13 | 14 | Contributors: 15 | Ian Craggs - initial implementation and/or documentation 16 | ******************************************************************* 17 | """ 18 | 19 | 20 | 21 | class MQTTException(Exception): 22 | pass 23 | 24 | 25 | class SubscribeOptions: 26 | """The MQTT v5.0 subscribe options class. 27 | 28 | The options are: 29 | qos: As in MQTT v3.1.1. 30 | noLocal: True or False. If set to True, the subscriber will not receive its own publications. 31 | retainAsPublished: True or False. If set to True, the retain flag on received publications will be as set 32 | by the publisher. 33 | retainHandling: RETAIN_SEND_ON_SUBSCRIBE, RETAIN_SEND_IF_NEW_SUB or RETAIN_DO_NOT_SEND 34 | Controls when the broker should send retained messages: 35 | - RETAIN_SEND_ON_SUBSCRIBE: on any successful subscribe request 36 | - RETAIN_SEND_IF_NEW_SUB: only if the subscribe request is new 37 | - RETAIN_DO_NOT_SEND: never send retained messages 38 | """ 39 | 40 | # retain handling options 41 | RETAIN_SEND_ON_SUBSCRIBE, RETAIN_SEND_IF_NEW_SUB, RETAIN_DO_NOT_SEND = range( 42 | 0, 3) 43 | 44 | def __init__( 45 | self, 46 | qos: int = 0, 47 | noLocal: bool = False, 48 | retainAsPublished: bool = False, 49 | retainHandling: int = RETAIN_SEND_ON_SUBSCRIBE, 50 | ): 51 | """ 52 | qos: 0, 1 or 2. 0 is the default. 53 | noLocal: True or False. False is the default and corresponds to MQTT v3.1.1 behavior. 54 | retainAsPublished: True or False. False is the default and corresponds to MQTT v3.1.1 behavior. 55 | retainHandling: RETAIN_SEND_ON_SUBSCRIBE, RETAIN_SEND_IF_NEW_SUB or RETAIN_DO_NOT_SEND 56 | RETAIN_SEND_ON_SUBSCRIBE is the default and corresponds to MQTT v3.1.1 behavior. 57 | """ 58 | object.__setattr__(self, "names", 59 | ["QoS", "noLocal", "retainAsPublished", "retainHandling"]) 60 | self.QoS = qos # bits 0,1 61 | self.noLocal = noLocal # bit 2 62 | self.retainAsPublished = retainAsPublished # bit 3 63 | self.retainHandling = retainHandling # bits 4 and 5: 0, 1 or 2 64 | if self.retainHandling not in (0, 1, 2): 65 | raise AssertionError(f"Retain handling should be 0, 1 or 2, not {self.retainHandling}") 66 | if self.QoS not in (0, 1, 2): 67 | raise AssertionError(f"QoS should be 0, 1 or 2, not {self.QoS}") 68 | 69 | def __setattr__(self, name, value): 70 | if name not in self.names: 71 | raise MQTTException( 72 | f"{name} Attribute name must be one of {self.names}") 73 | object.__setattr__(self, name, value) 74 | 75 | def pack(self): 76 | if self.retainHandling not in (0, 1, 2): 77 | raise AssertionError(f"Retain handling should be 0, 1 or 2, not {self.retainHandling}") 78 | if self.QoS not in (0, 1, 2): 79 | raise AssertionError(f"QoS should be 0, 1 or 2, not {self.QoS}") 80 | noLocal = 1 if self.noLocal else 0 81 | retainAsPublished = 1 if self.retainAsPublished else 0 82 | data = [(self.retainHandling << 4) | (retainAsPublished << 3) | 83 | (noLocal << 2) | self.QoS] 84 | return bytes(data) 85 | 86 | def unpack(self, buffer): 87 | b0 = buffer[0] 88 | self.retainHandling = ((b0 >> 4) & 0x03) 89 | self.retainAsPublished = True if ((b0 >> 3) & 0x01) == 1 else False 90 | self.noLocal = True if ((b0 >> 2) & 0x01) == 1 else False 91 | self.QoS = (b0 & 0x03) 92 | if self.retainHandling not in (0, 1, 2): 93 | raise AssertionError(f"Retain handling should be 0, 1 or 2, not {self.retainHandling}") 94 | if self.QoS not in (0, 1, 2): 95 | raise AssertionError(f"QoS should be 0, 1 or 2, not {self.QoS}") 96 | return 1 97 | 98 | def __repr__(self): 99 | return str(self) 100 | 101 | def __str__(self): 102 | return "{QoS="+str(self.QoS)+", noLocal="+str(self.noLocal) +\ 103 | ", retainAsPublished="+str(self.retainAsPublished) +\ 104 | ", retainHandling="+str(self.retainHandling)+"}" 105 | 106 | def json(self): 107 | data = { 108 | "QoS": self.QoS, 109 | "noLocal": self.noLocal, 110 | "retainAsPublished": self.retainAsPublished, 111 | "retainHandling": self.retainHandling, 112 | } 113 | return data 114 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/ext/velib_python/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Victron Energy BV 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/ext/velib_python/README.md: -------------------------------------------------------------------------------- 1 | velib_python 2 | ============ 3 | 4 | [![Build Status](https://travis-ci.com/victronenergy/velib_python.svg?branch=master)](https://travis-ci.org/victronenergy/velib_python) 5 | 6 | This is the general python library within Victron. It contains code that is related to D-Bus and the Color 7 | Control GX. See http://www.victronenergy.com/panel-systems-remote-monitoring/colorcontrol/ for more 8 | infomation about that panel. 9 | 10 | Files busitem.py, dbusitem.py and tracing.py are deprecated. 11 | 12 | The main files are vedbus.py, dbusmonitor.py and settingsdevice.py. 13 | 14 | - Use VeDbusService to put your process on dbus and let other services interact with you. 15 | - Use VeDbusItemImport to read a single value from other processes the dbus, and monitor its signals. 16 | - Use DbusMonitor to monitor multiple values from other processes 17 | - Use SettingsDevice to store your settings in flash, via the com.victronenergy.settings dbus service. See 18 | https://github.com/victronenergy/localsettings for more info. 19 | 20 | Code style 21 | ========== 22 | 23 | Comply with PEP8, except: 24 | - use tabs instead of spaces, since we use tabs for all projects within Victron. 25 | - max line length = 110 26 | 27 | Run this command to set git diff to tabsize is 4 spaces. Replace --local with --global to do it globally for the current 28 | user account. 29 | 30 | git config --local core.pager 'less -x4' 31 | 32 | Run this command to check your code agains PEP8 33 | 34 | pep8 --max-line-length=110 --ignore=W191 *.py 35 | 36 | D-Bus 37 | ===== 38 | 39 | D-Bus is an/the inter process communication bus used on Linux for many things. Victron uses it on the CCGX to have all the different processes exchange data. Protocol drivers publish data read from products (for example battery voltage) on the D-Bus, and other processes (the GUI for example) takes it from the D-Bus to show it on the display. 40 | 41 | Libraries that implement D-Bus connectivity are available in many programming languages (C, Python, etc). There are also many commandline tools available to talk to a running process via D-bus. See for example the dbuscli (executeable name dbus): http://code.google.com/p/dbus-tools/wiki/DBusCli, and also dbus-monitor and dbus-send. 42 | 43 | There are two sides in using the D-Bus, putting information on it (exporting as service with objects) and reading/writing to a process exporting a service. Note that instead of reading with GetValue, you can also subscribe to receive a signal when datachanges. Doing this saves unncessary context-switches in most cases. 44 | 45 | To get an idea of how to publish data on the dbus, run the example: 46 | 47 | matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ python vedbusservice_example.py 48 | vedbusservice_example.py starting up 49 | /Position value is 5 50 | /Position value is now 10 51 | try changing our RPM by executing the following command from a terminal 52 | 53 | dbus-send --print-reply --dest=com.victronenergy.example /RPM com.victronenergy.BusItem.SetValue int32:1200 54 | Reply will be <> 0 for values > 1000: not accepted. And reply will be 0 for values < 1000: accepted. 55 | 56 | Leave that terminal open, start a second terminal, and interrogate above service from the commandline: 57 | 58 | matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus 59 | org.freedesktop.DBus 60 | org.freedesktop.PowerManagement 61 | com.victronenergy.example 62 | org.xfce.Terminal5 63 | org.xfce.Xfconf 64 | [and many more services in which we are not interested] 65 | 66 | To get more details, add the servicename: 67 | 68 | matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus com.victronenergy.example 69 | / 70 | /Float 71 | /Int 72 | /NegativeInt 73 | /Position 74 | /RPM 75 | /String 76 | 77 | And get the value for the position: 78 | 79 | matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus com.victronenergy.example /RPM GetValue 80 | 100 81 | 82 | And setting the value is also possible, the % makes dbus evaluate what comes behind it, resulting in an int instead of the default (a string).: 83 | 84 | matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus com.victronenergy.example /RPM SetValue %1 85 | 0 86 | 87 | In this example, the 0 indicates succes. When trying an unsupported value, 2000, this is what happens: 88 | 89 | matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus com.victronenergy.example /RPM SetValue %2000 90 | 2 91 | 92 | Exporting services, and the object paths (/Float, /Position, /Group1/Value1, etcetera) is standard D-Bus functionality. At Victron we designed and implemented a D-Bus interface, called com.victronenergy.BusItem. Example showing all interfaces supported by an object: 93 | 94 | matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus com.victronenergy.example /RPM 95 | Interface org.freedesktop.DBus.Introspectable: 96 | String Introspect() 97 | 98 | Interface com.victronenergy.BusItem: 99 | Int32 SetValue(Variant newvalue) 100 | String GetDescription(String language, Int32 length) 101 | String GetText() 102 | Variant GetValue() 103 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/ext/velib_python/ve_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | from traceback import print_exc 5 | from os import _exit as os_exit 6 | from os import statvfs 7 | from subprocess import check_output, CalledProcessError 8 | import logging 9 | import dbus 10 | logger = logging.getLogger(__name__) 11 | 12 | VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) 13 | 14 | class NoVrmPortalIdError(Exception): 15 | pass 16 | 17 | # Use this function to make sure the code quits on an unexpected exception. Make sure to use it 18 | # when using GLib.idle_add and also GLib.timeout_add. 19 | # Without this, the code will just keep running, since GLib does not stop the mainloop on an 20 | # exception. 21 | # Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2) 22 | def exit_on_error(func, *args, **kwargs): 23 | try: 24 | return func(*args, **kwargs) 25 | except: 26 | try: 27 | print ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit') 28 | print_exc() 29 | except: 30 | pass 31 | 32 | # sys.exit() is not used, since that throws an exception, which does not lead to a program 33 | # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. 34 | os_exit(1) 35 | 36 | 37 | __vrm_portal_id = None 38 | def get_vrm_portal_id(): 39 | # The original definition of the VRM Portal ID is that it is the mac 40 | # address of the onboard- ethernet port (eth0), stripped from its colons 41 | # (:) and lower case. This may however differ between platforms. On Venus 42 | # the task is therefore deferred to /sbin/get-unique-id so that a 43 | # platform specific method can be easily defined. 44 | # 45 | # If /sbin/get-unique-id does not exist, then use the ethernet address 46 | # of eth0. This also handles the case where velib_python is used as a 47 | # package install on a Raspberry Pi. 48 | # 49 | # On a Linux host where the network interface may not be eth0, you can set 50 | # the VRM_IFACE environment variable to the correct name. 51 | 52 | global __vrm_portal_id 53 | 54 | if __vrm_portal_id: 55 | return __vrm_portal_id 56 | 57 | portal_id = None 58 | 59 | # First try the method that works if we don't have a data partition. This 60 | # will fail when the current user is not root. 61 | try: 62 | portal_id = check_output("/sbin/get-unique-id").decode("utf-8", "ignore").strip() 63 | if not portal_id: 64 | raise NoVrmPortalIdError("get-unique-id returned blank") 65 | __vrm_portal_id = portal_id 66 | return portal_id 67 | except CalledProcessError: 68 | # get-unique-id returned non-zero 69 | raise NoVrmPortalIdError("get-unique-id returned non-zero") 70 | except OSError: 71 | # File doesn't exist, use fallback 72 | pass 73 | 74 | # Fall back to getting our id using a syscall. Assume we are on linux. 75 | # Allow the user to override what interface is used using an environment 76 | # variable. 77 | import fcntl, socket, struct, os 78 | 79 | iface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii') 80 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 81 | try: 82 | info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) 83 | except IOError: 84 | raise NoVrmPortalIdError("ioctl failed for eth0") 85 | 86 | __vrm_portal_id = info[18:24].hex() 87 | return __vrm_portal_id 88 | 89 | 90 | # See VE.Can registers - public.docx for definition of this conversion 91 | def convert_vreg_version_to_readable(version): 92 | def str_to_arr(x, length): 93 | a = [] 94 | for i in range(0, len(x), length): 95 | a.append(x[i:i+length]) 96 | return a 97 | 98 | x = "%x" % version 99 | x = x.upper() 100 | 101 | if len(x) == 5 or len(x) == 3 or len(x) == 1: 102 | x = '0' + x 103 | 104 | a = str_to_arr(x, 2); 105 | 106 | # remove the first 00 if there are three bytes and it is 00 107 | if len(a) == 3 and a[0] == '00': 108 | a.remove(0); 109 | 110 | # if we have two or three bytes now, and the first character is a 0, remove it 111 | if len(a) >= 2 and a[0][0:1] == '0': 112 | a[0] = a[0][1]; 113 | 114 | result = '' 115 | for item in a: 116 | result += ('.' if result != '' else '') + item 117 | 118 | 119 | result = 'v' + result 120 | 121 | return result 122 | 123 | 124 | def get_free_space(path): 125 | result = -1 126 | 127 | try: 128 | s = statvfs(path) 129 | result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users 130 | except Exception as ex: 131 | logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) 132 | 133 | return result 134 | 135 | 136 | def _get_sysfs_machine_name(): 137 | try: 138 | with open('/sys/firmware/devicetree/base/model', 'r') as f: 139 | return f.read().rstrip('\x00') 140 | except IOError: 141 | pass 142 | 143 | return None 144 | 145 | # Returns None if it cannot find a machine name. Otherwise returns the string 146 | # containing the name 147 | def get_machine_name(): 148 | # First try calling the venus utility script 149 | try: 150 | return check_output("/usr/bin/product-name").strip().decode('UTF-8') 151 | except (CalledProcessError, OSError): 152 | pass 153 | 154 | # Fall back to sysfs 155 | name = _get_sysfs_machine_name() 156 | if name is not None: 157 | return name 158 | 159 | # Fall back to venus build machine name 160 | try: 161 | with open('/etc/venus/machine', 'r', encoding='UTF-8') as f: 162 | return f.read().strip() 163 | except IOError: 164 | pass 165 | 166 | return None 167 | 168 | 169 | def get_product_id(): 170 | """ Find the machine ID and return it. """ 171 | 172 | # First try calling the venus utility script 173 | try: 174 | return check_output("/usr/bin/product-id").strip().decode('UTF-8') 175 | except (CalledProcessError, OSError): 176 | pass 177 | 178 | # Fall back machine name mechanism 179 | name = _get_sysfs_machine_name() 180 | return { 181 | 'Color Control GX': 'C001', 182 | 'Venus GX': 'C002', 183 | 'Octo GX': 'C006', 184 | 'EasySolar-II': 'C007', 185 | 'MultiPlus-II': 'C008', 186 | 'Maxi GX': 'C009', 187 | 'Cerbo GX': 'C00A' 188 | }.get(name, 'C003') # C003 is Generic 189 | 190 | 191 | # Returns False if it cannot open the file. Otherwise returns its rstripped contents 192 | def read_file(path): 193 | content = False 194 | 195 | try: 196 | with open(path, 'r') as f: 197 | content = f.read().rstrip() 198 | except Exception as ex: 199 | logger.debug("Error while reading %s: %s" % (path, ex)) 200 | 201 | return content 202 | 203 | 204 | def wrap_dbus_value(value): 205 | if value is None: 206 | return VEDBUS_INVALID 207 | if isinstance(value, float): 208 | return dbus.Double(value, variant_level=1) 209 | if isinstance(value, bool): 210 | return dbus.Boolean(value, variant_level=1) 211 | if isinstance(value, int): 212 | try: 213 | return dbus.Int32(value, variant_level=1) 214 | except OverflowError: 215 | return dbus.Int64(value, variant_level=1) 216 | if isinstance(value, str): 217 | return dbus.String(value, variant_level=1) 218 | if isinstance(value, list): 219 | if len(value) == 0: 220 | # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. 221 | # A (signed) integer is dangerous, because an empty list of signed integers is used to encode 222 | # an invalid value. 223 | return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) 224 | return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) 225 | if isinstance(value, dict): 226 | # Wrapping the keys of the dictionary causes D-Bus errors like: 227 | # 'arguments to dbus_message_iter_open_container() were incorrect, 228 | # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && 229 | # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || 230 | # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' 231 | return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) 232 | return value 233 | 234 | 235 | dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) 236 | 237 | 238 | def unwrap_dbus_value(val): 239 | """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, 240 | a float will be returned.""" 241 | if isinstance(val, dbus_int_types): 242 | return int(val) 243 | if isinstance(val, dbus.Double): 244 | return float(val) 245 | if isinstance(val, dbus.Array): 246 | v = [unwrap_dbus_value(x) for x in val] 247 | return None if len(v) == 0 else v 248 | if isinstance(val, (dbus.Signature, dbus.String)): 249 | return str(val) 250 | # Python has no byte type, so we convert to an integer. 251 | if isinstance(val, dbus.Byte): 252 | return int(val) 253 | if isinstance(val, dbus.ByteArray): 254 | return "".join([bytes(x) for x in val]) 255 | if isinstance(val, (list, tuple)): 256 | return [unwrap_dbus_value(x) for x in val] 257 | if isinstance(val, (dbus.Dictionary, dict)): 258 | # Do not unwrap the keys, see comment in wrap_dbus_value 259 | return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) 260 | if isinstance(val, dbus.Boolean): 261 | return bool(val) 262 | return val 263 | 264 | # When supported, only name owner changes for the the given namespace are reported. This 265 | # prevents spending cpu time at irrelevant changes, like scripts accessing the bus temporarily. 266 | def add_name_owner_changed_receiver(dbus, name_owner_changed, namespace="com.victronenergy"): 267 | # support for arg0namespace is submitted upstream, but not included at the time of 268 | # writing, Venus OS does support it, so try if it works. 269 | if namespace is None: 270 | dbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged') 271 | else: 272 | try: 273 | dbus.add_signal_receiver(name_owner_changed, 274 | signal_name='NameOwnerChanged', arg0namespace=namespace) 275 | except TypeError: 276 | dbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged') 277 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/ext/velib_python/vedbus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import dbus.service 5 | import logging 6 | import traceback 7 | import os 8 | import weakref 9 | from collections import defaultdict 10 | from ve_utils import wrap_dbus_value, unwrap_dbus_value 11 | 12 | # vedbus contains three classes: 13 | # VeDbusItemImport -> use this to read data from the dbus, ie import 14 | # VeDbusItemExport -> use this to export data to the dbus (one value) 15 | # VeDbusService -> use that to create a service and export several values to the dbus 16 | 17 | # Code for VeDbusItemImport is copied from busitem.py and thereafter modified. 18 | # All projects that used busitem.py need to migrate to this package. And some 19 | # projects used to define there own equivalent of VeDbusItemExport. Better to 20 | # use VeDbusItemExport, or even better the VeDbusService class that does it all for you. 21 | 22 | # TODOS 23 | # 1 check for datatypes, it works now, but not sure if all is compliant with 24 | # com.victronenergy.BusItem interface definition. See also the files in 25 | # tests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps 26 | # something similar should also be done in VeDbusBusItemExport? 27 | # 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object? 28 | # 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking 29 | # changes possible. Does everybody first invalidate its data before leaving the bus? 30 | # And what about before taking one object away from the bus, instead of taking the 31 | # whole service offline? 32 | # They should! And after taking one value away, do we need to know that someone left 33 | # the bus? Or we just keep that value in invalidated for ever? Result is that we can't 34 | # see the difference anymore between an invalidated value and a value that was first on 35 | # the bus and later not anymore. See comments above VeDbusItemImport as well. 36 | # 9 there are probably more todos in the code below. 37 | 38 | # Some thoughts with regards to the data types: 39 | # 40 | # Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types 41 | # --- 42 | # Variants are represented by setting the variant_level keyword argument in the 43 | # constructor of any D-Bus data type to a value greater than 0 (variant_level 1 44 | # means a variant containing some other data type, variant_level 2 means a variant 45 | # containing a variant containing some other data type, and so on). If a non-variant 46 | # is passed as an argument but introspection indicates that a variant is expected, 47 | # it'll automatically be wrapped in a variant. 48 | # --- 49 | # 50 | # Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass 51 | # of Python int. dbus.String is a subclass of Python standard class unicode, etcetera 52 | # 53 | # So all together that explains why we don't need to explicitly convert back and forth 54 | # between the dbus datatypes and the standard python datatypes. Note that all datatypes 55 | # in python are objects. Even an int is an object. 56 | 57 | # The signature of a variant is 'v'. 58 | 59 | # Export ourselves as a D-Bus service. 60 | class VeDbusService(object): 61 | def __init__(self, servicename, bus=None, register=True): 62 | # dict containing the VeDbusItemExport objects, with their path as the key. 63 | self._dbusobjects = {} 64 | self._dbusnodes = {} 65 | self._ratelimiters = [] 66 | self._dbusname = None 67 | self.name = servicename 68 | 69 | # dict containing the onchange callbacks, for each object. Object path is the key 70 | self._onchangecallbacks = {} 71 | 72 | # Connect to session bus whenever present, else use the system bus 73 | self._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus()) 74 | 75 | # make the dbus connection available to outside, could make this a true property instead, but ach.. 76 | self.dbusconn = self._dbusconn 77 | 78 | # Add the root item that will return all items as a tree 79 | self._dbusnodes['/'] = VeDbusRootExport(self._dbusconn, '/', self) 80 | 81 | # Immediately register the service unless requested not to 82 | if register: 83 | logging.warning("USING OUTDATED REGISTRATION METHOD!") 84 | logging.warning("Please set register=False, then call the register method " 85 | "after adding all mandatory paths. See " 86 | "https://github.com/victronenergy/venus/wiki/dbus-api") 87 | self.register() 88 | 89 | def register(self): 90 | # Register ourselves on the dbus, trigger an error if already in use (do_not_queue) 91 | self._dbusname = dbus.service.BusName(self.name, self._dbusconn, do_not_queue=True) 92 | logging.info("registered ourselves on D-Bus as %s" % self.name) 93 | 94 | # To force immediate deregistering of this dbus service and all its object paths, explicitly 95 | # call __del__(). 96 | def __del__(self): 97 | for node in list(self._dbusnodes.values()): 98 | node.__del__() 99 | self._dbusnodes.clear() 100 | for item in list(self._dbusobjects.values()): 101 | item.__del__() 102 | self._dbusobjects.clear() 103 | if self._dbusname: 104 | self._dbusname.__del__() # Forces call to self._bus.release_name(self._name), see source code 105 | self._dbusname = None 106 | 107 | def get_name(self): 108 | return self._dbusname.get_name() 109 | 110 | # @param callbackonchange function that will be called when this value is changed. First parameter will 111 | # be the path of the object, second the new value. This callback should return 112 | # True to accept the change, False to reject it. 113 | def add_path(self, path, value, description="", writeable=False, 114 | onchangecallback=None, gettextcallback=None, valuetype=None, itemtype=None): 115 | 116 | if onchangecallback is not None: 117 | self._onchangecallbacks[path] = onchangecallback 118 | 119 | itemtype = itemtype or VeDbusItemExport 120 | item = itemtype(self._dbusconn, path, value, description, writeable, 121 | self._value_changed, gettextcallback, deletecallback=self._item_deleted, valuetype=valuetype) 122 | 123 | spl = path.split('/') 124 | for i in range(2, len(spl)): 125 | subPath = '/'.join(spl[:i]) 126 | if subPath not in self._dbusnodes and subPath not in self._dbusobjects: 127 | self._dbusnodes[subPath] = VeDbusTreeExport(self._dbusconn, subPath, self) 128 | self._dbusobjects[path] = item 129 | logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable)) 130 | return item 131 | 132 | # Add the mandatory paths, as per victron dbus api doc 133 | def add_mandatory_paths(self, processname, processversion, connection, 134 | deviceinstance, productid, productname, firmwareversion, hardwareversion, connected): 135 | self.add_path('/Mgmt/ProcessName', processname) 136 | self.add_path('/Mgmt/ProcessVersion', processversion) 137 | self.add_path('/Mgmt/Connection', connection) 138 | 139 | # Create rest of the mandatory objects 140 | self.add_path('/DeviceInstance', deviceinstance) 141 | self.add_path('/ProductId', productid) 142 | self.add_path('/ProductName', productname) 143 | self.add_path('/FirmwareVersion', firmwareversion) 144 | self.add_path('/HardwareVersion', hardwareversion) 145 | self.add_path('/Connected', connected) 146 | 147 | # Callback function that is called from the VeDbusItemExport objects when a value changes. This function 148 | # maps the change-request to the onchangecallback given to us for this specific path. 149 | def _value_changed(self, path, newvalue): 150 | if path not in self._onchangecallbacks: 151 | return True 152 | 153 | return self._onchangecallbacks[path](path, newvalue) 154 | 155 | def _item_deleted(self, path): 156 | self._dbusobjects.pop(path) 157 | for np in list(self._dbusnodes.keys()): 158 | if np != '/': 159 | for ip in self._dbusobjects: 160 | if ip.startswith(np + '/'): 161 | break 162 | else: 163 | self._dbusnodes[np].__del__() 164 | self._dbusnodes.pop(np) 165 | 166 | def __getitem__(self, path): 167 | return self._dbusobjects[path].local_get_value() 168 | 169 | def __setitem__(self, path, newvalue): 170 | self._dbusobjects[path].local_set_value(newvalue) 171 | 172 | def __delitem__(self, path): 173 | self._dbusobjects[path].__del__() # Invalidates and then removes the object path 174 | assert path not in self._dbusobjects 175 | 176 | def __contains__(self, path): 177 | return path in self._dbusobjects 178 | 179 | def __enter__(self): 180 | l = ServiceContext(self) 181 | self._ratelimiters.append(l) 182 | return l 183 | 184 | def __exit__(self, *exc): 185 | # pop off the top one and flush it. If with statements are nested 186 | # then each exit flushes its own part. 187 | if self._ratelimiters: 188 | self._ratelimiters.pop().flush() 189 | 190 | class ServiceContext(object): 191 | def __init__(self, parent): 192 | self.parent = parent 193 | self.changes = {} 194 | 195 | def __contains__(self, path): 196 | return path in self.parent 197 | 198 | def __getitem__(self, path): 199 | return self.parent[path] 200 | 201 | def __setitem__(self, path, newvalue): 202 | c = self.parent._dbusobjects[path]._local_set_value(newvalue) 203 | if c is not None: 204 | self.changes[path] = c 205 | 206 | def __delitem__(self, path): 207 | if path in self.changes: 208 | del self.changes[path] 209 | del self.parent[path] 210 | 211 | def flush(self): 212 | if self.changes: 213 | self.parent._dbusnodes['/'].ItemsChanged(self.changes) 214 | self.changes.clear() 215 | 216 | def add_path(self, path, value, *args, **kwargs): 217 | self.parent.add_path(path, value, *args, **kwargs) 218 | self.changes[path] = { 219 | 'Value': wrap_dbus_value(value), 220 | 'Text': self.parent._dbusobjects[path].GetText() 221 | } 222 | 223 | def del_tree(self, root): 224 | root = root.rstrip('/') 225 | for p in list(self.parent._dbusobjects.keys()): 226 | if p == root or p.startswith(root + '/'): 227 | self[p] = None 228 | self.parent._dbusobjects[p].__del__() 229 | 230 | def get_name(self): 231 | return self.parent.get_name() 232 | 233 | class TrackerDict(defaultdict): 234 | """ Same as defaultdict, but passes the key to default_factory. """ 235 | def __missing__(self, key): 236 | self[key] = x = self.default_factory(key) 237 | return x 238 | 239 | class VeDbusRootTracker(object): 240 | """ This tracks the root of a dbus path and listens for PropertiesChanged 241 | signals. When a signal arrives, parse it and unpack the key/value changes 242 | into traditional events, then pass it to the original eventCallback 243 | method. """ 244 | def __init__(self, bus, serviceName): 245 | self.importers = defaultdict(weakref.WeakSet) 246 | self.serviceName = serviceName 247 | self._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal( 248 | "ItemsChanged", weak_functor(self._items_changed_handler)) 249 | 250 | def __del__(self): 251 | self._match.remove() 252 | self._match = None 253 | 254 | def add(self, i): 255 | self.importers[i.path].add(i) 256 | 257 | def _items_changed_handler(self, items): 258 | if not isinstance(items, dict): 259 | return 260 | 261 | for path, changes in items.items(): 262 | try: 263 | v = changes['Value'] 264 | except KeyError: 265 | continue 266 | 267 | try: 268 | t = changes['Text'] 269 | except KeyError: 270 | t = str(unwrap_dbus_value(v)) 271 | 272 | for i in self.importers.get(path, ()): 273 | i._properties_changed_handler({'Value': v, 'Text': t}) 274 | 275 | """ 276 | Importing basics: 277 | - If when we power up, the D-Bus service does not exist, or it does exist and the path does not 278 | yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its 279 | initial value, which VeDbusItemImport will receive and use to update local cache. And, when set, 280 | call the eventCallback. 281 | - If when we power up, save it 282 | - When using get_value, know that there is no difference between services (or object paths) that don't 283 | exist and paths that are invalid (= empty array, see above). Both will return None. In case you do 284 | really want to know ifa path exists or not, use the exists property. 285 | - When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals 286 | with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged- 287 | signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this 288 | class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this 289 | class. 290 | 291 | Read when using this class: 292 | Note that when a service leaves that D-Bus without invalidating all its exported objects first, for 293 | example because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport, 294 | make sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor, 295 | because that takes care of all of that for you. 296 | """ 297 | class VeDbusItemImport(object): 298 | def __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True): 299 | instance = object.__new__(cls) 300 | 301 | # If signal tracking should be done, also add to root tracker 302 | if createsignal: 303 | if "_roots" not in cls.__dict__: 304 | cls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k)) 305 | 306 | return instance 307 | 308 | ## Constructor 309 | # @param bus the bus-object (SESSION or SYSTEM). 310 | # @param serviceName the dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1' 311 | # @param path the object-path, for example '/Dc/V' 312 | # @param eventCallback function that you want to be called on a value change 313 | # @param createSignal only set this to False if you use this function to one time read a value. When 314 | # leaving it to True, make sure to also subscribe to the NameOwnerChanged signal 315 | # elsewhere. See also note some 15 lines up. 316 | def __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True): 317 | # TODO: is it necessary to store _serviceName and _path? Isn't it 318 | # stored in the bus_getobjectsomewhere? 319 | self._serviceName = serviceName 320 | self._path = path 321 | self._match = None 322 | # TODO: _proxy is being used in settingsdevice.py, make a getter for that 323 | self._proxy = bus.get_object(serviceName, path, introspect=False) 324 | self.eventCallback = eventCallback 325 | 326 | assert eventCallback is None or createsignal == True 327 | if createsignal: 328 | self._match = self._proxy.connect_to_signal( 329 | "PropertiesChanged", weak_functor(self._properties_changed_handler)) 330 | self._roots[serviceName].add(self) 331 | 332 | # store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to 333 | # None, same as when a value is invalid 334 | self._cachedvalue = None 335 | try: 336 | v = self._proxy.GetValue() 337 | except dbus.exceptions.DBusException: 338 | pass 339 | else: 340 | self._cachedvalue = unwrap_dbus_value(v) 341 | 342 | def __del__(self): 343 | if self._match is not None: 344 | self._match.remove() 345 | self._match = None 346 | self._proxy = None 347 | 348 | def _refreshcachedvalue(self): 349 | self._cachedvalue = unwrap_dbus_value(self._proxy.GetValue()) 350 | 351 | ## Returns the path as a string, for example '/AC/L1/V' 352 | @property 353 | def path(self): 354 | return self._path 355 | 356 | ## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1 357 | @property 358 | def serviceName(self): 359 | return self._serviceName 360 | 361 | ## Returns the value of the dbus-item. 362 | # the type will be a dbus variant, for example dbus.Int32(0, variant_level=1) 363 | # this is not a property to keep the name consistant with the com.victronenergy.busitem interface 364 | # returns None when the property is invalid 365 | def get_value(self): 366 | return self._cachedvalue 367 | 368 | ## Writes a new value to the dbus-item 369 | def set_value(self, newvalue): 370 | r = self._proxy.SetValue(wrap_dbus_value(newvalue)) 371 | 372 | # instead of just saving the value, go to the dbus and get it. So we have the right type etc. 373 | if r == 0: 374 | self._refreshcachedvalue() 375 | 376 | return r 377 | 378 | ## Resets the item to its default value 379 | def set_default(self): 380 | self._proxy.SetDefault() 381 | self._refreshcachedvalue() 382 | 383 | ## Returns the text representation of the value. 384 | # For example when the value is an enum/int GetText might return the string 385 | # belonging to that enum value. Another example, for a voltage, GetValue 386 | # would return a float, 12.0Volt, and GetText could return 12 VDC. 387 | # 388 | # Note that this depends on how the dbus-producer has implemented this. 389 | def get_text(self): 390 | return self._proxy.GetText() 391 | 392 | ## Returns true of object path exists, and false if it doesn't 393 | @property 394 | def exists(self): 395 | # TODO: do some real check instead of this crazy thing. 396 | r = False 397 | try: 398 | r = self._proxy.GetValue() 399 | r = True 400 | except dbus.exceptions.DBusException: 401 | pass 402 | 403 | return r 404 | 405 | ## callback for the trigger-event. 406 | # @param eventCallback the event-callback-function. 407 | @property 408 | def eventCallback(self): 409 | return self._eventCallback 410 | 411 | @eventCallback.setter 412 | def eventCallback(self, eventCallback): 413 | self._eventCallback = eventCallback 414 | 415 | ## Is called when the value of the imported bus-item changes. 416 | # Stores the new value in our local cache, and calls the eventCallback, if set. 417 | def _properties_changed_handler(self, changes): 418 | if "Value" in changes: 419 | changes['Value'] = unwrap_dbus_value(changes['Value']) 420 | self._cachedvalue = changes['Value'] 421 | if self._eventCallback: 422 | # The reason behind this try/except is to prevent errors silently ending up the an error 423 | # handler in the dbus code. 424 | try: 425 | self._eventCallback(self._serviceName, self._path, changes) 426 | except: 427 | traceback.print_exc() 428 | os._exit(1) # sys.exit() is not used, since that also throws an exception 429 | 430 | 431 | class VeDbusTreeExport(dbus.service.Object): 432 | def __init__(self, bus, objectPath, service): 433 | dbus.service.Object.__init__(self, bus, objectPath) 434 | self._service = service 435 | logging.debug("VeDbusTreeExport %s has been created" % objectPath) 436 | 437 | def __del__(self): 438 | # self._get_path() will raise an exception when retrieved after the call to .remove_from_connection, 439 | # so we need a copy. 440 | path = self._get_path() 441 | if path is None: 442 | return 443 | self.remove_from_connection() 444 | logging.debug("VeDbusTreeExport %s has been removed" % path) 445 | 446 | def _get_path(self): 447 | if len(self._locations) == 0: 448 | return None 449 | return self._locations[0][1] 450 | 451 | def _get_value_handler(self, path, get_text=False): 452 | logging.debug("_get_value_handler called for %s" % path) 453 | r = {} 454 | px = path 455 | if not px.endswith('/'): 456 | px += '/' 457 | for p, item in self._service._dbusobjects.items(): 458 | if p.startswith(px): 459 | v = item.GetText() if get_text else wrap_dbus_value(item.local_get_value()) 460 | r[p[len(px):]] = v 461 | logging.debug(r) 462 | return r 463 | 464 | @dbus.service.method('com.victronenergy.BusItem', out_signature='v') 465 | def GetValue(self): 466 | value = self._get_value_handler(self._get_path()) 467 | return dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1) 468 | 469 | @dbus.service.method('com.victronenergy.BusItem', out_signature='v') 470 | def GetText(self): 471 | return self._get_value_handler(self._get_path(), True) 472 | 473 | def local_get_value(self): 474 | return self._get_value_handler(self.path) 475 | 476 | class VeDbusRootExport(VeDbusTreeExport): 477 | @dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}') 478 | def ItemsChanged(self, changes): 479 | pass 480 | 481 | @dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}') 482 | def GetItems(self): 483 | return { 484 | path: { 485 | 'Value': wrap_dbus_value(item.local_get_value()), 486 | 'Text': item.GetText() } 487 | for path, item in self._service._dbusobjects.items() 488 | } 489 | 490 | 491 | class VeDbusItemExport(dbus.service.Object): 492 | ## Constructor of VeDbusItemExport 493 | # 494 | # Use this object to export (publish), values on the dbus 495 | # Creates the dbus-object under the given dbus-service-name. 496 | # @param bus The dbus object. 497 | # @param objectPath The dbus-object-path. 498 | # @param value Value to initialize ourselves with, defaults to None which means Invalid 499 | # @param description String containing a description. Can be called over the dbus with GetDescription() 500 | # @param writeable what would this do!? :). 501 | # @param callback Function that will be called when someone else changes the value of this VeBusItem 502 | # over the dbus. First parameter passed to callback will be our path, second the new 503 | # value. This callback should return True to accept the change, False to reject it. 504 | def __init__(self, bus, objectPath, value=None, description=None, writeable=False, 505 | onchangecallback=None, gettextcallback=None, deletecallback=None, 506 | valuetype=None): 507 | dbus.service.Object.__init__(self, bus, objectPath) 508 | self._onchangecallback = onchangecallback 509 | self._gettextcallback = gettextcallback 510 | self._value = value 511 | self._description = description 512 | self._writeable = writeable 513 | self._deletecallback = deletecallback 514 | self._type = valuetype 515 | 516 | # To force immediate deregistering of this dbus object, explicitly call __del__(). 517 | def __del__(self): 518 | # self._get_path() will raise an exception when retrieved after the 519 | # call to .remove_from_connection, so we need a copy. 520 | path = self._get_path() 521 | if path == None: 522 | return 523 | if self._deletecallback is not None: 524 | self._deletecallback(path) 525 | self.remove_from_connection() 526 | logging.debug("VeDbusItemExport %s has been removed" % path) 527 | 528 | def _get_path(self): 529 | if len(self._locations) == 0: 530 | return None 531 | return self._locations[0][1] 532 | 533 | ## Sets the value. And in case the value is different from what it was, a signal 534 | # will be emitted to the dbus. This function is to be used in the python code that 535 | # is using this class to export values to the dbus. 536 | # set value to None to indicate that it is Invalid 537 | def local_set_value(self, newvalue): 538 | changes = self._local_set_value(newvalue) 539 | if changes is not None: 540 | self.PropertiesChanged(changes) 541 | 542 | def _local_set_value(self, newvalue): 543 | if self._value == newvalue: 544 | return None 545 | 546 | self._value = newvalue 547 | return { 548 | 'Value': wrap_dbus_value(newvalue), 549 | 'Text': self.GetText() 550 | } 551 | 552 | def local_get_value(self): 553 | return self._value 554 | 555 | # ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ==== 556 | 557 | ## Dbus exported method SetValue 558 | # Function is called over the D-Bus by other process. It will first check (via callback) if new 559 | # value is accepted. And it is, stores it and emits a changed-signal. 560 | # @param value The new value. 561 | # @return completion-code When successful a 0 is return, and when not a -1 is returned. 562 | @dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i') 563 | def SetValue(self, newvalue): 564 | if not self._writeable: 565 | return 1 # NOT OK 566 | 567 | newvalue = unwrap_dbus_value(newvalue) 568 | 569 | # If value type is enforced, cast it. If the type can be coerced 570 | # python will do it for us. This allows ints to become floats, 571 | # or bools to become ints. Additionally also allow None, so that 572 | # a path may be invalidated. 573 | if self._type is not None and newvalue is not None: 574 | try: 575 | newvalue = self._type(newvalue) 576 | except (ValueError, TypeError): 577 | return 1 # NOT OK 578 | 579 | if newvalue == self._value: 580 | return 0 # OK 581 | 582 | # call the callback given to us, and check if new value is OK. 583 | if (self._onchangecallback is None or 584 | (self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))): 585 | 586 | self.local_set_value(newvalue) 587 | return 0 # OK 588 | 589 | return 2 # NOT OK 590 | 591 | ## Dbus exported method GetDescription 592 | # 593 | # Returns the a description. 594 | # @param language A language code (e.g. ISO 639-1 en-US). 595 | # @param length Lenght of the language string. 596 | # @return description 597 | @dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s') 598 | def GetDescription(self, language, length): 599 | return self._description if self._description is not None else 'No description given' 600 | 601 | ## Dbus exported method GetValue 602 | # Returns the value. 603 | # @return the value when valid, and otherwise an empty array 604 | @dbus.service.method('com.victronenergy.BusItem', out_signature='v') 605 | def GetValue(self): 606 | return wrap_dbus_value(self._value) 607 | 608 | ## Dbus exported method GetText 609 | # Returns the value as string of the dbus-object-path. 610 | # @return text A text-value. '---' when local value is invalid 611 | @dbus.service.method('com.victronenergy.BusItem', out_signature='s') 612 | def GetText(self): 613 | if self._value is None: 614 | return '---' 615 | 616 | # Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we 617 | # have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from 618 | # the application itself, as all data from the D-Bus should have been unwrapped by now. 619 | if self._gettextcallback is None and type(self._value) == dbus.Byte: 620 | return str(int(self._value)) 621 | 622 | if self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId': 623 | return "0x%X" % self._value 624 | 625 | if self._gettextcallback is None: 626 | return str(self._value) 627 | 628 | return self._gettextcallback(self.__dbus_object_path__, self._value) 629 | 630 | ## The signal that indicates that the value has changed. 631 | # Other processes connected to this BusItem object will have subscribed to the 632 | # event when they want to track our state. 633 | @dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}') 634 | def PropertiesChanged(self, changes): 635 | pass 636 | 637 | ## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference 638 | ## to the object which method is to be called. 639 | ## Use this object to break circular references. 640 | class weak_functor: 641 | def __init__(self, f): 642 | self._r = weakref.ref(f.__self__) 643 | self._f = weakref.ref(f.__func__) 644 | 645 | def __call__(self, *args, **kargs): 646 | r = self._r() 647 | f = self._f() 648 | if r == None or f == None: 649 | return 650 | f(r, *args, **kargs) 651 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 3 | SERVICE_NAME=$(basename $SCRIPT_DIR) 4 | 5 | echo 6 | echo "Installing $SERVICE_NAME..." 7 | 8 | # set permissions for script files 9 | echo "Setting permissions..." 10 | chmod 755 $SCRIPT_DIR/*.py 11 | chmod 755 $SCRIPT_DIR/*.sh 12 | chmod 755 $SCRIPT_DIR/service/run 13 | chmod 755 $SCRIPT_DIR/service/log/run 14 | 15 | # check dependencies 16 | python -c "import paho.mqtt.client" 17 | if [ $? -gt 0 ] 18 | then 19 | echo "Installing paho.mqtt.client..." 20 | # install paho.mqtt.client 21 | python -m pip install paho-mqtt 22 | if [ $? -gt 0 ] 23 | then 24 | # if pip command fails install pip and then try again 25 | opkg update && opkg install python3-pip 26 | python -m pip install paho-mqtt 27 | fi 28 | fi 29 | 30 | # create sym-link to run script in deamon 31 | if [ ! -L /service/$SERVICE_NAME ]; then 32 | echo "Creating service..." 33 | ln -s $SCRIPT_DIR/service /service/$SERVICE_NAME 34 | else 35 | echo "Service already exists." 36 | fi 37 | 38 | # add install-script to rc.local to be ready for firmware update 39 | filename=/data/rc.local 40 | if [ ! -f $filename ] 41 | then 42 | touch $filename 43 | chmod 755 $filename 44 | echo "#!/bin/bash" >> $filename 45 | echo >> $filename 46 | fi 47 | 48 | # if not alreay added, then add to rc.local 49 | grep -qxF "bash $SCRIPT_DIR/install.sh" $filename || echo "bash $SCRIPT_DIR/install.sh" >> $filename 50 | 51 | echo 52 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 3 | SERVICE_NAME=$(basename $SCRIPT_DIR) 4 | 5 | echo 6 | echo "Restarting $SERVICE_NAME..." 7 | 8 | pid=$(pgrep -f "python $SCRIPT_DIR/$SERVICE_NAME.py") 9 | if [ -n "$pid" ]; then 10 | svc -t /service/$SERVICE_NAME 11 | pkill -f "python $SCRIPT_DIR/$SERVICE_NAME.py" > /dev/null 2>&1 12 | echo "done." 13 | else 14 | echo "driver is not running!" 15 | fi 16 | 17 | echo 18 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/service/log/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec multilog t s25000 n4 /var/log/dbus-mqtt-temperature 3 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/service/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "*** starting dbus-mqtt-temperature ***" 3 | exec 2>&1 4 | exec python /data/etc/dbus-mqtt-temperature/dbus-mqtt-temperature.py 5 | -------------------------------------------------------------------------------- /dbus-mqtt-temperature/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 3 | SERVICE_NAME=$(basename $SCRIPT_DIR) 4 | 5 | # make sure SERVICE_NAME is set and not empty 6 | if [ -z "$SERVICE_NAME" ]; then 7 | echo "Error: SERVICE_NAME is not set." 8 | exit 1 9 | fi 10 | 11 | echo 12 | echo "Uninstalling $SERVICE_NAME..." 13 | 14 | # Remove driver from rc.local 15 | echo "Removing driver from rc.local..." 16 | sed -i "/$SERVICE_NAME/d" /data/rc.local 17 | 18 | # Stop the service 19 | echo "Stopping service..." 20 | svc -d /service/$SERVICE_NAME 21 | 22 | sleep 1 23 | 24 | # Remove service driver 25 | echo "Removing driver from services..." 26 | rm /service/$SERVICE_NAME 27 | 28 | # kill 29 | pkill -f "supervise .*$SERVICE_NAME" 30 | pkill -f "multilog .*$SERVICE_NAME" 31 | pkill -f "python .*$SERVICE_NAME" 32 | 33 | echo "done." 34 | echo 35 | 36 | # Ask the user if they want to delete the service folder 37 | echo "Do you also want to delete all driver files including the config? [y/N]" 38 | read -r DELETE_FILES 39 | 40 | if [[ "$DELETE_FILES" == "y" || "$DELETE_FILES" == "Y" ]]; then 41 | echo "Deleting all driver files..." 42 | rm -rf "$SCRIPT_DIR" 43 | echo "done." 44 | else 45 | echo "Driver files not deleted." 46 | fi 47 | 48 | echo 49 | echo "*** Please reboot your device to complete the uninstallation. ***" 50 | echo 51 | -------------------------------------------------------------------------------- /download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | driver_path="/data/etc" 4 | driver_name="dbus-mqtt-temperature" 5 | 6 | echo "" 7 | echo "" 8 | 9 | # fetch version numbers for different versions 10 | echo -n "Fetch current version numbers..." 11 | 12 | # latest release 13 | latest_release_stable=$(curl -s https://api.github.com/repos/mr-manuel/venus-os_${driver_name}/releases/latest | grep "tag_name" | cut -d : -f 2,3 | tr -d "\ " | tr -d \" | tr -d \,) 14 | 15 | # nightly build 16 | latest_release_nightly=$(curl -s https://raw.githubusercontent.com/mr-manuel/venus-os_${driver_name}/master/${driver_name}/${driver_name}.py | grep FirmwareVersion | awk -F'"' '{print $4}') 17 | 18 | 19 | echo 20 | PS3=$'\nSelect which version you want to install and enter the corresponding number: ' 21 | 22 | # create list of versions 23 | version_list=( 24 | "latest release \"$latest_release_stable\"" 25 | "nightly build \"v$latest_release_nightly\"" 26 | "quit" 27 | ) 28 | 29 | select version in "${version_list[@]}" 30 | do 31 | case $version in 32 | "latest release \"$latest_release_stable\"") 33 | break 34 | ;; 35 | "nightly build \"v$latest_release_nightly\"") 36 | break 37 | ;; 38 | "quit") 39 | exit 0 40 | ;; 41 | *) 42 | echo "> Invalid option: $REPLY. Please enter a number!" 43 | ;; 44 | esac 45 | done 46 | 47 | echo "> Selected: $version" 48 | echo "" 49 | 50 | 51 | # Which driver instance do you want to install? 52 | echo "Which driver instance do you want to install/update?" 53 | while true; do 54 | read -p "Enter the driver instance number you want to install/update. If you don't know just press enter [1]: " driver_instance 55 | if [[ -z "$driver_instance" || ( "$driver_instance" =~ ^[0-9]+$ && "$driver_instance" -ge 1 && "$driver_instance" -le 99 ) ]]; then 56 | break 57 | else 58 | echo "Invalid input. Please enter a number between 1 and 255 or press enter." 59 | fi 60 | done 61 | 62 | if [ -n "$driver_instance" ] && [ "$driver_instance" != "1" ]; then 63 | driver_name_instance="${driver_name}-${driver_instance}" 64 | else 65 | driver_name_instance=${driver_name} 66 | fi 67 | 68 | 69 | echo "" 70 | if [ -d ${driver_path}/${driver_name_instance} ]; then 71 | echo "Updating driver '$driver_name' as '$driver_name_instance'..." 72 | else 73 | echo "Installing driver '$driver_name' as '$driver_name_instance'..." 74 | fi 75 | 76 | 77 | # change to temp folder 78 | cd /tmp 79 | 80 | 81 | # download driver 82 | echo "" 83 | echo "Downloading driver..." 84 | 85 | 86 | ## latest release 87 | if [ "$version" = "latest release \"$latest_release_stable\"" ]; then 88 | # download latest release 89 | url=$(curl -s https://api.github.com/repos/mr-manuel/venus-os_${driver_name}/releases/latest | grep "zipball_url" | sed -n 's/.*"zipball_url": "\([^"]*\)".*/\1/p') 90 | fi 91 | 92 | ## nightly build 93 | if [ "$version" = "nightly build \"v$latest_release_nightly\"" ]; then 94 | # download nightly build 95 | url="https://github.com/mr-manuel/venus-os_${driver_name}/archive/refs/heads/master.zip" 96 | fi 97 | 98 | echo "Downloading from: $url" 99 | wget -O /tmp/venus-os_${driver_name}.zip "$url" 100 | 101 | # check if download was successful 102 | if [ ! -f /tmp/venus-os_${driver_name}.zip ]; then 103 | echo "" 104 | echo "Download failed. Exiting..." 105 | exit 1 106 | fi 107 | 108 | 109 | # If updating: cleanup old folder 110 | if [ -d /tmp/venus-os_${driver_name}-master ]; then 111 | rm -rf /tmp/venus-os_${driver_name}-master 112 | fi 113 | 114 | 115 | # unzip folder 116 | echo "Unzipping driver..." 117 | unzip venus-os_${driver_name}.zip 118 | 119 | # Find and rename the extracted folder to be always the same 120 | extracted_folder=$(find /tmp/ -maxdepth 1 -type d -name "*${driver_name}-*") 121 | 122 | # Desired folder name 123 | desired_folder="/tmp/venus-os_${driver_name}-master" 124 | 125 | # Check if the extracted folder exists and does not already have the desired name 126 | if [ -n "$extracted_folder" ]; then 127 | if [ "$extracted_folder" != "$desired_folder" ]; then 128 | mv "$extracted_folder" "$desired_folder" 129 | else 130 | echo "Folder already has the desired name: $desired_folder" 131 | fi 132 | else 133 | echo "Error: Could not find extracted folder. Exiting..." 134 | exit 1 135 | fi 136 | 137 | 138 | # If updating: backup existing config file 139 | if [ -f ${driver_path}/${driver_name_instance}/config.ini ]; then 140 | echo "" 141 | echo "Backing up existing config file..." 142 | mv ${driver_path}/${driver_name_instance}/config.ini ${driver_path}/${driver_name_instance}_config.ini 143 | fi 144 | 145 | 146 | # If updating: cleanup existing driver 147 | if [ -d ${driver_path}/${driver_name_instance} ]; then 148 | echo "" 149 | echo "Cleaning up existing driver..." 150 | rm -rf ${driver_path:?}/${driver_name_instance} 151 | fi 152 | 153 | 154 | # copy files 155 | echo "" 156 | echo "Copying new driver files..." 157 | cp -R /tmp/venus-os_${driver_name}-master/${driver_name}/ ${driver_path}/${driver_name_instance}/ 158 | 159 | # remove temp files 160 | echo "" 161 | echo "Cleaning up temp files..." 162 | rm -rf /tmp/venus-os_${driver_name}.zip 163 | rm -rf /tmp/venus-os_${driver_name}-master 164 | 165 | 166 | # check if driver_name is no equal to driver_name_instance 167 | if [ "$driver_name" != "$driver_name_instance" ]; then 168 | echo "" 169 | echo "Renaming internal driver files..." 170 | # rename the driver_name.py file to driver_name_instance.py 171 | mv ${driver_path}/${driver_name_instance}/${driver_name}.py ${driver_path}/${driver_name_instance}/${driver_name_instance}.py 172 | # rename the driver_name in the run file to driver_name_instance 173 | sed -i 's:'${driver_name}':'${driver_name_instance}':g' ${driver_path}/${driver_name_instance}/service/run 174 | # rename the driver_name in the log run file to driver_name_instance 175 | sed -i 's:'${driver_name}':'${driver_name_instance}':g' ${driver_path}/${driver_name_instance}/service/log/run 176 | 177 | # add device_instance to the end of the line where device_name is found in the config sample file 178 | sed -i '/device_name/s/$/ '${driver_instance}'/' ${driver_path}/${driver_name_instance}/config.sample.ini 179 | 180 | # change the device_instance from 100 to 100 + device_instance in the config sample file 181 | config_file_device_instance=$(grep 'device_instance = ' ${driver_path}/${driver_name_instance}/config.sample.ini | awk -F' = ' '{print $2}') 182 | new_device_instance=$((config_file_device_instance + driver_instance)) 183 | sed -i 's/device_instance = 100/device_instance = '${new_device_instance}'/' ${driver_path}/${driver_name_instance}/config.sample.ini 184 | 185 | fi 186 | 187 | 188 | # If updating: restore existing config file 189 | if [ -f ${driver_path}/${driver_name_instance}_config.ini ]; then 190 | echo "" 191 | echo "Restoring existing config file..." 192 | mv ${driver_path}/${driver_name_instance}_config.ini ${driver_path}/${driver_name_instance}/config.ini 193 | fi 194 | 195 | 196 | # set permissions for files 197 | echo "" 198 | echo "Setting permissions for files..." 199 | chmod 755 ${driver_path}/${driver_name_instance}/${driver_name_instance}.py 200 | chmod 755 ${driver_path}/${driver_name_instance}/install.sh 201 | chmod 755 ${driver_path}/${driver_name_instance}/restart.sh 202 | chmod 755 ${driver_path}/${driver_name_instance}/uninstall.sh 203 | chmod 755 ${driver_path}/${driver_name_instance}/service/run 204 | chmod 755 ${driver_path}/${driver_name_instance}/service/log/run 205 | 206 | 207 | # copy default config file 208 | if [ ! -f ${driver_path}/${driver_name_instance}/config.ini ]; then 209 | echo "" 210 | echo "" 211 | echo "First installation detected. Copying default config file..." 212 | echo "" 213 | echo "** Do not forget to edit the config file with your settings! **" 214 | echo "You can edit the config file with the following command:" 215 | echo "nano ${driver_path}/${driver_name_instance}/config.ini" 216 | cp ${driver_path}/${driver_name_instance}/config.sample.ini ${driver_path}/${driver_name_instance}/config.ini 217 | echo "" 218 | echo "** Execute the install.sh script after you have edited the config file! **" 219 | echo "You can execute the install.sh script with the following command:" 220 | echo "bash ${driver_path}/${driver_name_instance}/install.sh" 221 | echo "" 222 | else 223 | echo "" 224 | echo "Restart driver to apply new version..." 225 | /bin/bash ${driver_path}/${driver_name_instance}/restart.sh 226 | fi 227 | 228 | 229 | echo 230 | echo "Done." 231 | echo 232 | echo 233 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 216 3 | exclude = 'dbus-mqtt-temperature/ext' 4 | -------------------------------------------------------------------------------- /screenshots/temperature_device-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-manuel/venus-os_dbus-mqtt-temperature/08f0e716d99659e4db88eef27e90ac9bd6328e9c/screenshots/temperature_device-list.png -------------------------------------------------------------------------------- /screenshots/temperature_device-list_mqtt-temperature-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-manuel/venus-os_dbus-mqtt-temperature/08f0e716d99659e4db88eef27e90ac9bd6328e9c/screenshots/temperature_device-list_mqtt-temperature-1.png -------------------------------------------------------------------------------- /screenshots/temperature_device-list_mqtt-temperature-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-manuel/venus-os_dbus-mqtt-temperature/08f0e716d99659e4db88eef27e90ac9bd6328e9c/screenshots/temperature_device-list_mqtt-temperature-2.png -------------------------------------------------------------------------------- /screenshots/temperature_pages_guimods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-manuel/venus-os_dbus-mqtt-temperature/08f0e716d99659e4db88eef27e90ac9bd6328e9c/screenshots/temperature_pages_guimods.png --------------------------------------------------------------------------------