-
Notifications
You must be signed in to change notification settings - Fork 9
/
doorvivint-card.js
executable file
·345 lines (296 loc) · 15 KB
/
doorvivint-card.js
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
class DoorVivintCard extends HTMLElement {
// Version number is contained in the console.info() below.
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
set hass(hass) {
if(this.notYetInitialized()) {
this.initJsSIPIfNecessary(hass);
this.initCameraView(hass);
} else if(this.cameraEntityHasNewAccessToken(hass)) {
this.updateCameraView(hass);
}
}
setConfig(config) {
if (!config.camera_entity) {
throw new Error('You need to define a camera entity');
}
if(!config.sip_settings) {
throw new Error('You need to define the SIP settings');
} else {
if(!config.sip_settings.sip_wss_url) throw new Error('You need to define the SIP Secure Webservice url');
if(!config.sip_settings.sip_server) throw new Error('You need to define the SIP Server (ip or hostname)');
if(!config.sip_settings.sip_username) throw new Error('You need to define the SIP username');
if(!config.sip_settings.sip_password) throw new Error('You need to define the SIP password');
}
this.config = config;
const root = this.shadowRoot;
if (root.lastChild) root.removeChild(root.lastChild);
const card = document.createElement('ha-card');
const content = document.createElement('div');
const style = document.createElement('style');
style.textContent = `
ha-card {
/* sample css */
}
.button {
overflow: auto;
padding: 16px;
text-align: right;
}
#cameraview img{
object-fit: cover;
height: 400px;
}
mwc-button {
margin-right: 16px;
}
`;
content.innerHTML = `
<div id='cameraview'>
<p style="padding: 16px">Initializing SIP connection and webcam view</p>
<audio id='audio-player'></audio>
</div>
<div class='button'>
<!--<mwc-button raised id='btn-open-door'>` + 'Open the portal' + `</mwc-button> -->
<mwc-button raised id='btn-make-call'>` + 'Call the Doorbell' + `</mwc-button>
<mwc-button style='display:none' raised id='btn-accept-call'>` + 'Accept call' + `</mwc-button>
<mwc-button style='display:none' raised id='btn-reject-call'>` + 'Reject call' + `</mwc-button>
<mwc-button style='display:none' raised id='btn-end-call'>` + 'Terminate call' + `</mwc-button>
</div>
`;
card.appendChild(content);
card.appendChild(style);
root.appendChild(card);
}
// The height of your card. Home Assistant uses this to automatically
// distribute all cards over the available columns.
getCardSize() {
return 1;
}
notYetInitialized() {
return window.JsSIP && !this.sipPhone && this.config;
}
initJsSIPIfNecessary(hass) {
let droidCard = this;
console.info("%c DoorVivint-Card \n%c Version 0.1.0 ","color: orange; font-weight: bold; background: black","color: white; font-weight: bold; background: dimgray");
//If you want to add another button to perform some other action, can use the following:
// let openDoorBtn = droidCard.getElementById('btn-open-door');
// openDoorBtn.addEventListener('click', function(opendoor) {
// hass.callService('input_boolean', 'turn_on', { entity_id: 'input_boolean.door' });
// });
// Audio
// Local audio stream (input from mic, output to speaker) is handled
// by getUserMedia which is called under the covers by JSSIP.
// getUserMedia checks that the user has
// given permission for this code to access the mic/speaker.
// Remote:
// The html tag and element <audio> represents the peer's audio stream to listen to.
const remoteAudio = document.createElement('audio');
let sip_doorbell_username = this.config.sip_settings.sip_doorbell_username;
let sip_doorbell_domain = this.config.sip_settings.sip_doorbell_domain;
console.log('Loading SIPPhone');
let socket = new JsSIP.WebSocketInterface(this.config.sip_settings.sip_wss_url);
let configuration = {
sockets : [ socket ],
uri : `sip:${this.config.sip_settings.sip_username}@${this.config.sip_settings.sip_server}`,
password : this.config.sip_settings.sip_password
};
//Create a new SIP User Agent, and start it (connect to SIP server, Register, etc.)
this.sipPhone = new JsSIP.UA(configuration);
this.sipPhone.start();
// Register callbacks for outgoing call events.
// It appears the following eventHandlers are JsSIP.RTCSession Events which can
// also be registered during Session Outgoing Events, which is what I'll do for now.
// var eventHandlers = {
// 'progress': function(e) {
// console.log('call is in progress');
// },
// 'failed': function(e) {
// console.log('call failed with cause: '+ e.cause);
// },
// 'ended': function(e) {
// console.log('call ended with cause: '+ e.cause);
// },
// 'confirmed': function(e) {
// console.log('call confirmed');
// },
// };
let callOptions = {
// 'eventHandlers' : eventHandlers,
'mediaConstraints': { 'audio': true, 'video': false } // only audio calls
};
//Register callbacks to tell us SIP Registration events
this.sipPhone.on("registered", () => console.log('SIPPhone Registered with SIP Server'));
this.sipPhone.on("unregistered", () => console.log('SIPPhone Unregistered with SIP Server'));
this.sipPhone.on("registrationFailed", () => console.log('SIPPhone Failed Registeration with SIP Server'));
//Register a callback when a new WebRTC media session is established
// which occurs on incoming or outgoing calls.
this.sipPhone.on("newRTCSession", function(data){
let session = data.session;
if (session.direction === "incoming") {
console.log('Session - Incoming call from ' + session.remote_identity );
//If you want to perform an action on incoming call, can use the following:
// hass.callService('input_boolean', 'turn_on', { entity_id: 'input_boolean.gds_ringing' });
let acceptCallBtn = droidCard.getElementById('btn-accept-call');
let rejectCallBtn = droidCard.getElementById('btn-reject-call');
let endCallBtn = droidCard.getElementById('btn-end-call');
let makeCallBtn = droidCard.getElementById('btn-make-call');
makeCallBtn.style.display = 'none';
acceptCallBtn.style.display = 'inline-flex';
rejectCallBtn.style.display = 'inline-flex';
//Register for various incoming call session events
session.on("accepted", () => {
console.log('Incoming - call accepted');
acceptCallBtn.style.display = 'none';
rejectCallBtn.style.display = 'none';
endCallBtn.style.display = 'inline-flex';
});
session.on("confirmed", () => console.log('Incoming - call confirmed'));
session.on("ended", () => {console.log('Incoming - call ended'); droidCard.cleanup(hass)});
session.on("failed", () =>{console.log('Incoming - call failed'); droidCard.cleanup(hass)});
session.on("peerconnection", () => {
session.connection.addEventListener("addstream", (e) => {
console.log('Incoming - adding audiostream')
remoteAudio.srcObject = e.stream;
remoteAudio.play();
})
});
acceptCallBtn.addEventListener('click', () => {
session.answer(callOptions);
//If you want to perform an action on accepting an incoming call, can use the following:
// hass.callService('input_boolean', 'turn_off', { entity_id: 'input_boolean.gds_ringing' });
});
endCallBtn.addEventListener('click', () => session.terminate());
rejectCallBtn.addEventListener('click', () => {
//If you want to perform an action on rejecting an incoming call, can use the following:
// hass.callService('input_boolean', 'turn_off', { entity_id: 'input_boolean.gds_ringing' });
session.answer(callOptions);
setTimeout(() => {
session.terminate();
}, 1000);
});
acceptCallBtn.style.display = 'inline-flex';
rejectCallBtn.style.display = 'inline-flex';
}
if (session.direction === "outgoing") {
console.log('Session - Outgoing Call Event')
let endCallBtn = droidCard.getElementById('btn-end-call');
let makeCallBtn = droidCard.getElementById('btn-make-call');
makeCallBtn.style.display = 'none';
endCallBtn.style.display = 'inline-flex';
endCallBtn.addEventListener('click', () => session.terminate());
//Register for various call session events:
session.on('progress', function(e) {
console.log('Outgoing - call is in progress');
});
session.on('failed', function(e) {
console.log('Outgoing - call failed with cause: '+ e.cause);
if (e.cause === JsSIP.C.causes.SIP_FAILURE_CODE) {
console.log(' Called party may not be reachable');
};
droidCard.cleanup(hass);
});
session.on('confirmed', function(e) {
console.log('Outgoing - call confirmed');
});
session.on('ended', function(e) {
console.log('Outgoing - call ended with cause: '+ e.cause);
droidCard.cleanup(hass);
});
//Note: peerconnection never fires for outoing, but I'll leave it here anyway.
session.on('peerconnection', () => console.log('Outgoing - Peer Connection'));
//Note: 'connection' is the RTCPeerConnection instance - set after calling ua.call().
// From this, use a WebRTC API for registering event handlers.
//Note: Was not able to get session.connection.ontrack = function(e) to work
session.connection.onaddstream = function(e) {
console.log('Outoing - addstream');
remoteAudio.srcObject = e.stream;
remoteAudio.play();
};
//Handle Browser not allowing access to mic and speaker
session.on("getusermediafailed", function(DOMError) {
console.log('Get User Media Failed Call Event ' + DOMError )
});
}
});
let MakeCallBtn = droidCard.getElementById('btn-make-call');
MakeCallBtn.addEventListener('click', () => {
//console.log('Making Call...');
console.log('Calling '+`sip:${sip_doorbell_username}@${sip_doorbell_domain}`);
//this.sipPhone.call('sip:4001@192.168.0.11', callOptions);
//this.sipPhone.call('sip:2001@192.168.0.31', callOptions);
this.sipPhone.call(`sip:${sip_doorbell_username}@${sip_doorbell_domain}`, callOptions);
});
}
initCameraView(hass) {
this.cameraViewerShownTimeout = window.setTimeout(() => this.isDoorVivintNotShown() , 15000);
const cameraView = this.getElementById('cameraview');
const imgEl = document.createElement('img');
const camera_entity = this.config.camera_entity;
this.access_token = hass.states[camera_entity].attributes['access_token'];
imgEl.src = `/api/camera_proxy_stream/${camera_entity}?token=${this.access_token}`;
imgEl.style.width = '100%';
while (cameraView.firstChild) {
cameraView.removeChild(cameraView.firstChild);
}
cameraView.appendChild(imgEl);
console.log('initialized camera view');
}
updateCameraView(hass) {
const imgEl = this.shadowRoot.querySelector('#cameraview img');
const camera_entity = this.config.camera_entity;
this.access_token = hass.states[camera_entity].attributes['access_token'];
imgEl.src = `/api/camera_proxy_stream/${camera_entity}?token=${this.access_token}`;
}
cameraEntityHasNewAccessToken(hass) {
clearTimeout(this.cameraViewerShownTimeout);
this.cameraViewerShownTimeout = window.setTimeout(() => this.isDoorVivintNotShown() , 15000);
if(!this.sipPhone) return false;
const old_access_token = this.access_token;
const new_access_token = hass.states[this.config.camera_entity].attributes['access_token'];
return old_access_token !== new_access_token;
}
isDoorVivintNotShown() {
const imgEl = this.shadowRoot.querySelector('#cameraview img');
if(!this.isVisible(imgEl)) {
this.stopCameraStreaming();
}
}
stopCameraStreaming() {
console.log('Stopping camera stream...');
const imgEl = this.shadowRoot.querySelector('#cameraview img');
imgEl.src = '';
this.access_token = undefined;
}
isVisible(el) {
if (!el.offsetParent && el.offsetWidth === 0 && el.offsetHeight === 0) {
return false;
}
return true;
}
cleanup(hass) {
let acceptCallBtn = this.getElementById('btn-accept-call');
let rejectCallBtn = this.getElementById('btn-reject-call');
let endCallBtn = this.getElementById('btn-end-call');
let makeCallBtn = this.getElementById('btn-make-call');
//acceptCallBtn remove eventlisteners and hide
let clonedAcceptCallBtn = acceptCallBtn.cloneNode(true)
clonedAcceptCallBtn.style.display = 'none';
acceptCallBtn.parentNode.replaceChild(clonedAcceptCallBtn, acceptCallBtn);
//rejectCallBtn remove eventlisteners and hide
let clonedRejectCallBtn = rejectCallBtn.cloneNode(true)
clonedRejectCallBtn.style.display = 'none';
rejectCallBtn.parentNode.replaceChild(clonedRejectCallBtn, rejectCallBtn);
//endCallBtn remove eventlisteners and hide
let clonedEndCallBtn = endCallBtn.cloneNode(true)
clonedEndCallBtn.style.display = 'none';
endCallBtn.parentNode.replaceChild(clonedEndCallBtn, endCallBtn);
makeCallBtn.style.display = 'inline-flex';
}
getElementById(id) {
return this.shadowRoot.querySelector(`#${id}`);
}
}
customElements.define('doorvivint-card', DoorVivintCard);