├── .gitattributes ├── .gitignore ├── README.md └── plugin.service.mqtt ├── LICENSE.txt ├── addon.xml ├── changelog.txt ├── icon.png ├── lib ├── __init__.py ├── client.py ├── matcher.py ├── packettypes.py ├── properties.py ├── publish.py ├── reasoncodes.py ├── subscribe.py └── subscribeoptions.py ├── resources ├── language │ └── English │ │ └── strings.po └── settings.xml └── service.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.py text eol=lf 2 | *.po text eol=lf 3 | *.xml text eol=lf 4 | *.txt text eol=lf 5 | *.md text eol=lf 6 | .gitattributes text eol=lf 7 | .gitignore text eol=lf 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyo 2 | *.zip 3 | .idea/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MQTT addon for Kodi 2 | =================== 3 | 4 | Written and (C) 2015-16 Oliver Wagner 5 | Additional changes: See [service.mqtt/changelog.txt](service.mqtt/changelog.txt) 6 | 7 | Provided under the terms of the MIT license. 8 | 9 | 10 | Overview 11 | -------- 12 | This is a Kodi addon which acts as an adapter between a Kodi media center instance and MQTT. 13 | It publishes Kodi's playback state on MQTT topics, and provides remote control capability also via 14 | messages to MQTT topics. 15 | 16 | It's intended as a building block in heterogenous smart home environments where an MQTT message broker is used as the centralized message bus. 17 | See https://github.com/mqtt-smarthome for a rationale and architectural overview. 18 | 19 | Modifications from original owanger version: 20 | * Bugfix: playing/resumed events now fire consistently 21 | * Bugfix: Updated Paho to 1.5.1 to fix reconnect crash (https://github.com/eclipse/paho.mqtt.python) 22 | * Feature: All Kodi API notification events are published 23 | * Feature: Volume control 24 | * Feature: Send title with each progress update, to handle radio streams which update their 'now playing'. 25 | 26 | Dependencies 27 | ------------ 28 | * Tested on Kodi 19 Matrix. Other versions may work, feel free to report any that do, I'll add them here. 29 | * Eclipse Paho for Python - http://www.eclipse.org/paho/clients/python/ (used for MQTT communication). 30 | This is included with the plugin. 31 | 32 | 33 | Settings 34 | -------- 35 | The addon has multiple settings: 36 | 37 | * the MQTT broker's host name or IP address (defaults to 127.0.0.1) 38 | * the MQTT broker's port. This defaults to 1883, which is the MQTT standard port for unencrypted connections. 39 | * the topic prefix which to use in all published and subscribed topics. Defaults to "kodi/". 40 | * MQTT authentication and TLS settings 41 | * update frequency intervals 42 | * keyword filtering on content details, to prevent certain kind of content to be e.g. displayed in a SmartHome visualization 43 | 44 | 45 | Topics 46 | ------ 47 | The addon publishes on the following topics (prefixed with the configured topic prefix): 48 | 49 | * connected: 2 if the addon is currently connected to the broker, 0 otherwise. This topic is set to 0 with a MQTT will. 50 | * status/playbackstate: a JSON-encoded object with the fields 51 | - "val" for the current playback state with 0=stopped, 1=playing, 2=paused 52 | - "kodi_playbackdetails": an object with further details about the playback state. This is effectivly the result 53 | of the JSON-RPC call Player.GetItem with the properties "speed", "currentsubtitle", "currentaudiostream", "repeat" 54 | and "subtitleenabled" 55 | - "kodi_playerid": the ID of the active player 56 | - "kodi_playertype": the type of the active player (e.g. "video") 57 | * status/progress: a JSON-encoded object with the fields 58 | - "val" is the percentage of progress in playing back the current item 59 | - "kodi_time": the playback position in the current item 60 | - "kodi_totaltime": the total length of the current item 61 | * status/playertitle: The title of what's currently playing, updated at the same interval as progress. 62 | For radio streams which tend to change their title, this can be used to show an up to date title. 63 | * status/title: a JSON-encoded object with the fields 64 | - "val": the title of the current playback item 65 | - "kodi_details": an object with further details about the current playback items. This is effectivly the result 66 | of a JSON-RPC call Player.GetItem with the properties "title", "streamdetails", "file", "thumbnail" 67 | and "fanart" 68 | * status/notification/: Any Kodi notification event, with the event json data as body. 69 | This is connected to xbmc::Monitor::onNotification, with method as last part of the topic, and data as the payload. 70 | * status/screensaver: '1'/'0' on screensaver activation/deactivation 71 | 72 | The addon listens to the following topics (prefixed with the configured topic prefix): 73 | 74 | * command/notify: Either a simple string, or a JSON encoded object with the fields "message" and "title". Shows 75 | a popup notification in Kodi 76 | * command/play: Either a simple string which is a filename or URL, or a JSON encoded object which correspondents 77 | to the Player.Open() JSON_RPC call 78 | * command/volume: Set the volume to value or or a JSON encoded object which correspondents 79 | to the Application.SetVolume() JSON_RPC call 80 | * command/playbackstate: A simple string or numeric with the values: 81 | - "0" or "stop" to stop playback 82 | - "1" or "resume" or "play" to resume playback (when paused or stopped) 83 | - "2" or "pause" to stop playback (when playing) 84 | - "toggle" to toggle between play and pause 85 | - "next" to play the next track 86 | - "previous" to play the previous track 87 | - "playcurrent" to play the currently selected track 88 | * command/progress: A string having format hours:minutes:seconds. Changes position of currently played file 89 | * command/api: The full JSON_RPC API is accessible: 90 | - {"method":"GUI.ShowNotification","jsonrpc":"2.0","params":{"title":"Test Title","message":"Test Message"},"playerid":"1"} 91 | * command/cecstate: Expects value '1' or 'activate', might wake up tv with CEC (workaround by muracz) 92 | 93 | 94 | 95 | See also 96 | -------- 97 | - kodi.tv forum thread: http://forum.kodi.tv/showthread.php?tid=222109 98 | - JSON-RPC API in Kodi: http://kodi.wiki/view/JSON-RPC_API 99 | - Project overview: https://github.com/mqtt-smarthome 100 | 101 | 102 | Changelog 103 | --------- 104 | Please see [service.mqtt/changelog.txt](service.mqtt/changelog.txt) for the change log 105 | -------------------------------------------------------------------------------- /plugin.service.mqtt/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The kodi2mqtt addon is licensed under the terms of the MIT license: 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2015 Oliver Wagner 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | --------------------------------------------------------------------- 26 | 27 | For the included Eclipse Paho MQTT library: 28 | 29 | This project is dual licensed under the Eclipse Public License 1.0 and the 30 | Eclipse Distribution License 1.0 as described in the epl-v10 and edl-v10 files. 31 | -------------------------------------------------------------------------------- /plugin.service.mqtt/addon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | MQTT Adapter, adhering to mqtt-smarthome specification 9 | The addon is an adapter to an MQTT broker. It will publish information about what is playing, and provides remote control capability. It adheres to the mqtt-smarthome specification. 10 | 11 | all 12 | MIT 13 | 14 | 15 | 16 | https://github.com/void-spark/kodi2mqtt 17 | 18 | 19 | -------------------------------------------------------------------------------- /plugin.service.mqtt/changelog.txt: -------------------------------------------------------------------------------- 1 | v0.22 - 2022-01-24 2 | - Fixed some things to follow conventions 3 | - Tested on 19.4 Matrix 4 | 5 | v0.21 - 2021-03-22 6 | - Convert to work with Kodi 19 Matrix (just some minor changes were needed) 7 | - Update MQTT client Paho to latest: 1.5.1 8 | 9 | v0.20 - 2020-05-17 10 | - Fork from richcrabtree 11 | - Merged rubikscuber changes (Fix for netflixaddon) 12 | - Merged muracz changes (Simple screensaver notification, TV wake-up workaround/cecstate command) 13 | - Merge mariansoban changes (Handle MQTT reconnect exceptions) 14 | - Send current title together with progress, so know what's playing with for example radio streams, 15 | which change their title without notification 16 | 17 | v0.16 - 2019-12-16 - richcrabtree 18 | - Bugfix: playing/resumed events now fire consistently 19 | - Bugfix: Updated Paho to 1.5 to fix reconnect crash 20 | - Feature: All Kodi API notification events are published 21 | - Feature: Volume control 22 | 23 | v0.15 - 2019-11-04 - eschava 24 | - option to publish/receive time with millis as a progress 25 | 26 | v0.14 - 2019-10-31 - devtown/eschava 27 | - added set volume 28 | - change playback position 29 | - play currently selected file 30 | - bugfixes 31 | 32 | v0.13 - 2016-05-01 - owagner 33 | - change logic for command/playbackstate: 34 | - resume/play/1 will now properly resume when pausing, and attempt to 35 | start play when playback has endeded. Fixes #17 36 | - pause/2 will now only pause if the player is actually playing. 37 | It can no longer be used to toggle pause mode. 38 | - toggle (new) will now toggle pause mode, similarily to how "pause" 39 | did before 40 | 41 | v0.12 - 2016-02-022 - markferry 42 | - fix non-JSON case for handling command/notify 43 | 44 | V0.11 - 2016-02-22 - jvandenbroek 45 | - optional ignore list to prevent certain content to be published as being 46 | played back by a word match on title and/or path 47 | - configurable push frequencies for details and progress 48 | - automatically retry MQTT connections up to 20 times 49 | - various minor fixes 50 | 51 | v0.10 - 2016-01-30 - 2Zero 52 | - added TLS parameters to settings 53 | - noisy logging now optional via "Debug" settings switch 54 | 55 | v0.9 - 2015-11-17 - drlobo 56 | - added MQTT authentication parameters to settings 57 | 58 | v0.8 - 2015-09-15 - owagner 59 | - updated PAHO library to v1.0.2 to fix startup issues caused by 60 | "localhost" not resolving, which apparently seems to happen on some 61 | OpenELEC installations. Fixes #6 (thanks fab33) 62 | 63 | v0.7 - 2015-08-01 - existsec 64 | - include "thumbnail" and "fanart" in title message 65 | 66 | v0.6 - 2015-07-29 - owagner 67 | - include "kodi_playerid" and "kodi_playertype" in playbackstate updates [#2] 68 | 69 | V0.5 - 2015-07-25 - owagner 70 | - fixed script error occuring when playback details where not immediate 71 | available when starting playback 72 | 73 | V0.4 - 2015-06-16 - owagner 74 | - Settings: MQTT broker address is now a text field and thus allows entering of hostnames 75 | - will now check whether the title information changes during progress 76 | checking, and will republish the "title" topic if a change was detected 77 | - increase progress publish rate to 20s (from 30s) 78 | - avoid "kodi2mqtt" as a name in the documentation and addon itself, and instead stick to "MQTT Adapter" 79 | 80 | V0.3 - 2015-03-22 - owagner 81 | - fixed division by zero when switching TV channels 82 | - now supports command/notify to send notifications 83 | - now supports command/play to start playback of files or items 84 | - now supports command/playbackstate to control the playback state 85 | 86 | V0.2 - 2015-03-22 - owagner 87 | - refactored as a Kodi addon 88 | -------------------------------------------------------------------------------- /plugin.service.mqtt/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/void-spark/kodi2mqtt/113983efd8d57e94c1fbe0e03ebac067d1c6c676/plugin.service.mqtt/icon.png -------------------------------------------------------------------------------- /plugin.service.mqtt/lib/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.5.1" 2 | 3 | 4 | class MQTTException(Exception): 5 | pass 6 | -------------------------------------------------------------------------------- /plugin.service.mqtt/lib/matcher.py: -------------------------------------------------------------------------------- 1 | class MQTTMatcher(object): 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(object): 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: 37 | raise KeyError(key) 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: 50 | raise KeyError(key) 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 | -------------------------------------------------------------------------------- /plugin.service.mqtt/lib/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 v1.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-v10.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 | -------------------------------------------------------------------------------- /plugin.service.mqtt/lib/properties.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 v1.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-v10.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 | import sys, struct 20 | 21 | from .packettypes import PacketTypes 22 | 23 | 24 | class MQTTException(Exception): 25 | pass 26 | 27 | 28 | class MalformedPacket(MQTTException): 29 | pass 30 | 31 | 32 | def writeInt16(length): 33 | # serialize a 16 bit integer to network format 34 | return bytearray(struct.pack("!H", length)) 35 | 36 | 37 | def readInt16(buf): 38 | # deserialize a 16 bit integer from network format 39 | return struct.unpack("!H", buf[:2])[0] 40 | 41 | 42 | def writeInt32(length): 43 | # serialize a 32 bit integer to network format 44 | return bytearray(struct.pack("!L", length)) 45 | 46 | 47 | def readInt32(buf): 48 | # deserialize a 32 bit integer from network format 49 | return struct.unpack("!L", buf[:4])[0] 50 | 51 | 52 | def writeUTF(data): 53 | # data could be a string, or bytes. If string, encode into bytes with utf-8 54 | if sys.version_info[0] < 3: 55 | data = bytearray(data, 'utf-8') 56 | else: 57 | data = data if type(data) == type(b"") else bytes(data, "utf-8") 58 | return writeInt16(len(data)) + data 59 | 60 | 61 | def readUTF(buffer, maxlen): 62 | if maxlen >= 2: 63 | length = readInt16(buffer) 64 | else: 65 | raise MalformedPacket("Not enough data to read string length") 66 | maxlen -= 2 67 | if length > maxlen: 68 | raise MalformedPacket("Length delimited string too long") 69 | buf = buffer[2:2+length].decode("utf-8") 70 | # look for chars which are invalid for MQTT 71 | for c in buf: # look for D800-DFFF in the UTF string 72 | ord_c = ord(c) 73 | if ord_c >= 0xD800 and ord_c <= 0xDFFF: 74 | raise MalformedPacket("[MQTT-1.5.4-1] D800-DFFF found in UTF-8 data") 75 | if ord_c == 0x00: # look for null in the UTF string 76 | raise MalformedPacket("[MQTT-1.5.4-2] Null found in UTF-8 data") 77 | if ord_c == 0xFEFF: 78 | raise MalformedPacket("[MQTT-1.5.4-3] U+FEFF in UTF-8 data") 79 | return buf, length+2 80 | 81 | 82 | def writeBytes(buffer): 83 | return writeInt16(len(buffer)) + buffer 84 | 85 | 86 | def readBytes(buffer): 87 | length = readInt16(buffer) 88 | return buffer[2:2+length], length+2 89 | 90 | 91 | class VariableByteIntegers: # Variable Byte Integer 92 | """ 93 | MQTT variable byte integer helper class. Used 94 | in several places in MQTT v5.0 properties. 95 | 96 | """ 97 | 98 | @staticmethod 99 | def encode(x): 100 | """ 101 | Convert an integer 0 <= x <= 268435455 into multi-byte format. 102 | Returns the buffer convered from the integer. 103 | """ 104 | assert 0 <= x <= 268435455 105 | buffer = b'' 106 | while 1: 107 | digit = x % 128 108 | x //= 128 109 | if x > 0: 110 | digit |= 0x80 111 | if sys.version_info[0] >= 3: 112 | buffer += bytes([digit]) 113 | else: 114 | buffer += bytes(chr(digit)) 115 | if x == 0: 116 | break 117 | return buffer 118 | 119 | @staticmethod 120 | def decode(buffer): 121 | """ 122 | Get the value of a multi-byte integer from a buffer 123 | Return the value, and the number of bytes used. 124 | 125 | [MQTT-1.5.5-1] the encoded value MUST use the minimum number of bytes necessary to represent the value 126 | """ 127 | multiplier = 1 128 | value = 0 129 | bytes = 0 130 | while 1: 131 | bytes += 1 132 | digit = buffer[0] 133 | buffer = buffer[1:] 134 | value += (digit & 127) * multiplier 135 | if digit & 128 == 0: 136 | break 137 | multiplier *= 128 138 | return (value, bytes) 139 | 140 | 141 | class Properties(object): 142 | """MQTT v5.0 properties class. 143 | 144 | See Properties.names for a list of accepted property names along with their numeric values. 145 | 146 | See Properties.properties for the data type of each property. 147 | 148 | Example of use: 149 | 150 | publish_properties = Properties(PacketTypes.PUBLISH) 151 | publish_properties.UserProperty = ("a", "2") 152 | publish_properties.UserProperty = ("c", "3") 153 | 154 | First the object is created with packet type as argument, no properties will be present at 155 | this point. Then properties are added as attributes, the name of which is the string property 156 | name without the spaces. 157 | 158 | """ 159 | 160 | def __init__(self, packetType): 161 | self.packetType = packetType 162 | self.types = ["Byte", "Two Byte Integer", "Four Byte Integer", "Variable Byte Integer", 163 | "Binary Data", "UTF-8 Encoded String", "UTF-8 String Pair"] 164 | 165 | self.names = { 166 | "Payload Format Indicator": 1, 167 | "Message Expiry Interval": 2, 168 | "Content Type": 3, 169 | "Response Topic": 8, 170 | "Correlation Data": 9, 171 | "Subscription Identifier": 11, 172 | "Session Expiry Interval": 17, 173 | "Assigned Client Identifier": 18, 174 | "Server Keep Alive": 19, 175 | "Authentication Method": 21, 176 | "Authentication Data": 22, 177 | "Request Problem Information": 23, 178 | "Will Delay Interval": 24, 179 | "Request Response Information": 25, 180 | "Response Information": 26, 181 | "Server Reference": 28, 182 | "Reason String": 31, 183 | "Receive Maximum": 33, 184 | "Topic Alias Maximum": 34, 185 | "Topic Alias": 35, 186 | "Maximum QoS": 36, 187 | "Retain Available": 37, 188 | "User Property": 38, 189 | "Maximum Packet Size": 39, 190 | "Wildcard Subscription Available": 40, 191 | "Subscription Identifier Available": 41, 192 | "Shared Subscription Available": 42 193 | } 194 | 195 | self.properties = { 196 | # id: type, packets 197 | # payload format indicator 198 | 1: (self.types.index("Byte"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), 199 | 2: (self.types.index("Four Byte Integer"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), 200 | 3: (self.types.index("UTF-8 Encoded String"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), 201 | 8: (self.types.index("UTF-8 Encoded String"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), 202 | 9: (self.types.index("Binary Data"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), 203 | 11: (self.types.index("Variable Byte Integer"), 204 | [PacketTypes.PUBLISH, PacketTypes.SUBSCRIBE]), 205 | 17: (self.types.index("Four Byte Integer"), 206 | [PacketTypes.CONNECT, PacketTypes.CONNACK, PacketTypes.DISCONNECT]), 207 | 18: (self.types.index("UTF-8 Encoded String"), [PacketTypes.CONNACK]), 208 | 19: (self.types.index("Two Byte Integer"), [PacketTypes.CONNACK]), 209 | 21: (self.types.index("UTF-8 Encoded String"), 210 | [PacketTypes.CONNECT, PacketTypes.CONNACK, PacketTypes.AUTH]), 211 | 22: (self.types.index("Binary Data"), 212 | [PacketTypes.CONNECT, PacketTypes.CONNACK, PacketTypes.AUTH]), 213 | 23: (self.types.index("Byte"), 214 | [PacketTypes.CONNECT]), 215 | 24: (self.types.index("Four Byte Integer"), [PacketTypes.WILLMESSAGE]), 216 | 25: (self.types.index("Byte"), [PacketTypes.CONNECT]), 217 | 26: (self.types.index("UTF-8 Encoded String"), [PacketTypes.CONNACK]), 218 | 28: (self.types.index("UTF-8 Encoded String"), 219 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]), 220 | 31: (self.types.index("UTF-8 Encoded String"), 221 | [PacketTypes.CONNACK, PacketTypes.PUBACK, PacketTypes.PUBREC, 222 | PacketTypes.PUBREL, PacketTypes.PUBCOMP, PacketTypes.SUBACK, 223 | PacketTypes.UNSUBACK, PacketTypes.DISCONNECT, PacketTypes.AUTH]), 224 | 33: (self.types.index("Two Byte Integer"), 225 | [PacketTypes.CONNECT, PacketTypes.CONNACK]), 226 | 34: (self.types.index("Two Byte Integer"), 227 | [PacketTypes.CONNECT, PacketTypes.CONNACK]), 228 | 35: (self.types.index("Two Byte Integer"), [PacketTypes.PUBLISH]), 229 | 36: (self.types.index("Byte"), [PacketTypes.CONNACK]), 230 | 37: (self.types.index("Byte"), [PacketTypes.CONNACK]), 231 | 38: (self.types.index("UTF-8 String Pair"), 232 | [PacketTypes.CONNECT, PacketTypes.CONNACK, 233 | PacketTypes.PUBLISH, PacketTypes.PUBACK, 234 | PacketTypes.PUBREC, PacketTypes.PUBREL, PacketTypes.PUBCOMP, 235 | PacketTypes.SUBSCRIBE, PacketTypes.SUBACK, 236 | PacketTypes.UNSUBSCRIBE, PacketTypes.UNSUBACK, 237 | PacketTypes.DISCONNECT, PacketTypes.AUTH, PacketTypes.WILLMESSAGE]), 238 | 39: (self.types.index("Four Byte Integer"), 239 | [PacketTypes.CONNECT, PacketTypes.CONNACK]), 240 | 40: (self.types.index("Byte"), [PacketTypes.CONNACK]), 241 | 41: (self.types.index("Byte"), [PacketTypes.CONNACK]), 242 | 42: (self.types.index("Byte"), [PacketTypes.CONNACK]), 243 | } 244 | 245 | def allowsMultiple(self, compressedName): 246 | return self.getIdentFromName(compressedName) in [11, 38] 247 | 248 | def getIdentFromName(self, compressedName): 249 | # return the identifier corresponding to the property name 250 | result = -1 251 | for name in self.names.keys(): 252 | if compressedName == name.replace(' ', ''): 253 | result = self.names[name] 254 | break 255 | return result 256 | 257 | def __setattr__(self, name, value): 258 | name = name.replace(' ', '') 259 | privateVars = ["packetType", "types", "names", "properties"] 260 | if name in privateVars: 261 | object.__setattr__(self, name, value) 262 | else: 263 | # the name could have spaces in, or not. Remove spaces before assignment 264 | if name not in [aname.replace(' ', '') for aname in self.names.keys()]: 265 | raise MQTTException( 266 | "Property name must be one of "+str(self.names.keys())) 267 | # check that this attribute applies to the packet type 268 | if self.packetType not in self.properties[self.getIdentFromName(name)][1]: 269 | raise MQTTException("Property %s does not apply to packet type %s" 270 | % (name, PacketTypes.Names[self.packetType])) 271 | if self.allowsMultiple(name): 272 | if type(value) != type([]): 273 | value = [value] 274 | if hasattr(self, name): 275 | value = object.__getattribute__(self, name) + value 276 | object.__setattr__(self, name, value) 277 | 278 | def __str__(self): 279 | buffer = "[" 280 | first = True 281 | for name in self.names.keys(): 282 | compressedName = name.replace(' ', '') 283 | if hasattr(self, compressedName): 284 | if not first: 285 | buffer += ", " 286 | buffer += compressedName + " : " + \ 287 | str(getattr(self, compressedName)) 288 | first = False 289 | buffer += "]" 290 | return buffer 291 | 292 | def json(self): 293 | data = {} 294 | for name in self.names.keys(): 295 | compressedName = name.replace(' ', '') 296 | if hasattr(self, compressedName): 297 | data[compressedName] = getattr(self, compressedName) 298 | return data 299 | 300 | def isEmpty(self): 301 | rc = True 302 | for name in self.names.keys(): 303 | compressedName = name.replace(' ', '') 304 | if hasattr(self, compressedName): 305 | rc = False 306 | break 307 | return rc 308 | 309 | def clear(self): 310 | for name in self.names.keys(): 311 | compressedName = name.replace(' ', '') 312 | if hasattr(self, compressedName): 313 | delattr(self, compressedName) 314 | 315 | def writeProperty(self, identifier, type, value): 316 | buffer = b"" 317 | buffer += VariableByteIntegers.encode(identifier) # identifier 318 | if type == self.types.index("Byte"): # value 319 | if sys.version_info[0] < 3: 320 | buffer += chr(value) 321 | else: 322 | buffer += bytes([value]) 323 | elif type == self.types.index("Two Byte Integer"): 324 | buffer += writeInt16(value) 325 | elif type == self.types.index("Four Byte Integer"): 326 | buffer += writeInt32(value) 327 | elif type == self.types.index("Variable Byte Integer"): 328 | buffer += VariableByteIntegers.encode(value) 329 | elif type == self.types.index("Binary Data"): 330 | buffer += writeBytes(value) 331 | elif type == self.types.index("UTF-8 Encoded String"): 332 | buffer += writeUTF(value) 333 | elif type == self.types.index("UTF-8 String Pair"): 334 | buffer += writeUTF(value[0]) + writeUTF(value[1]) 335 | return buffer 336 | 337 | def pack(self): 338 | # serialize properties into buffer for sending over network 339 | buffer = b"" 340 | for name in self.names.keys(): 341 | compressedName = name.replace(' ', '') 342 | if hasattr(self, compressedName): 343 | identifier = self.getIdentFromName(compressedName) 344 | attr_type = self.properties[identifier][0] 345 | if self.allowsMultiple(compressedName): 346 | for prop in getattr(self, compressedName): 347 | buffer += self.writeProperty(identifier, 348 | attr_type, prop) 349 | else: 350 | buffer += self.writeProperty(identifier, attr_type, 351 | getattr(self, compressedName)) 352 | return VariableByteIntegers.encode(len(buffer)) + buffer 353 | 354 | def readProperty(self, buffer, type, propslen): 355 | if type == self.types.index("Byte"): 356 | value = buffer[0] 357 | valuelen = 1 358 | elif type == self.types.index("Two Byte Integer"): 359 | value = readInt16(buffer) 360 | valuelen = 2 361 | elif type == self.types.index("Four Byte Integer"): 362 | value = readInt32(buffer) 363 | valuelen = 4 364 | elif type == self.types.index("Variable Byte Integer"): 365 | value, valuelen = VariableByteIntegers.decode(buffer) 366 | elif type == self.types.index("Binary Data"): 367 | value, valuelen = readBytes(buffer) 368 | elif type == self.types.index("UTF-8 Encoded String"): 369 | value, valuelen = readUTF(buffer, propslen) 370 | elif type == self.types.index("UTF-8 String Pair"): 371 | value, valuelen = readUTF(buffer, propslen) 372 | buffer = buffer[valuelen:] # strip the bytes used by the value 373 | value1, valuelen1 = readUTF(buffer, propslen - valuelen) 374 | value = (value, value1) 375 | valuelen += valuelen1 376 | return value, valuelen 377 | 378 | def getNameFromIdent(self, identifier): 379 | rc = None 380 | for name in self.names: 381 | if self.names[name] == identifier: 382 | rc = name 383 | return rc 384 | 385 | def unpack(self, buffer): 386 | if sys.version_info[0] < 3: 387 | buffer = bytearray(buffer) 388 | self.clear() 389 | # deserialize properties into attributes from buffer received from network 390 | propslen, VBIlen = VariableByteIntegers.decode(buffer) 391 | buffer = buffer[VBIlen:] # strip the bytes used by the VBI 392 | propslenleft = propslen 393 | while propslenleft > 0: # properties length is 0 if there are none 394 | identifier, VBIlen = VariableByteIntegers.decode( 395 | buffer) # property identifier 396 | buffer = buffer[VBIlen:] # strip the bytes used by the VBI 397 | propslenleft -= VBIlen 398 | attr_type = self.properties[identifier][0] 399 | value, valuelen = self.readProperty( 400 | buffer, attr_type, propslenleft) 401 | buffer = buffer[valuelen:] # strip the bytes used by the value 402 | propslenleft -= valuelen 403 | propname = self.getNameFromIdent(identifier) 404 | compressedName = propname.replace(' ', '') 405 | if not self.allowsMultiple(compressedName) and hasattr(self, compressedName): 406 | raise MQTTException( 407 | "Property '%s' must not exist more than once" % property) 408 | setattr(self, propname, value) 409 | return self, propslen + VBIlen 410 | -------------------------------------------------------------------------------- /plugin.service.mqtt/lib/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 v1.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-v10.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 absolute_import 22 | 23 | import collections 24 | try: 25 | from collections.abc import Iterable 26 | except ImportError: 27 | from collections import Iterable 28 | 29 | from . import client as paho 30 | from .. import mqtt 31 | 32 | def _do_publish(client): 33 | """Internal function""" 34 | 35 | message = client._userdata.popleft() 36 | 37 | if isinstance(message, dict): 38 | client.publish(**message) 39 | elif isinstance(message, (tuple, list)): 40 | client.publish(*message) 41 | else: 42 | raise TypeError('message must be a dict, tuple, or list') 43 | 44 | 45 | def _on_connect(client, userdata, flags, rc): 46 | """Internal callback""" 47 | #pylint: disable=invalid-name, unused-argument 48 | 49 | if rc == 0: 50 | if len(userdata) > 0: 51 | _do_publish(client) 52 | else: 53 | raise mqtt.MQTTException(paho.connack_string(rc)) 54 | 55 | 56 | def _on_publish(client, userdata, mid): 57 | """Internal callback""" 58 | #pylint: disable=unused-argument 59 | 60 | if len(userdata) == 0: 61 | client.disconnect() 62 | else: 63 | _do_publish(client) 64 | 65 | 66 | def multiple(msgs, hostname="localhost", port=1883, client_id="", keepalive=60, 67 | will=None, auth=None, tls=None, protocol=paho.MQTTv311, 68 | transport="tcp", proxy_args=None): 69 | """Publish multiple messages to a broker, then disconnect cleanly. 70 | 71 | This function creates an MQTT client, connects to a broker and publishes a 72 | list of messages. Once the messages have been delivered, it disconnects 73 | cleanly from the broker. 74 | 75 | msgs : a list of messages to publish. Each message is either a dict or a 76 | tuple. 77 | 78 | If a dict, only the topic must be present. Default values will be 79 | used for any missing arguments. The dict must be of the form: 80 | 81 | msg = {'topic':"", 'payload':"", 'qos':, 82 | 'retain':} 83 | topic must be present and may not be empty. 84 | If payload is "", None or not present then a zero length payload 85 | will be published. 86 | If qos is not present, the default of 0 is used. 87 | If retain is not present, the default of False is used. 88 | 89 | If a tuple, then it must be of the form: 90 | ("", "", qos, retain) 91 | 92 | hostname : a string containing the address of the broker to connect to. 93 | Defaults to localhost. 94 | 95 | port : the port to connect to the broker on. Defaults to 1883. 96 | 97 | client_id : the MQTT client id to use. If "" or None, the Paho library will 98 | generate a client id automatically. 99 | 100 | keepalive : the keepalive timeout value for the client. Defaults to 60 101 | seconds. 102 | 103 | will : a dict containing will parameters for the client: will = {'topic': 104 | "", 'payload':", 'qos':, 'retain':}. 105 | Topic is required, all other parameters are optional and will 106 | default to None, 0 and False respectively. 107 | Defaults to None, which indicates no will should be used. 108 | 109 | auth : a dict containing authentication parameters for the client: 110 | auth = {'username':"", 'password':""} 111 | Username is required, password is optional and will default to None 112 | if not provided. 113 | Defaults to None, which indicates no authentication is to be used. 114 | 115 | tls : a dict containing TLS configuration parameters for the client: 116 | dict = {'ca_certs':"", 'certfile':"", 117 | 'keyfile':"", 'tls_version':"", 118 | 'ciphers':", 'insecure':""} 119 | ca_certs is required, all other parameters are optional and will 120 | default to None if not provided, which results in the client using 121 | the default behaviour - see the paho.mqtt.client documentation. 122 | Alternatively, tls input can be an SSLContext object, which will be 123 | processed using the tls_set_context method. 124 | Defaults to None, which indicates that TLS should not be used. 125 | 126 | transport : set to "tcp" to use the default setting of transport which is 127 | raw TCP. Set to "websockets" to use WebSockets as the transport. 128 | proxy_args: a dictionary that will be given to the client. 129 | """ 130 | 131 | if not isinstance(msgs, Iterable): 132 | raise TypeError('msgs must be an iterable') 133 | 134 | client = paho.Client(client_id=client_id, userdata=collections.deque(msgs), 135 | protocol=protocol, transport=transport) 136 | 137 | client.on_publish = _on_publish 138 | client.on_connect = _on_connect 139 | 140 | if proxy_args is not None: 141 | client.proxy_set(**proxy_args) 142 | 143 | if auth: 144 | username = auth.get('username') 145 | if username: 146 | password = auth.get('password') 147 | client.username_pw_set(username, password) 148 | else: 149 | raise KeyError("The 'username' key was not found, this is " 150 | "required for auth") 151 | 152 | if will is not None: 153 | client.will_set(**will) 154 | 155 | if tls is not None: 156 | if isinstance(tls, dict): 157 | insecure = tls.pop('insecure', False) 158 | client.tls_set(**tls) 159 | if insecure: 160 | # Must be set *after* the `client.tls_set()` call since it sets 161 | # up the SSL context that `client.tls_insecure_set` alters. 162 | client.tls_insecure_set(insecure) 163 | else: 164 | # Assume input is SSLContext object 165 | client.tls_set_context(tls) 166 | 167 | client.connect(hostname, port, keepalive) 168 | client.loop_forever() 169 | 170 | 171 | def single(topic, payload=None, qos=0, retain=False, hostname="localhost", 172 | port=1883, client_id="", keepalive=60, will=None, auth=None, 173 | tls=None, protocol=paho.MQTTv311, transport="tcp", proxy_args=None): 174 | """Publish a single message to a broker, then disconnect cleanly. 175 | 176 | This function creates an MQTT client, connects to a broker and publishes a 177 | single message. Once the message has been delivered, it disconnects cleanly 178 | from the broker. 179 | 180 | topic : the only required argument must be the topic string to which the 181 | payload will be published. 182 | 183 | payload : the payload to be published. If "" or None, a zero length payload 184 | will be published. 185 | 186 | qos : the qos to use when publishing, default to 0. 187 | 188 | retain : set the message to be retained (True) or not (False). 189 | 190 | hostname : a string containing the address of the broker to connect to. 191 | Defaults to localhost. 192 | 193 | port : the port to connect to the broker on. Defaults to 1883. 194 | 195 | client_id : the MQTT client id to use. If "" or None, the Paho library will 196 | generate a client id automatically. 197 | 198 | keepalive : the keepalive timeout value for the client. Defaults to 60 199 | seconds. 200 | 201 | will : a dict containing will parameters for the client: will = {'topic': 202 | "", 'payload':", 'qos':, 'retain':}. 203 | Topic is required, all other parameters are optional and will 204 | default to None, 0 and False respectively. 205 | Defaults to None, which indicates no will should be used. 206 | 207 | auth : a dict containing authentication parameters for the client: 208 | auth = {'username':"", 'password':""} 209 | Username is required, password is optional and will default to None 210 | if not provided. 211 | Defaults to None, which indicates no authentication is to be used. 212 | 213 | tls : a dict containing TLS configuration parameters for the client: 214 | dict = {'ca_certs':"", 'certfile':"", 215 | 'keyfile':"", 'tls_version':"", 216 | 'ciphers':", 'insecure':""} 217 | ca_certs is required, all other parameters are optional and will 218 | default to None if not provided, which results in the client using 219 | the default behaviour - see the paho.mqtt.client documentation. 220 | Defaults to None, which indicates that TLS should not be used. 221 | Alternatively, tls input can be an SSLContext object, which will be 222 | processed using the tls_set_context method. 223 | 224 | transport : set to "tcp" to use the default setting of transport which is 225 | raw TCP. Set to "websockets" to use WebSockets as the transport. 226 | proxy_args: a dictionary that will be given to the client. 227 | """ 228 | 229 | msg = {'topic':topic, 'payload':payload, 'qos':qos, 'retain':retain} 230 | 231 | multiple([msg], hostname, port, client_id, keepalive, will, auth, tls, 232 | protocol, transport, proxy_args) 233 | -------------------------------------------------------------------------------- /plugin.service.mqtt/lib/reasoncodes.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 v1.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-v10.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 | import sys 20 | from .packettypes import PacketTypes 21 | 22 | 23 | class ReasonCodes: 24 | """MQTT version 5.0 reason codes class. 25 | 26 | See ReasonCodes.names for a list of possible numeric values along with their 27 | names and the packets to which they apply. 28 | 29 | """ 30 | 31 | def __init__(self, packetType, aName="Success", identifier=-1): 32 | """ 33 | packetType: the type of the packet, such as PacketTypes.CONNECT that 34 | this reason code will be used with. Some reason codes have different 35 | names for the same identifier when used a different packet type. 36 | 37 | aName: the String name of the reason code to be created. Ignored 38 | if the identifier is set. 39 | 40 | identifier: an integer value of the reason code to be created. 41 | 42 | """ 43 | 44 | self.packetType = packetType 45 | self.names = { 46 | 0: {"Success": [PacketTypes.CONNACK, PacketTypes.PUBACK, 47 | PacketTypes.PUBREC, PacketTypes.PUBREL, PacketTypes.PUBCOMP, 48 | PacketTypes.UNSUBACK, PacketTypes.AUTH], 49 | "Normal disconnection": [PacketTypes.DISCONNECT], 50 | "Granted QoS 0": [PacketTypes.SUBACK]}, 51 | 1: {"Granted QoS 1": [PacketTypes.SUBACK]}, 52 | 2: {"Granted QoS 2": [PacketTypes.SUBACK]}, 53 | 4: {"Disconnect with will message": [PacketTypes.DISCONNECT]}, 54 | 16: {"No matching subscribers": 55 | [PacketTypes.PUBACK, PacketTypes.PUBREC]}, 56 | 17: {"No subscription found": [PacketTypes.UNSUBACK]}, 57 | 24: {"Continue authentication": [PacketTypes.AUTH]}, 58 | 25: {"Re-authenticate": [PacketTypes.AUTH]}, 59 | 128: {"Unspecified error": [PacketTypes.CONNACK, PacketTypes.PUBACK, 60 | PacketTypes.PUBREC, PacketTypes.SUBACK, PacketTypes.UNSUBACK, 61 | PacketTypes.DISCONNECT], }, 62 | 129: {"Malformed packet": 63 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 64 | 130: {"Protocol error": 65 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 66 | 131: {"Implementation specific error": [PacketTypes.CONNACK, 67 | PacketTypes.PUBACK, PacketTypes.PUBREC, PacketTypes.SUBACK, 68 | PacketTypes.UNSUBACK, PacketTypes.DISCONNECT], }, 69 | 132: {"Unsupported protocol version": [PacketTypes.CONNACK]}, 70 | 133: {"Client identifier not valid": [PacketTypes.CONNACK]}, 71 | 134: {"Bad user name or password": [PacketTypes.CONNACK]}, 72 | 135: {"Not authorized": [PacketTypes.CONNACK, PacketTypes.PUBACK, 73 | PacketTypes.PUBREC, PacketTypes.SUBACK, PacketTypes.UNSUBACK, 74 | PacketTypes.DISCONNECT], }, 75 | 136: {"Server unavailable": [PacketTypes.CONNACK]}, 76 | 137: {"Server busy": [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 77 | 138: {"Banned": [PacketTypes.CONNACK]}, 78 | 139: {"Server shutting down": [PacketTypes.DISCONNECT]}, 79 | 140: {"Bad authentication method": 80 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 81 | 141: {"Keep alive timeout": [PacketTypes.DISCONNECT]}, 82 | 142: {"Session taken over": [PacketTypes.DISCONNECT]}, 83 | 143: {"Topic filter invalid": 84 | [PacketTypes.SUBACK, PacketTypes.UNSUBACK, PacketTypes.DISCONNECT]}, 85 | 144: {"Topic name invalid": 86 | [PacketTypes.CONNACK, PacketTypes.PUBACK, 87 | PacketTypes.PUBREC, PacketTypes.DISCONNECT]}, 88 | 145: {"Packet identifier in use": 89 | [PacketTypes.PUBACK, PacketTypes.PUBREC, 90 | PacketTypes.SUBACK, PacketTypes.UNSUBACK]}, 91 | 146: {"Packet identifier not found": 92 | [PacketTypes.PUBREL, PacketTypes.PUBCOMP]}, 93 | 147: {"Receive maximum exceeded": [PacketTypes.DISCONNECT]}, 94 | 148: {"Topic alias invalid": [PacketTypes.DISCONNECT]}, 95 | 149: {"Packet too large": [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 96 | 150: {"Message rate too high": [PacketTypes.DISCONNECT]}, 97 | 151: {"Quota exceeded": [PacketTypes.CONNACK, PacketTypes.PUBACK, 98 | PacketTypes.PUBREC, PacketTypes.SUBACK, PacketTypes.DISCONNECT], }, 99 | 152: {"Administrative action": [PacketTypes.DISCONNECT]}, 100 | 153: {"Payload format invalid": 101 | [PacketTypes.PUBACK, PacketTypes.PUBREC, PacketTypes.DISCONNECT]}, 102 | 154: {"Retain not supported": 103 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 104 | 155: {"QoS not supported": 105 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 106 | 156: {"Use another server": 107 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 108 | 157: {"Server moved": 109 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 110 | 158: {"Shared subscription not supported": 111 | [PacketTypes.SUBACK, PacketTypes.DISCONNECT]}, 112 | 159: {"Connection rate exceeded": 113 | [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, 114 | 160: {"Maximum connect time": 115 | [PacketTypes.DISCONNECT]}, 116 | 161: {"Subscription identifiers not supported": 117 | [PacketTypes.SUBACK, PacketTypes.DISCONNECT]}, 118 | 162: {"Wildcard subscription not supported": 119 | [PacketTypes.SUBACK, PacketTypes.DISCONNECT]}, 120 | } 121 | if identifier == -1: 122 | if packetType == PacketTypes.DISCONNECT and aName == "Success": 123 | aName = "Normal disconnection" 124 | self.set(aName) 125 | else: 126 | self.value = identifier 127 | self.getName() # check it's good 128 | 129 | def __getName__(self, packetType, identifier): 130 | """ 131 | Get the reason code string name for a specific identifier. 132 | The name can vary by packet type for the same identifier, which 133 | is why the packet type is also required. 134 | 135 | Used when displaying the reason code. 136 | """ 137 | assert identifier in self.names.keys(), identifier 138 | names = self.names[identifier] 139 | namelist = [name for name in names.keys() if packetType in names[name]] 140 | assert len(namelist) == 1 141 | return namelist[0] 142 | 143 | def getId(self, name): 144 | """ 145 | Get the numeric id corresponding to a reason code name. 146 | 147 | Used when setting the reason code for a packetType 148 | check that only valid codes for the packet are set. 149 | """ 150 | identifier = None 151 | for code in self.names.keys(): 152 | if name in self.names[code].keys(): 153 | if self.packetType in self.names[code][name]: 154 | identifier = code 155 | break 156 | assert identifier != None, name 157 | return identifier 158 | 159 | def set(self, name): 160 | self.value = self.getId(name) 161 | 162 | def unpack(self, buffer): 163 | c = buffer[0] 164 | if sys.version_info[0] < 3: 165 | c = ord(c) 166 | name = self.__getName__(self.packetType, c) 167 | self.value = self.getId(name) 168 | return 1 169 | 170 | def getName(self): 171 | """Returns the reason code name corresponding to the numeric value which is set. 172 | """ 173 | return self.__getName__(self.packetType, self.value) 174 | 175 | def __eq__(self, other): 176 | if isinstance(other, int): 177 | return self.value == other 178 | if isinstance(other, str): 179 | return self.value == str(self) 180 | if isinstance(other, ReasonCodes): 181 | return self.value == other.value 182 | return False 183 | 184 | def __str__(self): 185 | return self.getName() 186 | 187 | def json(self): 188 | return self.getName() 189 | 190 | def pack(self): 191 | return bytearray([self.value]) -------------------------------------------------------------------------------- /plugin.service.mqtt/lib/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 v1.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-v10.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 | from __future__ import absolute_import 22 | 23 | from . import client as paho 24 | from .. import mqtt 25 | 26 | def _on_connect(client, userdata, flags, rc): 27 | """Internal callback""" 28 | if rc != 0: 29 | raise mqtt.MQTTException(paho.connack_string(rc)) 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 | callback : function of the form "on_message(client, userdata, message)" for 76 | processing the messages received. 77 | 78 | topics : either a string containing a single topic to subscribe to, or a 79 | list of topics to subscribe to. 80 | 81 | qos : the qos to use when subscribing. This is applied to all topics. 82 | 83 | userdata : passed to the callback 84 | 85 | hostname : a string containing the address of the broker to connect to. 86 | Defaults to localhost. 87 | 88 | port : the port to connect to the broker on. Defaults to 1883. 89 | 90 | client_id : the MQTT client id to use. If "" or None, the Paho library will 91 | generate a client id automatically. 92 | 93 | keepalive : the keepalive timeout value for the client. Defaults to 60 94 | seconds. 95 | 96 | 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 | Defaults to None, which indicates no will should be used. 101 | 102 | auth : a dict containing authentication parameters for the client: 103 | auth = {'username':"", 'password':""} 104 | Username is required, password is optional and will default to None 105 | if not provided. 106 | Defaults to None, which indicates no authentication is to be used. 107 | 108 | tls : a dict containing TLS configuration parameters for the client: 109 | dict = {'ca_certs':"", 'certfile':"", 110 | 'keyfile':"", 'tls_version':"", 111 | 'ciphers':", 'insecure':""} 112 | ca_certs is required, all other parameters are optional and will 113 | default to None if not provided, which results in the client using 114 | the default behaviour - see the paho.mqtt.client documentation. 115 | Alternatively, tls input can be an SSLContext object, which will be 116 | processed using the tls_set_context method. 117 | Defaults to None, which indicates that TLS should not be used. 118 | 119 | transport : set to "tcp" to use the default setting of transport which is 120 | raw TCP. Set to "websockets" to use WebSockets as the transport. 121 | 122 | clean_session : a boolean that determines the client type. If True, 123 | the broker will remove all information about this client 124 | when it disconnects. If False, the client is a persistent 125 | client and subscription information and queued messages 126 | will be retained when the client disconnects. 127 | Defaults to True. 128 | 129 | proxy_args: a dictionary that will be given to the client. 130 | """ 131 | 132 | if qos < 0 or qos > 2: 133 | raise ValueError('qos must be in the range 0-2') 134 | 135 | callback_userdata = { 136 | 'callback':callback, 137 | 'topics':topics, 138 | 'qos':qos, 139 | 'userdata':userdata} 140 | 141 | client = paho.Client(client_id=client_id, userdata=callback_userdata, 142 | protocol=protocol, transport=transport, 143 | clean_session=clean_session) 144 | client.on_message = _on_message_callback 145 | client.on_connect = _on_connect 146 | 147 | if proxy_args is not None: 148 | client.proxy_set(**proxy_args) 149 | 150 | if auth: 151 | username = auth.get('username') 152 | if username: 153 | password = auth.get('password') 154 | client.username_pw_set(username, password) 155 | else: 156 | raise KeyError("The 'username' key was not found, this is " 157 | "required for auth") 158 | 159 | if will is not None: 160 | client.will_set(**will) 161 | 162 | if tls is not None: 163 | if isinstance(tls, dict): 164 | insecure = tls.pop('insecure', False) 165 | client.tls_set(**tls) 166 | if insecure: 167 | # Must be set *after* the `client.tls_set()` call since it sets 168 | # up the SSL context that `client.tls_insecure_set` alters. 169 | client.tls_insecure_set(insecure) 170 | else: 171 | # Assume input is SSLContext object 172 | client.tls_set_context(tls) 173 | 174 | client.connect(hostname, port, keepalive) 175 | client.loop_forever() 176 | 177 | 178 | def simple(topics, qos=0, msg_count=1, retained=True, hostname="localhost", 179 | port=1883, client_id="", keepalive=60, will=None, auth=None, 180 | tls=None, protocol=paho.MQTTv311, transport="tcp", 181 | clean_session=True, proxy_args=None): 182 | """Subscribe to a list of topics and return msg_count messages. 183 | 184 | This function creates an MQTT client, connects to a broker and subscribes 185 | to a list of topics. Once "msg_count" messages have been received, it 186 | disconnects cleanly from the broker and returns the messages. 187 | 188 | topics : either a string containing a single topic to subscribe to, or a 189 | list of topics to subscribe to. 190 | 191 | qos : the qos to use when subscribing. This is applied to all topics. 192 | 193 | msg_count : the number of messages to retrieve from the broker. 194 | if msg_count == 1 then a single MQTTMessage will be returned. 195 | if msg_count > 1 then a list of MQTTMessages will be returned. 196 | 197 | retained : If set to True, retained messages will be processed the same as 198 | non-retained messages. If set to False, retained messages will 199 | be ignored. This means that with retained=False and msg_count=1, 200 | the function will return the first message received that does 201 | not have the retained flag set. 202 | 203 | hostname : a string containing the address of the broker to connect to. 204 | Defaults to localhost. 205 | 206 | port : the port to connect to the broker on. Defaults to 1883. 207 | 208 | client_id : the MQTT client id to use. If "" or None, the Paho library will 209 | generate a client id automatically. 210 | 211 | keepalive : the keepalive timeout value for the client. Defaults to 60 212 | seconds. 213 | 214 | will : a dict containing will parameters for the client: will = {'topic': 215 | "", 'payload':", 'qos':, 'retain':}. 216 | Topic is required, all other parameters are optional and will 217 | default to None, 0 and False respectively. 218 | Defaults to None, which indicates no will should be used. 219 | 220 | auth : a dict containing authentication parameters for the client: 221 | auth = {'username':"", 'password':""} 222 | Username is required, password is optional and will default to None 223 | if not provided. 224 | Defaults to None, which indicates no authentication is to be used. 225 | 226 | tls : a dict containing TLS configuration parameters for the client: 227 | dict = {'ca_certs':"", 'certfile':"", 228 | 'keyfile':"", 'tls_version':"", 229 | 'ciphers':", 'insecure':""} 230 | ca_certs is required, all other parameters are optional and will 231 | default to None if not provided, which results in the client using 232 | the default behaviour - see the paho.mqtt.client documentation. 233 | Alternatively, tls input can be an SSLContext object, which will be 234 | processed using the tls_set_context method. 235 | Defaults to None, which indicates that TLS should not be used. 236 | 237 | transport : set to "tcp" to use the default setting of transport which is 238 | raw TCP. Set to "websockets" to use WebSockets as the transport. 239 | 240 | clean_session : a boolean that determines the client type. If True, 241 | the broker will remove all information about this client 242 | when it disconnects. If False, the client is a persistent 243 | client and subscription information and queued messages 244 | will be retained when the client disconnects. 245 | Defaults to True. 246 | 247 | proxy_args: a dictionary that will be given to the client. 248 | """ 249 | 250 | if msg_count < 1: 251 | raise ValueError('msg_count must be > 0') 252 | 253 | # Set ourselves up to return a single message if msg_count == 1, or a list 254 | # if > 1. 255 | if msg_count == 1: 256 | messages = None 257 | else: 258 | messages = [] 259 | 260 | userdata = {'retained':retained, 'msg_count':msg_count, 'messages':messages} 261 | 262 | callback(_on_message_simple, topics, qos, userdata, hostname, port, 263 | client_id, keepalive, will, auth, tls, protocol, transport, 264 | clean_session, proxy_args) 265 | 266 | return userdata['messages'] 267 | -------------------------------------------------------------------------------- /plugin.service.mqtt/lib/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 v1.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-v10.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 | import sys 20 | 21 | 22 | class MQTTException(Exception): 23 | pass 24 | 25 | 26 | class SubscribeOptions(object): 27 | """The MQTT v5.0 subscribe options class. 28 | 29 | The options are: 30 | qos: As in MQTT v3.1.1. 31 | noLocal: True or False. If set to True, the subscriber will not receive its own publications. 32 | retainAsPublished: True or False. If set to True, the retain flag on received publications will be as set 33 | by the publisher. 34 | retainHandling: RETAIN_SEND_ON_SUBSCRIBE, RETAIN_SEND_IF_NEW_SUB or RETAIN_DO_NOT_SEND 35 | Controls when the broker should send retained messages: 36 | - RETAIN_SEND_ON_SUBSCRIBE: on any successful subscribe request 37 | - RETAIN_SEND_IF_NEW_SUB: only if the subscribe request is new 38 | - RETAIN_DO_NOT_SEND: never send retained messages 39 | """ 40 | 41 | # retain handling options 42 | RETAIN_SEND_ON_SUBSCRIBE, RETAIN_SEND_IF_NEW_SUB, RETAIN_DO_NOT_SEND = range( 43 | 0, 3) 44 | 45 | def __init__(self, qos=0, noLocal=False, retainAsPublished=False, retainHandling=RETAIN_SEND_ON_SUBSCRIBE): 46 | """ 47 | qos: 0, 1 or 2. 0 is the default. 48 | noLocal: True or False. False is the default and corresponds to MQTT v3.1.1 behavior. 49 | retainAsPublished: True or False. False is the default and corresponds to MQTT v3.1.1 behavior. 50 | retainHandling: RETAIN_SEND_ON_SUBSCRIBE, RETAIN_SEND_IF_NEW_SUB or RETAIN_DO_NOT_SEND 51 | RETAIN_SEND_ON_SUBSCRIBE is the default and corresponds to MQTT v3.1.1 behavior. 52 | """ 53 | object.__setattr__(self, "names", 54 | ["QoS", "noLocal", "retainAsPublished", "retainHandling"]) 55 | self.QoS = qos # bits 0,1 56 | self.noLocal = noLocal # bit 2 57 | self.retainAsPublished = retainAsPublished # bit 3 58 | self.retainHandling = retainHandling # bits 4 and 5: 0, 1 or 2 59 | assert self.QoS in [0, 1, 2] 60 | assert self.retainHandling in [ 61 | 0, 1, 2], "Retain handling should be 0, 1 or 2" 62 | 63 | def __setattr__(self, name, value): 64 | if name not in self.names: 65 | raise MQTTException( 66 | name + " Attribute name must be one of "+str(self.names)) 67 | object.__setattr__(self, name, value) 68 | 69 | def pack(self): 70 | assert self.QoS in [0, 1, 2] 71 | assert self.retainHandling in [ 72 | 0, 1, 2], "Retain handling should be 0, 1 or 2" 73 | noLocal = 1 if self.noLocal else 0 74 | retainAsPublished = 1 if self.retainAsPublished else 0 75 | data = [(self.retainHandling << 4) | (retainAsPublished << 3) | 76 | (noLocal << 2) | self.QoS] 77 | if sys.version_info[0] >= 3: 78 | buffer = bytes(data) 79 | else: 80 | buffer = bytearray(data) 81 | return buffer 82 | 83 | def unpack(self, buffer): 84 | b0 = buffer[0] 85 | self.retainHandling = ((b0 >> 4) & 0x03) 86 | self.retainAsPublished = True if ((b0 >> 3) & 0x01) == 1 else False 87 | self.noLocal = True if ((b0 >> 2) & 0x01) == 1 else False 88 | self.QoS = (b0 & 0x03) 89 | assert self.retainHandling in [ 90 | 0, 1, 2], "Retain handling should be 0, 1 or 2, not %d" % self.retainHandling 91 | assert self.QoS in [ 92 | 0, 1, 2], "QoS should be 0, 1 or 2, not %d" % self.QoS 93 | return 1 94 | 95 | def __repr__(self): 96 | return str(self) 97 | 98 | def __str__(self): 99 | return "{QoS="+str(self.QoS)+", noLocal="+str(self.noLocal) +\ 100 | ", retainAsPublished="+str(self.retainAsPublished) +\ 101 | ", retainHandling="+str(self.retainHandling)+"}" 102 | 103 | def json(self): 104 | data = { 105 | "QoS": self.QoS, 106 | "noLocal": self.noLocal, 107 | "retainAsPublished": self.retainAsPublished, 108 | "retainHandling": self.retainHandling, 109 | } 110 | return data 111 | -------------------------------------------------------------------------------- /plugin.service.mqtt/resources/language/English/strings.po: -------------------------------------------------------------------------------- 1 | # Kodi Media Center language file 2 | # Addon Name: MQTT Adapter 3 | # Addon id: service.mqtt 4 | # Addon Provider: owagner 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: XBMC Addons\n" 8 | "Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" 9 | "POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 11 | "Last-Translator: Kodi Translation Team\n" 12 | "Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Language: en\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 18 | 19 | msgctxt "#30001" 20 | msgid "General" 21 | msgstr "" 22 | 23 | msgctxt "#30011" 24 | msgid "Broker IP" 25 | msgstr "" 26 | 27 | msgctxt "#30012" 28 | msgid "Broker port" 29 | msgstr "" 30 | 31 | msgctxt "#30013" 32 | msgid "Topic prefix" 33 | msgstr "" 34 | 35 | msgctxt "#30014" 36 | msgid "MQTT Debug Logging" 37 | msgstr "" 38 | 39 | msgctxt "#30100" 40 | msgid "Authentication" 41 | msgstr "" 42 | 43 | msgctxt "#30101" 44 | msgid "Use anonymous connection" 45 | msgstr "" 46 | 47 | msgctxt "#30102" 48 | msgid "Username" 49 | msgstr "" 50 | 51 | msgctxt "#30103" 52 | msgid "Password" 53 | msgstr "" 54 | 55 | msgctxt "#30104" 56 | msgid "Use TLS connection" 57 | msgstr "" 58 | 59 | msgctxt "#30105" 60 | msgid "TLS broker CA crt" 61 | msgstr "" 62 | 63 | msgctxt "#30106" 64 | msgid "Use client certificates" 65 | msgstr "" 66 | 67 | msgctxt "#30107" 68 | msgid "TLS client certificate" 69 | msgstr "" 70 | 71 | msgctxt "#30108" 72 | msgid "TLS client key" 73 | msgstr "" 74 | 75 | msgctxt "#30200" 76 | msgid "Advanced" 77 | msgstr "" 78 | 79 | msgctxt "#30201" 80 | msgid "Publish progress" 81 | msgstr "" 82 | 83 | msgctxt "#30202" 84 | msgid "Progress interval (in seconds)" 85 | msgstr "" 86 | 87 | msgctxt "#30203" 88 | msgid "Publish details" 89 | msgstr "" 90 | 91 | msgctxt "#30204" 92 | msgid "Ignore words (comma separated)" 93 | msgstr "" 94 | 95 | msgctxt "#30205" 96 | msgid "Publish progress with milliseconds" 97 | msgstr "" 98 | -------------------------------------------------------------------------------- /plugin.service.mqtt/resources/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /plugin.service.mqtt/service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import xbmc,xbmcaddon 4 | import json 5 | import threading 6 | import time 7 | import os 8 | import socket 9 | from lib import client as mqtt 10 | 11 | __addon__ = xbmcaddon.Addon() 12 | __version__ = __addon__.getAddonInfo('version') 13 | 14 | def getSetting(setting): 15 | return __addon__.getSetting(setting).strip() 16 | 17 | def load_settings(): 18 | global mqttprogress,mqttinterval,mqttdetails,mqttignore 19 | mqttprogress = getSetting('mqttprogress').lower() == "true" 20 | mqttinterval = int(getSetting('mqttinterval')) 21 | mqttdetails = getSetting('mqttdetails').lower() == "true" 22 | mqttignore = getSetting('mqttignore') 23 | if mqttignore: 24 | mqttignore = mqttignore.lower().split(',') 25 | 26 | activeplayerid=-1 27 | activeplayertype="" 28 | playbackstate=0 29 | lasttitle="" 30 | lastdetail={} 31 | 32 | # 33 | # Returns true when no words are found, false on one or more matches 34 | # 35 | def ignorelist(data,val): 36 | if val == "filepath": 37 | val=xbmc.Player().getPlayingFile() 38 | return all(val.lower().find (v.strip()) <= -1 for v in data) 39 | 40 | def mqttlogging(log): 41 | if __addon__.getSetting("mqttdebug")=='true': 42 | xbmc.log(log,level=xbmc.LOGINFO) 43 | 44 | def sendrpc(method,params): 45 | res=xbmc.executeJSONRPC(json.dumps({"jsonrpc":"2.0","method":method,"params":params,"id":1})) 46 | mqttlogging("MQTT: JSON-RPC call "+method+" returned "+res) 47 | return json.loads(res) 48 | 49 | def setvol(data): 50 | params=json.loads('{"volume":' + str(data) + '}') 51 | sendrpc("Application.SetVolume",params) 52 | #res=xbmc.executebuiltin("XBMC.SetVolume("+data+")") 53 | xbmc.log(data) 54 | # 55 | # Publishes a MQTT message. The topic is built from the configured 56 | # topic prefix and the suffix. The message itself is JSON encoded, 57 | # with the "val" field set, and possibly more fields merged in. 58 | # 59 | def publish(suffix,val,more): 60 | global topic,mqc 61 | robj={} 62 | robj["val"]=val 63 | if more is not None: 64 | robj.update(more) 65 | jsonstr=json.dumps(robj) 66 | fulltopic=topic+"status/"+suffix 67 | mqttlogging("MQTT: Publishing @"+fulltopic+": "+jsonstr) 68 | mqc.publish(fulltopic,jsonstr,qos=0,retain=True) 69 | 70 | # 71 | # Set and publishes the playback state. Publishes more info if 72 | # the state is "playing" 73 | # 74 | def setplaystate(state,detail): 75 | global activeplayerid,activeplayertype,playbackstate 76 | playbackstate=state 77 | if state==1: 78 | res=sendrpc("Player.GetActivePlayers",{}) 79 | activeplayerid=res["result"][0]["playerid"] 80 | activeplayertype=res["result"][0]["type"] 81 | if mqttdetails and ignorelist(mqttignore,"filepath"): 82 | res=sendrpc("Player.GetProperties",{"playerid":activeplayerid,"properties":["speed","currentsubtitle","currentaudiostream","repeat","subtitleenabled"]}) 83 | publish("playbackstate",state,{"kodi_state":detail,"kodi_playbackdetails":res["result"],"kodi_playerid":activeplayerid,"kodi_playertype":activeplayertype,"kodi_timestamp":int(time.time())}) 84 | publishdetails() 85 | else: 86 | publish("playbackstate",state,{"kodi_state":detail,"kodi_playerid":activeplayerid,"kodi_playertype":activeplayertype,"kodi_timestamp":int(time.time())}) 87 | else: 88 | publish("playbackstate",state,{"kodi_state":detail,"kodi_playerid":activeplayerid,"kodi_playertype":activeplayertype,"kodi_timestamp":int(time.time())}) 89 | 90 | def convtime(ts): 91 | return("%02d:%02d:%02d" % (ts/3600,(ts/60)%60,ts%60)) 92 | 93 | # 94 | # Publishes playback progress 95 | # 96 | def publishprogress(): 97 | global player 98 | if not player.isPlaying(): 99 | return 100 | pt=player.getTime() 101 | tt=player.getTotalTime() 102 | if pt<0: 103 | pt=0 104 | if tt>0: 105 | progress=(pt*100)/tt 106 | else: 107 | progress=0 108 | state={"kodi_time":convtime(pt),"kodi_totaltime":convtime(tt)} 109 | publish("progress","%.1f" % progress,state) 110 | # Publish title at interval, this is for things like radio streams that change the title without notification 111 | title=xbmc.getInfoLabel('Player.Title') 112 | publish("playertitle",title,{}) 113 | 114 | # 115 | # Publish more details about the currently playing item 116 | # 117 | 118 | def publishdetails(): 119 | global player,activeplayerid 120 | global lasttitle,lastdetail 121 | if not player.isPlaying(): 122 | return 123 | if ignorelist(mqttignore,"filepath"): 124 | res=sendrpc("Player.GetItem",{"playerid":activeplayerid,"properties":["title","streamdetails","file","thumbnail","fanart"]}) 125 | if "result" in res: 126 | newtitle=res["result"]["item"]["title"] 127 | newdetail={"kodi_details":res["result"]["item"]} 128 | if newtitle!=lasttitle or newdetail!=lastdetail: 129 | lasttitle=newtitle 130 | lastdetail=newdetail 131 | if ignorelist(mqttignore,newtitle): 132 | publish("title",newtitle,newdetail) 133 | if mqttprogress: 134 | publishprogress() 135 | 136 | # 137 | # Notification subclasses 138 | # 139 | class MQTTMonitor(xbmc.Monitor): 140 | def onSettingsChanged(self): 141 | global mqc 142 | mqttlogging("MQTT: Settings changed, reconnecting broker") 143 | mqc.loop_stop(True) 144 | load_settings() 145 | startmqtt() 146 | 147 | def onNotification(self,sender,method,data): 148 | publish("notification/"+method,data,None) 149 | 150 | # fix for netflixaddon - so that start notification 151 | try: 152 | if method == 'Player.OnAVStart': 153 | setplaystate(1,"started") 154 | except Exception: 155 | import traceback 156 | mqttlogging("MQTT: "+traceback.format_exc()) 157 | 158 | def onScreensaverActivated(self): 159 | publish("screensaver",1,"") 160 | 161 | def onScreensaverDeactivated(self): 162 | publish("screensaver",0,"") 163 | 164 | class MQTTPlayer(xbmc.Player): 165 | 166 | def onAVStarted(self): 167 | setplaystate(1, "started") 168 | 169 | def onPlayBackPaused(self): 170 | setplaystate(2,"paused") 171 | 172 | def onPlayBackResumed(self): 173 | setplaystate(1,"resumed") 174 | 175 | def onPlayBackEnded(self): 176 | setplaystate(0,"ended") 177 | 178 | def onPlayBackStopped(self): 179 | setplaystate(0,"stopped") 180 | 181 | def onPlayBackSeek(self, time, seek_offset): 182 | publishprogress() 183 | 184 | def onPlayBackSeekChapter(self, chapter): 185 | publishprogress() 186 | 187 | def onPlayBackSpeedChanged(self,speed): 188 | setplaystate(1,"speed") 189 | 190 | def onQueueNextItem(self): 191 | mqttlogging("MQTT onqn") 192 | 193 | # 194 | # Handles commands 195 | # 196 | def processnotify(data): 197 | try: 198 | params=json.loads(data) 199 | except ValueError: 200 | parts = data.split(None, 1) 201 | params={"title":parts[0],"message":parts[1]} 202 | sendrpc("GUI.ShowNotification",params) 203 | 204 | def processplay(data): 205 | try: 206 | params=json.loads(data) 207 | sendrpc("Player.Open",params) 208 | except ValueError: 209 | player.play(data) 210 | 211 | def processvolume(data): 212 | try: 213 | vol = int(data) 214 | setvol(vol) 215 | except ValueError: 216 | params=json.loads(data) 217 | sendrpc("Application.SetVolume",params) 218 | 219 | def processplaybackstate(data): 220 | global playbackstate 221 | if data=="0" or data=="stop": 222 | player.stop() 223 | elif data=="1" or data=="resume" or data=="play": 224 | if playbackstate==2: 225 | player.pause() 226 | elif playbackstate!=1: 227 | player.play() 228 | elif data=="2" or data=="pause": 229 | if playbackstate==1: 230 | player.pause() 231 | elif data=="toggle": 232 | if playbackstate==1 or playbackstate==2: 233 | player.pause() 234 | elif data=="next": 235 | player.playnext() 236 | elif data=="previous": 237 | player.playprevious() 238 | elif data=="playcurrent": 239 | path = xbmc.getInfoLabel('ListItem.FileNameAndPath') 240 | sendrpc("Player.Open", {"item": {"file": path}}) 241 | 242 | def processprogress(data): 243 | hours, minutes, seconds = [int(i) for i in data.split(":")] 244 | time = hours * 3600 + minutes * 60 + seconds 245 | player.seekTime(time) 246 | 247 | def processsendcomand(data): 248 | try: 249 | cmd=json.loads(data) 250 | res=xbmc.executeJSONRPC(json.dumps(cmd)) 251 | mqttlogging("MQTT: JSON-RPC call "+cmd['method']+" returned "+res) 252 | except ValueError: 253 | mqttlogging("MQTT: JSON-RPC call ValueError") 254 | 255 | def processcecstate(data): 256 | if data=="1" or data=="activate": 257 | #Stupid workaround to wake TV 258 | mqttlogging("CEC Activate") 259 | os.system('kodi-send --action=""') 260 | 261 | def processcommand(topic,data): 262 | mqttlogging("MQTT: Received command %s with data %s" % (topic, data)) 263 | if topic=="notify": 264 | processnotify(data) 265 | elif topic=="play": 266 | processplay(data) 267 | elif topic=="playbackstate": 268 | processplaybackstate(data) 269 | elif topic=="progress": 270 | processprogress(data) 271 | elif topic=="api": 272 | processsendcomand(data) 273 | elif topic=="volume": 274 | processvolume(data) 275 | elif topic=="cecstate": 276 | processcecstate(data) 277 | else: 278 | mqttlogging("MQTT: Unknown command "+topic) 279 | 280 | # 281 | # Handles incoming MQTT messages 282 | # 283 | def msghandler(mqc,userdata,msg): 284 | try: 285 | global topic 286 | if msg.retain: 287 | return 288 | mytopic=msg.topic[len(topic):] 289 | if mytopic.startswith("command/"): 290 | processcommand(mytopic[8:],msg.payload.decode("utf-8")) 291 | except Exception as e: 292 | mqttlogging("MQTT: Error processing message %s: %s" % (type(e).__name__,e)) 293 | 294 | def connecthandler(mqc,userdata,flags,rc): 295 | mqttlogging("MQTT: Connected to MQTT broker with rc=%d" % (rc)) 296 | mqc.publish(topic+"connected",2,qos=1,retain=True) 297 | mqc.subscribe(topic+"command/#",qos=0) 298 | 299 | def disconnecthandler(mqc,userdata,rc): 300 | mqttlogging("MQTT: Disconnected from MQTT broker with rc=%d" % (rc)) 301 | time.sleep(5) 302 | try: 303 | mqc.reconnect() 304 | except Exception as e: 305 | mqttlogging("MQTT: Error while reconnectig: message %s: %s" % (type(e).__name__,e)) 306 | 307 | # 308 | # Starts connection to the MQTT broker, sets the will 309 | # and subscribes to the command topic 310 | # 311 | def startmqtt(): 312 | global topic,mqc 313 | mqc=mqtt.Client() 314 | mqc.on_message=msghandler 315 | mqc.on_connect=connecthandler 316 | mqc.on_disconnect=disconnecthandler 317 | if __addon__.getSetting("mqttanonymousconnection")=='false': 318 | mqc.username_pw_set(__addon__.getSetting("mqttusername"), __addon__.getSetting("mqttpassword")) 319 | mqttlogging("MQTT: Anonymous disabled, connecting as user: %s" % __addon__.getSetting("mqttusername")) 320 | if __addon__.getSetting("mqtttlsconnection")=='true' and __addon__.getSetting("mqtttlsconnectioncrt")!='' and __addon__.getSetting("mqtttlsclient")=='false': 321 | mqc.tls_set(__addon__.getSetting("mqtttlsconnectioncrt")) 322 | mqttlogging("MQTT: TLS enabled, connecting using CA certificate: %s" % __addon__.getSetting("mqtttlsconnectioncrt")) 323 | elif __addon__.getSetting("mqtttlsconnection")=='true' and __addon__.getSetting("mqtttlsclient")=='true' and __addon__.getSetting("mqtttlsclientcrt")!='' and __addon__.getSetting("mqtttlsclientkey")!='': 324 | mqc.tls_set(__addon__.getSetting("mqtttlsconnectioncrt"), __addon__.getSetting("mqtttlsclientcrt"), __addon__.getSetting("mqtttlsclientkey")) 325 | mqttlogging("MQTT: TLS with client certificates enabled, connecting using certificates CA: %s, client %s and key: %s" % (__addon__.getSetting("mqttusername"), __addon__.getSetting("mqtttlsclientcrt"), __addon__.getSetting("mqtttlsclientkey"))) 326 | topic=__addon__.getSetting("mqtttopic") 327 | if not topic.endswith("/"): 328 | topic+="/" 329 | mqc.will_set(topic+"connected",0,qos=2,retain=True) 330 | sleep=2 331 | for attempt in range(10): 332 | try: 333 | mqttlogging("MQTT: Connecting to MQTT broker at %s:%s" % (__addon__.getSetting("mqtthost"),__addon__.getSetting("mqttport"))) 334 | mqc.connect(__addon__.getSetting("mqtthost"),int(__addon__.getSetting("mqttport")),60) 335 | except socket.error: 336 | mqttlogging("MQTT: Socket error raised, retry in %d seconds" % sleep) 337 | monitor.waitForAbort(sleep) 338 | sleep=sleep*2 339 | else: 340 | break 341 | else: 342 | mqttlogging("MQTT: No connection possible, giving up") 343 | return(False) 344 | mqc.loop_start() 345 | return(True) 346 | 347 | # 348 | # Addon initialization and shutdown 349 | # 350 | if (__name__ == "__main__"): 351 | global monitor,player 352 | mqttlogging('MQTT: MQTT Adapter Version %s started' % __version__) 353 | load_settings() 354 | monitor=MQTTMonitor() 355 | if startmqtt(): 356 | player=MQTTPlayer() 357 | # Publish a reasonable initial state. Fancier would be to check actual current state. 358 | setplaystate(0,"stopped") 359 | if mqttprogress: 360 | mqttlogging("MQTT: Progress Publishing enabled, interval is set to %d seconds" % mqttinterval) 361 | while not monitor.waitForAbort(mqttinterval): 362 | publishprogress() 363 | else: 364 | mqttlogging("MQTT: Progress Publishing disabled, waiting for abort") 365 | monitor.waitForAbort() 366 | mqc.loop_stop(True) 367 | mqttlogging("MQTT: Shutting down") 368 | --------------------------------------------------------------------------------