├── .gitignore
├── plugin.service.mqtt
├── icon.png
├── lib
│ ├── __init__.py
│ ├── packettypes.py
│ ├── matcher.py
│ ├── subscribeoptions.py
│ ├── reasoncodes.py
│ ├── publish.py
│ ├── subscribe.py
│ └── properties.py
├── addon.xml
├── LICENSE.txt
├── resources
│ ├── settings.xml
│ └── language
│ │ └── English
│ │ └── strings.po
├── changelog.txt
└── service.py
├── .gitattributes
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyo
2 | *.zip
3 | .idea/
4 |
--------------------------------------------------------------------------------
/plugin.service.mqtt/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/void-spark/kodi2mqtt/HEAD/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------