/
properties.py
242 lines (195 loc) · 8.62 KB
/
properties.py
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
import logging
from json import loads
import mimetypes
# only used for publishing
TOPIC_FRIENDLY_NAME = "chromecast/%s/friendly_name"
TOPIC_CONNECTION_STATUS = "chromecast/%s/connection_status"
TOPIC_CAST_TYPE = "chromecast/%s/cast_type"
TOPIC_CURRENT_APP = "chromecast/%s/current_app"
TOPIC_PLAYER_DURATION = "chromecast/%s/player_duration"
TOPIC_PLAYER_POSITION = "chromecast/%s/player_position"
TOPIC_PLAYER_STATE = "chromecast/%s/player_state"
TOPIC_VOLUME_LEVEL = "chromecast/%s/volume_level"
TOPIC_VOLUME_MUTED = "chromecast/%s/volume_muted"
TOPIC_MEDIA_TITLE = "chromecast/%s/media/title"
TOPIC_MEDIA_ALBUM_NAME = "chromecast/%s/media/album_name"
TOPIC_MEDIA_ARTIST = "chromecast/%s/media/artist"
TOPIC_MEDIA_ALBUM_ARTIST = "chromecast/%s/media/album_artist"
TOPIC_MEDIA_TRACK = "chromecast/%s/media/track"
TOPIC_MEDIA_IMAGES = "chromecast/%s/media/images"
TOPIC_MEDIA_CONTENT_TYPE = "chromecast/%s/media/content_type"
TOPIC_MEDIA_CONTENT_URL = "chromecast/%s/media/content_url"
# subscribe
TOPIC_COMMAND_VOLUME_LEVEL = "chromecast/%s/command/volume_level"
TOPIC_COMMAND_VOLUME_MUTED = "chromecast/%s/command/volume_muted"
TOPIC_COMMAND_PLAYER_POSITION = "chromecast/%s/command/player_position"
TOPIC_COMMAND_PLAYER_STATE = "chromecast/%s/command/player_state"
STATE_REQUEST_RESUME = "RESUME"
STATE_REQUEST_PAUSE = "PAUSE"
STATE_REQUEST_STOP = "STOP"
STATE_REQUEST_SKIP = "SKIP"
STATE_REQUEST_REWIND = "REWIND"
# play stream has another syntax, not listed here therefore
class MqttChangesCallback:
def on_volume_mute_requested(self, is_muted):
pass
def on_volume_level_relative_requested(self, relative_value):
pass
def on_volume_level_absolute_requested(self, absolute_value):
pass
def on_player_position_requested(self, position):
pass
def on_player_play_stream_requested(self, content_url, content_type):
pass
def on_player_pause_requested(self):
pass
def on_player_resume_requested(self):
pass
def on_player_stop_requested(self):
pass
def on_player_skip_requested(self):
pass
def on_player_rewind_requested(self):
pass
class MqttPropertyHandler:
def __init__(self, mqtt_connection, mqtt_topic_filter, changes_callback):
self.logger = logging.getLogger("mqtt")
self.mqtt = mqtt_connection
self.topic_filter = mqtt_topic_filter
self.changes_callback = changes_callback
self.write_filter = {}
def is_topic_filter_matching(self, topic):
"""
Check if a topic (e.g.: chromecast/192.168.0.1/player_state) matches our filter (the ip address part).
"""
try:
return topic.split("/")[1] == self.topic_filter
except IndexError:
return False
def _write(self, topic, value):
# noinspection PyBroadException
try:
if isinstance(value, float):
if 0 <= value <= 1: # chromecast volume
value *= 100
value = str(round(value))
elif isinstance(value, bool):
if value:
value = "1"
else:
value = "0"
elif value is None:
value = ""
else:
value = str(value)
formatted_topic = topic % self.topic_filter
# filter to prevent writing the same value again until it has changed
if formatted_topic in self.write_filter and self.write_filter[formatted_topic] == value:
return
self.write_filter[formatted_topic] = value
self.mqtt.send_message(formatted_topic, value)
except Exception:
self.logger.exception("value conversion error")
def write_cast_status(self, app_name, volume_level, is_volume_muted):
self._write(TOPIC_CURRENT_APP, app_name)
self._write(TOPIC_VOLUME_LEVEL, volume_level)
self._write(TOPIC_VOLUME_MUTED, is_volume_muted)
def write_player_status(self, state, current_time, duration):
self._write(TOPIC_PLAYER_STATE, state)
self._write(TOPIC_PLAYER_POSITION, current_time)
self._write(TOPIC_PLAYER_DURATION, duration)
def write_media_status(self, title, album_name, artist, album_artist, track, images, content_type, content_id):
self._write(TOPIC_MEDIA_TITLE, title)
self._write(TOPIC_MEDIA_ALBUM_NAME, album_name)
self._write(TOPIC_MEDIA_ARTIST, artist)
self._write(TOPIC_MEDIA_ALBUM_ARTIST, album_artist)
self._write(TOPIC_MEDIA_TRACK, track)
self._write(TOPIC_MEDIA_IMAGES, images)
self._write(TOPIC_MEDIA_CONTENT_TYPE, content_type)
self._write(TOPIC_MEDIA_CONTENT_URL, content_id)
def write_connection_status(self, status):
self._write(TOPIC_CONNECTION_STATUS, status)
def write_cast_data(self, cast_type, friendly_name):
self._write(TOPIC_CAST_TYPE, cast_type)
self._write(TOPIC_FRIENDLY_NAME, friendly_name)
def handle_message(self, topic, payload):
if isinstance(payload, bytes):
payload = payload.decode('utf-8')
payload = str(payload).strip()
if TOPIC_COMMAND_VOLUME_MUTED % self.topic_filter == topic:
self.handle_volume_mute_change(payload)
elif TOPIC_COMMAND_VOLUME_LEVEL % self.topic_filter == topic:
self.handle_volume_level_change(payload)
elif TOPIC_COMMAND_PLAYER_POSITION % self.topic_filter == topic:
self.handle_player_position_change(payload)
elif TOPIC_COMMAND_PLAYER_STATE % self.topic_filter == topic:
self.handle_player_state_change(payload)
def handle_volume_mute_change(self, payload):
"""
Change volume mute where 1 = muted, 0 = unmuted.
"""
if payload != "0" and payload != "1":
return
self.changes_callback.on_volume_mute_requested(payload == "1")
def handle_volume_level_change(self, payload):
"""
Change volume level to either absolute value between 0 .. 100 or by relative offset (prefix with "-" or "+",
e.g +5 or -10).
"""
if len(payload) == 0:
return
is_relative = payload[0] == "-" or payload[0] == "+"
# noinspection PyBroadException
try:
value = int(payload)
except Exception:
self.logger.exception("failed decoding requested volume level")
return
if is_relative:
self.changes_callback.on_volume_level_relative_requested(value)
else:
self.changes_callback.on_volume_level_absolute_requested(value)
def handle_player_position_change(self, payload):
"""
Change current player position
"""
if len(payload) == 0:
return
# noinspection PyBroadException
try:
value = int(payload)
self.changes_callback.on_player_position_requested(value)
except Exception:
self.logger.exception("failed decoding requested position")
def handle_player_state_change(self, payload):
if payload == STATE_REQUEST_PAUSE:
self.changes_callback.on_player_pause_requested()
elif payload == STATE_REQUEST_RESUME:
self.changes_callback.on_player_resume_requested()
elif payload == STATE_REQUEST_STOP:
self.changes_callback.on_player_stop_requested()
elif payload == STATE_REQUEST_SKIP:
self.changes_callback.on_player_skip_requested()
elif payload == STATE_REQUEST_REWIND:
self.changes_callback.on_player_rewind_requested()
else:
if len(payload) == 0:
return
# noinspection PyBroadException
try:
if payload[0] != "[":
url = payload
mime_data = mimetypes.guess_type(url, strict=False)
found_mime_type = None
if mime_data is not None:
found_mime_type = mime_data[0]
if found_mime_type is None:
self.logger.warning("no mime type found")
self.changes_callback.on_player_play_stream_requested(url, found_mime_type)
else:
data = loads(payload)
if not isinstance(data, list) or len(data) != 2:
raise AssertionError("data must be array and must possess two elements (url, content type)")
self.changes_callback.on_player_play_stream_requested(data[0], data[1])
except Exception:
self.logger.exception("failed decoding requested play stream data: %s" % payload)