-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathblue-iris-poly.py
394 lines (337 loc) · 16.8 KB
/
blue-iris-poly.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
390
391
392
393
394
#!/usr/bin/env python3
"""
This is a NodeServer for Blue Iris written by fahrer16 (Brian Feeney)
based on the template for Polyglot v2 written in Python2/3 by Einstein.42 (James Milne) [email protected]
Blue Iris json functionality based on 'blueiriscmd' project by magapp (https://github.com/magapp/blueiriscmd)
"""
import udi_interface
import requests, json, hashlib
import sys
LOGGER = udi_interface.LOGGER
SERVERDATA = json.load(open('server.json'))
VERSION = SERVERDATA['credits'][0]['version']
class Controller(udi_interface.Node):
def __init__(self, polyglot, primary, address, name):
super().__init__(polyglot, primary, address, name)
self.poly = polyglot
self.name = 'Blue Iris'
self.initialized = False
self.tries = 0
self.url = ''
self.session = None
polyglot.subscribe(polyglot.START, self.start, address)
polyglot.subscribe(polyglot.CUSTOMPARAMS, self.parameterHandler)
polyglot.subscribe(polyglot.POLL, self.shortPoll)
polyglot.ready()
polyglot.addNode(self, conn_status="ST")
def parameterHandler(self, params):
self.poly.Notices.clear()
try:
if 'host' in params:
self.host = params['host']
else:
self.host = ""
if 'user' in params:
self.user = params['user']
else:
self.user = ""
if 'password' in params:
self.password = params['password']
else:
self.password = ""
if self.host == "" or self.user == "" or self.password == "":
LOGGER.error('Blue Iris requires \'host\', \'user\', and \'password\' parameters to be specified in custom configuration.')
self.poly.Notices['cfg'] = 'Please enter host, user and password.'
return False
else:
if self.connect():
self.discover()
except Exception as ex:
LOGGER.error('Error starting Blue Iris NodeServer: %s', str(ex))
def start(self):
self.poly.updateProfile()
self.poly.setCustomParamsDoc()
LOGGER.info('Started Blue Iris NodeServer for v3 NodeServer version %s', str(VERSION))
def connect(self):
try:
LOGGER.info('Connecting to Blue Iris host %s', str(self.host))
self.url = "http://" + self.host + "/json"
#Retrieve session ID from Blue Iris Server
r = requests.post(self.url, data=json.dumps({"cmd":"login"}))
if r.status_code != 200:
LOGGER.error('Error establishing connection to Blue Iris, status code: %s', str(r.status.code))
return False
self.session = r.json()["session"]
#Log into Blue Iris Server using username, password, and session ID obtained earlier.
_data = ("%s:%s:%s" % (self.user, self.session, self.password)).encode('utf-8')
loginHash = hashlib.md5(_data).hexdigest()
r = requests.post(self.url, data=json.dumps({"cmd":"login", "session": self.session, "response": loginHash}))
LOGGER.debug("Initializing Blue Iris Connection, session: %s", str(self.session))
if r.status_code != 200 or r.json()["result"] != "success":
LOGGER.error('Error logging into Blue Iris Server: %s', str(r.text))
return False
#Get System Name:
self.system_name = r.json()["data"]["system name"]
LOGGER.info('Connected to %s', str(self.system_name))
self.fillPanels()
return True
except Exception as ex:
LOGGER.error('Error connecting to Blue Iris Server, host: %s. %s', str(self.host), str(ex))
return False
# if the connection failed, try and reconnect
def reconnect(self):
while not self.connect():
time.sleep(30) # wait 30 seconds before trying again
LOGGER.info('Failed to re-connect to Blue Iris server, will retry.')
# do we need to do a discovery each time?
self.initialized = True
return True
def fillPanels(self):
for node in self.poly.nodes():
node.reportDrivers()
def shortPoll(self, pollflag):
if not self.initialized: return False #ensure discovery is completed before polling
if 'longPoll' in pollflag:
return False
try:
# if this fails, i.e. returns False, then it may mean that
# we don't have a connection
self.cameras = self.cmd("camlist")
if not self.cameras:
LOGGER.error('We are not connected to server, re-connect')
self.setDriver('GV1',3)
self.initialized = False
self.reconnect()
else:
# We're good, so query each camera
for node in self.poly.nodes():
node.query()
except Exception as ex:
# error trying to query server, mark as disconnect and re-connect
LOGGER.error('Error processing shortPoll for %s: %s', self.name, str(ex))
self.setDriver('GV1',3)
self.initialized = False
self.reconnect()
def query(self, command=None):
try:
_status = self.cmd("status")
self.setDriver('GV1',_status["signal"])
self.setDriver('GV2',_status["profile"])
except Exception as ex:
LOGGER.error('Error querying Blue Iris %s', self.name)
self.setDriver('GV1',3) #If there was an error querying the server, set the status to "Disconnected" so that the ISY can trigger an appropriate action (after a time delay set in ISY program).
self.initialized = False
self.reconnect()
def discover(self, *args, **kwargs):
LOGGER.debug('Beginning Discovery on %s', str(self.name))
try:
self.cameras = self.cmd("camlist")
for cam in self.cameras:
if 'ptz' in cam: #If there is not a 'ptz' element for this item, it's not a camera
_shortName = cam['optionValue']
_address = _shortName.lower() #ISY address must be lower case but Blue Iris requires the name to be passed in the same case as it is defined so both are needed
_name = cam['optionDisplay']
if not self.poly.getNode(_address) and _name[0] != '+': #if the name starts with a '+', it's not a camera
if cam['ptz']:
self.poly.addNode(camNodePTZ(self.poly, self.address, _address, _name, _shortName))
else:
self.poly.addNode(camNode(self.poly, self.address, _address, _name, _shortName))
self.initialized = True
return True
except Exception as ex:
LOGGER.error('Error discovering cameras on Blue-Iris: %s', str(ex))
return False
def delete(self):
LOGGER.info('Deleting Blue Iris controller')
# returns:
# true or false
# _response['data']
# _response
#
def cmd(self, cmd, params=dict()):
try:
#LOGGER.debug('Sending command to Blue Iris, cmd: %s, params: %s', str(cmd), str(params))
args = {"session": self.session, "cmd": cmd} #v1.3.0: Removed "Response" from this. It never needed to be here but v5 seems to have a problem with it.
args.update(params)
r = requests.post(self.url, data=json.dumps(args))
# if r.status_code != 200 or r.json()["result"] != "success":
if r.status_code != 200:
LOGGER.error('Error sending command to Blue Iris, status code: %s: %s', str(r.status_code), str(r.text))
_response = r.json()
#LOGGER.debug('Blue Iris Command Response: %s', str(_response))
if 'data' in _response:
self.tries = 0
return _response["data"]
elif 'session' in _response and 'result' in _response:
if _response['result'] == 'fail' and self.tries <= 2:
self.tries += 1
if self.connect():
return self.cmd(cmd, params) #retry this command after re-connecting
else:
self.tries = 0
return True
elif 'profile' in _response and 'signal' in _response:
self.tries = 0
return _response
else:
self.tries = 0
return True
except Exception as ex:
LOGGER.error('Error sending command to Blue Iris: %s', str(ex))
return False
def setState(self, command = None):
try:
LOGGER.info('Command received to set Blue Iris Server State: %s', str(command))
_state = int(command.get('value'))
if _state >=0 and _state <=2:
self.cmd("status",{"signal":_state})
return True
else:
LOGGER.error('Commanded state must be between 0 and 2 but received %i', _state)
return False
except Exception as ex:
LOGGER.error('Error setting state of Blue Iris Server: %s', str(ex))
return False
def setProfile(self, command = None):
try:
LOGGER.info('Command received to set Blue iris Profile: %s', str(command))
_profile = int(command.get('value'))
if _profile >= 0 and _profile <= 7:
self.cmd("status",{"profile":_profile})
return True
else:
LOGGER.error('Commanded profile must be between 0 and 7 but received %i', _profile)
return False
except Exception as ex:
LOGGER.error('Error setting profile of Blue Iris Server: %s', str(ex))
return False
id = 'controller'
commands = {'DISCOVER': discover, 'SET_STATE': setState, 'SET_PROFILE': setProfile}
drivers = [{'driver': 'ST', 'value': 1, 'uom': 2}, #Polyglot connection status
{'driver': 'GV1', 'value': 0, 'uom': 25}, #Blue Iris Server Status (0=red, 1=green, 2=yellow, 3=disconnected)
{'driver': 'GV2', 'value':0, 'uom': 56} #Blue Iris Profile
]
class camNode(udi_interface.Node):
def __init__(self, controller, primary, address, name, shortName):
super().__init__(controller, primary, address, name)
self.shortName = shortName
self.parent = controller.getNode(primary)
controller.subscribe(controller.START, self.start, address)
def start(self):
self.query()
def trigger(self, command):
LOGGER.info('Triggering camera: %s', str(self.name))
self.parent.cmd("trigger", {"camera": self.shortName})
#TODO: Admin access required for this command, add check that it completed successfully
def pause(self, command):
LOGGER.info('Pausing camera: %s', str(self.name))
self.parent.cmd("camconfig", {"camera": self.shortName, "pause":-1})
def unpause(self, command):
LOGGER.info('Un-Pausing camera: %s', str(self.name))
self.parent.cmd("camconfig", {"camera": self.shortName, "pause":0})
def enable(self, command):
LOGGER.info('Enable camera: %s', str(self.name))
self.parent.cmd("camconfig", {"camera": self.shortName, "enable": True})
def disable(self, command):
LOGGER.info('disabling camera: %s', str(self.name))
self.parent.cmd("camconfig", {"camera": self.shortName, "enable": False})
def query(self, command=None):
try:
for cam in self.parent.cameras:
if cam['optionValue'] == self.shortName:
_cam = cam
break
except Exception as ex:
LOGGER.error('Error querying %s: %s', self.address, str(ex))
return False
try:
self.setDriver('ST', int(_cam["isTriggered"]))
self.setDriver('GV1', int(_cam["isEnabled"]))
_connected = _cam["isOnline"] and not _cam["isNoSignal"]
self.setDriver('GV2', int(_connected))
self.setDriver('GV3', int(_cam["isMotion"]))
self.setDriver('GV4', int(_cam["isAlerting"]))
self.setDriver('GV5', int(_cam["isPaused"]))
self.setDriver('GV6', int(_cam["isRecording"]))
self.setDriver('GV7', _cam["profile"])
except Exception as ex:
LOGGER.error('Error querying %s: %s', self.name, str(ex))
return False
def ptz(self, command=None):
try:
if command is None:
LOGGER.error('No command passed for PTZ on camera: %s', self.address)
return False
LOGGER.debug('Processing PTZ command for camera %s: %s', self.name, str(command))
_cmd = command.get('cmd').lower()
_value = int(command.get('value'))
if _cmd =='ptz':
LOGGER.info('Processing PTZ command for camera %s', self.name)
self.parent.cmd("ptz",{"camera":self.shortName, "button":_value})
elif _cmd == 'position':
LOGGER.info('Processing Position command for camera %s', self.name)
self.parent.cmd("ptz",{"camera":self.shortName, "button":(_value + 100)})
elif _cmd == 'ir':
LOGGER.info('Processing IR command for camera %s', self.name)
self.parent.cmd("ptz",{"camera":self.shortName, "button":(_value + 34)})
except Exception as ex:
LOGGER.error('Error processing PTZ command for %s: %s', self.name, str(ex))
drivers = [{'driver': 'ST', 'value': 0, 'uom': 2}, #Triggered, true or false
{'driver': 'GV1', 'value':0, 'uom': 2}, #Enabled, true or false
{'driver': 'GV2', 'value':0, 'uom': 2}, #Connected (online and signal present), true or false
{'driver': 'GV3', 'value':0, 'uom': 2}, #Motion Detected, true or false
{'driver': 'GV4', 'value':0, 'uom': 2}, #Alert Active, true or false
{'driver': 'GV5', 'value':0, 'uom': 2}, #Paused, true or false
{'driver': 'GV6', 'value':0, 'uom': 2}, #Recording, true or false
{'driver': 'GV7', 'value':0, 'uom': 56} #Profile number
]
id = 'camNode'
commands = {
'DON': trigger, 'PAUSE': pause, 'CONTINUE': unpause,
'ENABLE': enable, 'DISABLE': disable,
'IR':ptz
}
class camNodePTZ(camNode):
def __init__(self, controller, primary, address, name, shortName):
super().__init__(controller, primary, address, name, shortName)
self.parent = controller.getNode(primary)
controller.subscribe(controller.START, self.start, address)
def start(self):
super().start()
def trigger(self, command):
super().trigger(command)
def pause(self, command):
super().pause(command)
def unpause(self, command):
super().unpause(command)
def enable(self, command):
super().enable(command)
def disable(self, command):
super().disable(command)
def query(self, command=None):
super().query(command)
def ptz(self, command=None):
super().ptz(command)
drivers = [{'driver': 'ST', 'value':0, 'uom': 25}, #Triggered, true or false
{'driver': 'GV1', 'value':0, 'uom': 2}, #Enabled, true or false
{'driver': 'GV2', 'value':0, 'uom': 2}, #Connected (online and signal present), true or false
{'driver': 'GV3', 'value':0, 'uom': 2}, #Motion Detected, true or false
{'driver': 'GV4', 'value':0, 'uom': 2}, #Alert Active, true or false
{'driver': 'GV5', 'value':0, 'uom': 2}, #Paused, true or false
{'driver': 'GV6', 'value':0, 'uom': 2}, #Recording, true or false
{'driver': 'GV7', 'value':0, 'uom': 56} #Profile number
]
id = 'camNodePTZ'
commands = {
'DON': trigger, 'PAUSE': pause, 'CONTINUE': unpause,
'ENABLE': enable, 'DISABLE': disable, 'PTZ':ptz,
'IR':ptz, 'POSITION':ptz
}
if __name__ == "__main__":
try:
polyglot = udi_interface.Interface([])
polyglot.start('2.0.5')
Controller(polyglot, 'controller', 'controller', 'BlueIrisNodeServer')
polyglot.runForever()
except (KeyboardInterrupt, SystemExit):
sys.exit(0)