-
Notifications
You must be signed in to change notification settings - Fork 0
/
plugin.py
389 lines (337 loc) · 21 KB
/
plugin.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
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
#! /usr/bin/env python
# -*- coding: utf-8 -*-
#######################################################################################
# Domotics Pad Mobile Client Plugin by RogueProeliator <rp@rogueproeliator.com>
# Indigo plugin designed to interface with the various Google services supported by
# Domotics Pad, such as mobile clients and Google Home devices
#######################################################################################
# region Python imports
import json
import requests
import RPFramework
import domoPadDevices
import dicttoxml
from RPFramework.RPFrameworkPlugin import RPFrameworkPlugin
from RPFramework.RPFrameworkCommand import RPFrameworkCommand
# endregion
# region Constants and configuration variables
DOMOPADCOMMAND_SENDNOTIFICATION = "SendNotification"
DOMOPADCOMMAND_SPEAKANNOUNCEMENTNOTIFICATION = "SendTextToSpeechNotification"
DOMOPADCOMMAND_CPDISPLAYNOTIFICATION = "SendCPDisplayRequest"
DOMOPADCOMMAND_DEVICEUPDATEREQUESTNOTIFICATION = "RequestDeviceStatusUpdate"
# endregion
#######################################################################################
# Plugin
# Primary Indigo plugin class that is universal for all devices (receivers) to be
# controlled
#######################################################################################
class Plugin(RPFrameworkPlugin):
#######################################################################################
# region Class construction and destruction methods
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Constructor called once upon plugin class creation; setup the device tracking
# variables for later use
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def __init__(self, plugin_id, plugin_display_name, plugin_version, plugin_prefs):
# RP framework base class's init method
super().__init__(plugin_id, plugin_display_name, plugin_version, plugin_prefs, managed_device_class_module=domoPadDevices)
# initialize the member variable that tracks whether we are reporting device
# states back to Google Home
self.report_state_to_assistant = plugin_prefs.get("sendUpdatesToGoogle", False)
# endregion
#######################################################################################
#######################################################################################
# region Indigo control methods
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# startup is called by Indigo whenever the plugin is first starting up (by a restart
# of Indigo server or the plugin or an update
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def startup(self):
super(Plugin, self).startup()
# endregion
#######################################################################################
#######################################################################################
# region Action/command processing routines
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# This routine will be called to handle any unknown commands at the plugin level; it
# can/should be overridden in the plugin implementation (if needed)
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def handle_unknown_plugin_command(self, rp_command, requeue_commands_list):
if rp_command.command_name == DOMOPADCOMMAND_SENDNOTIFICATION:
self.logger.threaddebug(f"Push Notification Send Command: DevicePairID={rp_command.command_payload[0]}; Type={rp_command.command_payload[2]}; Message={rp_command.command_payload[1]}")
# set up the defaults so that we know all the parameters have a value...
query_string_params = { "devicePairingId": rp_command.command_payload[0],
"notificationType": "Alert",
"priority": rp_command.command_payload[2],
"message": f"{rp_command.command_payload[1]}"}
query_string_params["action1Name"] = ""
query_string_params["action1Group"] = ""
query_string_params["action2Name"] = ""
query_string_params["action2Group"] = ""
# build the query string as it must be URL encoded
if rp_command.command_payload[3] != "" and rp_command.command_payload[4] != "":
self.logger.threaddebug(f"Push Notification Send Action 1: {rp_command.command_payload[3]} => {rp_command.command_payload[4]}")
query_string_params["action1Name"] = f"{rp_command.command_payload[3]}"
query_string_params["action1Group"] = f"{rp_command.command_payload[4]}"
query_string_params["notificationType"] = "ActionAlert"
if rp_command.command_payload[5] != "" and rp_command.command_payload[6] != "":
self.logger.threaddebug(f"Push Notification Send Action 2: {rp_command.command_payload[5]} => {rp_command.command_payload[6]}")
query_string_params["action2Name"] = f"{rp_command.command_payload[5]}"
query_string_params["action2Group"] = f"{rp_command.command_payload[6]}"
query_string_params["notificationType"] = "ActionAlert"
self.logger.threaddebug(f"Push Notification Payload={json.dumps(query_string_params)}")
# this routine is executed asynchronously and thus can directly send the
# request to the server
api_endpoint_url = "https://com-duncanware-domopad.appspot.com/_ah/api/messaging/v1/sendActionablePushNotification"
try:
response = requests.post(api_endpoint_url, data=json.dumps(query_string_params))
self.logger.threaddebug(f"Push notification Response: [{response.status_code}] {response.text}")
if response.status_code == 204:
self.logger.debug("Push notification sent successfully")
else:
self.logger.error("Error sending push notification.")
except:
self.logger.exception("Error sending push notification.")
elif rp_command.command_name == DOMOPADCOMMAND_CPDISPLAYNOTIFICATION:
self.logger.threaddebug(f"Control Page Display Request Command: Id={rp_command.command_payload[0]}; Page={rp_command.command_payload[1]}")
# load the control page name so that we may pass it along to the device
requested_page = indigo.rawServerRequest("GetControlPage", {"ID": rp_command.command_payload[1]})
cp_page_name = requested_page["Name"]
query_string_params = {"devicePairingId": rp_command.command_payload[0], "pageRequested": rp_command.command_payload[1], "pageName": cp_page_name}
api_endpoint_url = "https://com-duncanware-domopad.appspot.com/_ah/api/messaging/v1/sendControlPageDisplayRequest"
try:
response = requests.post(api_endpoint_url, data=json.dumps(query_string_params))
self.logger.threaddebug(f"Control Page Display Request Response: [{response.status_code}] {response.text}")
if response.status_code == 204:
self.logger.debug("Control page display request sent successfully")
else:
self.logger.error("Error sending control page display request")
except:
self.logger.exception("Error sending control page display request")
elif rp_command.command_name == DOMOPADCOMMAND_DEVICEUPDATEREQUESTNOTIFICATION:
self.logger.threaddebug(f"Device Status Update Request: Device Id={rp_command.command_payload}")
query_string_params = {"devicePairingId": rp_command.command_payload}
api_endpoint_url = "https://com-duncanware-domopad.appspot.com/_ah/api/messaging/v1/sendDeviceStatusUpdateRequest"
try:
response = requests.post(api_endpoint_url, data=json.dumps(query_string_params))
self.logger.threaddebug(f"Device Status Update Request Response: [{response.status_code}] {response.text}")
if response.status_code == 204:
self.logger.debug("Device status update request sent successfully")
else:
self.logger.error("Error sending device status update request")
except:
self.logger.exception("Error requesting device status update")
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# This routine will process the Send Notification action... it will queue up the
# command for the plugin to process asynchronously
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def process_send_notification(self, action):
rp_device = self.managed_devices[action.deviceId]
registration_id = rp_device.indigoDevice.pluginProps.get("deviceRegistrationId", "")
message = self.substitute(action.props.get("message"))
importance_level = action.props.get("importanceLevel")
action1_name = action.props.get("action1Name" , "")
action1_group = action.props.get("action1Group", "")
action2_name = action.props.get("action2Name" , "")
action2_group = action.props.get("action2Group", "")
if registration_id == "":
indigo.server.log(f"Unable to send push notification to {rp_device.indigoDevice.deviceId}; the device is not paired.", isError=True)
else:
self.logger.threaddebug(f"Queuing push notification command for {action.deviceId}")
self.plugin_command_queue.put(RPFrameworkCommand(DOMOPADCOMMAND_SENDNOTIFICATION, command_payload=(
registration_id, message, importance_level, action1_name, action1_group, action2_name, action2_group)))
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# This routine is called whenever the user has clicked to clear his/her selection of
# an action in slot 1
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def clear_notification_action_1(self, values_dict, type_id, dev_id):
values_dict["action1Name"] = ""
values_dict["action1Group"] = ""
return values_dict
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# This routine is called whenever the user has clicked to clear his/her selection of
# an action in slot 2
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def clear_notification_action_2(self, values_dict, type_id, dev_id):
values_dict["action2Name"] = ""
values_dict["action2Group"] = ""
return values_dict
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# This routine will send the Speak Announcement command to an Android Device
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def process_speak_announcement_notification(self, action):
rp_device = self.managed_devices[action.deviceId]
device_registration_id = rp_device.indigoDevice.pluginProps.get("deviceRegistrationId", "")
announcement_msg = action.props.get("announcement", "")
if device_registration_id == "":
self.logger.error(f"Unable to send speak announcement request notification to {rp_device.indigoDevice.deviceId}; the device is not paired.")
elif announcement_msg == "":
self.logger.error(f"Unable to send speak announcement request notification to {rp_device.indigoDevice.deviceId}; no announcement text was entered.")
else:
self.logger.threaddebug("Queuing peak announcement request notification command for {action.deviceId}")
self.plugin_command_queue.put(RPFrameworkCommand(DOMOPADCOMMAND_SPEAKANNOUNCEMENTNOTIFICATION, command_payload=(device_registration_id, announcement_msg, rp_device)))
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# This routine will send the Control Page Display Command to a Android device (in
# order to request that a specific control page be shown on the device)
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def process_control_page_display_notification(self, action):
rp_device = self.managed_devices[action.deviceId]
device_registration_id = rp_device.indigoDevice.pluginProps.get("deviceRegistrationId", "")
control_page_id = int(action.props.get("controlPageId", "0"))
if device_registration_id == "":
self.logger.error(f"Unable to send control page display request notification to {rp_device.indigoDevice.deviceId}; the device is not paired.")
elif control_page_id <= 0:
self.logger.error(f"Unable to send control page display request notification to {rp_device.indigoDevice.deviceId}; no control page was selected.")
else:
self.logger.threaddebug(f"Queuing control page display request notification command for {action.deviceId}")
self.plugin_command_queue.put(RPFrameworkCommand(DOMOPADCOMMAND_CPDISPLAYNOTIFICATION, command_payload=(device_registration_id, control_page_id)))
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# This routine will send the Update Device Status request notification in order to ask
# the device to update its status immediately (instead of waiting for its normal
# 15-minute update interval)
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def request_device_status_update(self, action):
rp_device = self.managed_devices[action.deviceId]
device_registration_id = rp_device.indigoDevice.pluginProps.get("deviceRegistrationId", "")
if device_registration_id == "":
self.logger.error(f"Unable to send status update request to {rp_device.indigoDevice.deviceId}; the device is not paired.")
else:
self.logger.threaddebug(f"Queuing device status update request notification command for {action.deviceId}")
self.plugin_command_queue.put(RPFrameworkCommand(DOMOPADCOMMAND_DEVICEUPDATEREQUESTNOTIFICATION, command_payload=device_registration_id))
# endregion
#######################################################################################
#######################################################################################
# region Configuration Dialog Callback Routines
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# When called, clears the current device pairing ID, disabling push notification and
# updates from the old device
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def clear_device_pairing(self, values_dict, type_id, dev_id):
values_dict["deviceRegistrationId"] = ""
return values_dict
# endregion
#######################################################################################
#######################################################################################
# region API Action Handlers
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# API call that allows the Android client to register itself against a specific Indigo
# Android device
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def register_android_device(self, action, dev=None, caller_waiting_for_result=None):
try:
body_params = action.props["body_params"] if "body_params" in action.props else action.props["url_query_args"]
device_id = body_params.get("deviceId", "")
pairing_id = body_params.get("pairingId", "")
overwrite = int(body_params.get("allowOverwrite", 0))
if device_id == "" or pairing_id == "":
return {"status": 400, "content": "Invalid or missing parameters supplied to pairing request"}
android_dev = indigo.devices[int(device_id)]
plugin_props = android_dev.pluginProps
if plugin_props.get("deviceRegistrationId", "") == "" or overwrite == 1:
plugin_props["deviceRegistrationId"] = pairing_id
android_dev.replacePluginPropsOnServer(plugin_props)
android_dev.updateStateOnServer("isPaired", True, uiValue="Paired")
command_response = "OK"
indigo.server.log(f"Successfully paired Android device to Indigo Device {device_id}")
else:
indigo.server.log("Rejected device pairing - Indigo Device already paired to another Android device.", isError=True)
command_response = "ERROR: Exception Processing Request"
return {"status": 200, "content": command_response}
except Exception as ex:
self.logger.error("Failed to register android device via API")
return {"status": 500, "content": f"{ex}"}
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# API call that allows the Android client to de-register itself against a device
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def unregister_android_device(self, action, dev=None, caller_waiting_for_result=None):
try:
body_params = action.props["body_params"] if "body_params" in action.props else action.props["url_query_args"]
device_id = body_params.get("deviceId", "")
pairing_id = body_params.get("pairingId", "")
android_dev = indigo.devices[int(device_id)]
plugin_props = android_dev.pluginProps
if plugin_props.get("deviceRegistrationId", "") == pairing_id:
plugin_props["deviceRegistrationId"] = ""
android_dev.replacePluginPropsOnServer(plugin_props)
android_dev.updateStateOnServer("isPaired", False, uiValue="Not Paired")
command_response = "OK"
indigo.server.log(f"Successfully un-aired Android device to Indigo Device {device_id}")
else:
indigo.server.log("Rejected device un-pairing request - Indigo Device not paired to device making the request", isError=True)
command_response = "ERROR: Exception Processing Request"
return {"status": 200, "content": command_response}
except Exception as ex:
self.logger.error("Unable to de-register Android device via API")
return {"status": 500, "content": f"{ex}"}
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# API call allowing a client to update its status (battery, location, etc.)
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def update_client_status(self, action, dev=None, caller_waiting_for_result=None):
try:
body_params = action.props["body_params"] if "body_params" in action.props else action.props["url_query_args"]
pairing_id = body_params.get("pairingId", "")
device_model = body_params.get("deviceModel", "")
battery_status = body_params.get("batteryStatus", "")
battery_level = int(body_params.get("batteryLevel", "0"))
longitude = body_params.get("longitude", "")
latitude = body_params.get("latitude", "")
location_time = body_params.get("locationFixTime")
# we need to find the proper devices based upon the pairing id; the default response will be
# that the device was not found
command_response = "ERROR: Device not found"
dev_iter = indigo.devices.iter(filter="com.duncanware.domoPadMobileClient.domoPadAndroidClient")
for dev in dev_iter:
if dev.pluginProps.get("deviceRegistrationId", "") == pairing_id:
updated_states = [
{"key": "modelName", "value": device_model},
{"key": "batteryStatus", "value": battery_status},
{"key": "batteryLevel", "value": battery_level},
{"key": "longitude", "value": longitude},
{"key": "latitude", "value": latitude},
{"key": "locationFixTime", "value": location_time}
]
dev.updateStatesOnServer(updated_states)
command_response = "OK"
if command_response != "OK":
self.logger.error(f"Received status update for unknown device with Pairing ID: {pairing_id}")
return {"status": 200, "content": command_response}
except Exception as ex:
self.logger.exception("Failed to update mobile client status via API")
return {"status": 500, "content": f"Failed to update mobile client status: {ex}"}
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Called from the updated Android application in order to execute a plugin action not
# defined in the mobile XML messages
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
def execute_plugin_action(self, action, dev=None, caller_waiting_for_result=None):
try:
body_params = action.props["body_params"] if "body_params" in action.props else action.props["url_query_args"]
plugin_id = body_params.get("pluginId")
device_id = body_params.get("deviceId")
action_id = body_params.get("actionId")
action_props = body_params.get("actionProps")
command_response = "unknown"
if plugin_id == "com.duncanware.domoPadMobileClient":
indigo_plugin = indigo.activePlugin
if action_id == "sendUpdateStatusRequestNotification":
self.logger.warning(f"Should request device status update for {device_id}")
else:
self.logger.error(f"Unknown action received for domoPadMobileClient: {action_id}")
else:
indigo_plugin = indigo.server.getPlugin(plugin_id)
if indigo_plugin is None:
command_response = "ERROR: Invalid plugin specified"
elif (action_props is None) or (len(action_props) == 0):
indigo_plugin.executeAction(action_id, deviceId=int(device_id))
command_response = "OK"
else:
action_prop_dict = eval(action_props)
indigo_plugin.executeAction(action_id, deviceId=int(device_id), props=action_prop_dict)
command_response = "OK"
if caller_waiting_for_result:
return f"{'status': '{command_response}'}"
self.logger.debug(f"Received request to execute {action_id} against {device_id} via {plugin_id} with props {action_props}")
except Exception as ex:
self.logger.exception("Failed to execute plugin action from Mobile device")
return {"status": 500, "content": f"Failed to execute plugin action from Mobile: {ex}"}
# endregion
#######################################################################################