I do an hangup call with my ZigBee door sensor , if I open door manually, when I’m close, my indoor sip clients stop ringing for ex
Hello Using same scenario and I use a python script
here my automation:
alias: SIP - Hangup Call when door opened
description: ''
trigger:
- platform: state
entity_id: binary_sensor.door_window_sensor_158d00019f3070
from: 'off'
to: 'on'
condition: []
action:
- service: shell_command.hangup_doorbell
data: {}
mode: single
Here the shell command:
hangup_doorbell: 'python3 /config/python_scripts/asterisk_hang_on.py 8001'
the 8001 is the extension I want the call to quit.
the python script:
import sys
import telnetlib
import re
host = "192.168.1.248" #IP Address of Asterisk Server
port = 5038 #Port of Asterisk Server. AMI default port is 5038.
user = "hass" #username for Asterisk AMI as configured in /etc/asterisk/manager_additional.conf
password = "hass" #password for Asterisk AMI as configured in /etc/asterisk/manager_additional.conf
debug = 1 #Set default debug mode.
actionindex = 1 #ActionID index required by Asterisk AMI protocol. Always starts at 1 and increments with each command.
if len(sys.argv) == 1:
sys.exit("ERROR: No Asterisk Extension Number passed in as argument. Terminating.")
extension = sys.argv[1]
print("Trying to hangup Extension ",str(extension))
#Connect to Asterisk AMI
tn = telnetlib.Telnet(host,port)
#Wait till asterisk responds
out = tn.read_until('Asterisk Call Manager'.encode('ascii'),2)
if debug: print("RECEIVED:",out)
#Send Login Info
message = "Action: Login\nActionID: " + str(actionindex) + "\nUsername: " + user + "\nSecret: " + password + "\nEvents: off\n\n"
if debug: print("SENT:\n",message)
tn.write(message.encode('ascii'))
actionindex = actionindex + 1
#Wait till AMI responds
out = tn.read_until(b"Message: Authentication accepted",2)
if debug: print("RECEIVED:\n",out)
#Send Status Request
message = "Action: Status\nActionID: " + str(actionindex) + "\n\n"
if debug: print("SENT:\n",message)
tn.write(message.encode('ascii'))
actionindex = actionindex + 1
#Wait till AMI responds
out = tn.read_until(b"Event: StatusComplete",2)
if debug: print("RECEIVED:",out)
#Search for a PJSIP channel matching the extension we are looking for
matchtext = 'Channel: PJSIP/' + str(extension) + '-........'
match = re.search(matchtext.encode('ascii'), out)
if match:
result = match.group()
# channel = result.lstrip('Channel: ')
channel = result.lstrip(b'Channel: ')
print("Extension ",str(extension)," is active. Found Channel ",channel)
message = "Action: Hangup\nChannel: " + str(channel.decode()) + "\nActionID: " + str(actionindex) + "\n\n"
# message = "Action: Hangup\nChannel: " + channel + "\nActionID: " + str(actionindex) + "\n\n"
if debug: print("SENT:\n",message)
tn.write(message.encode('ascii'))
actionindex = actionindex + 1
out = tn.read_until(b"Hungup",2)
if debug: print("RECEIVED:",out)
else:
print("No active Asterisk Channel for Extension ",str(extension)," was found")
#Logoff
message = "Action: Logoff\nActionID: " + str(actionindex) + "\n\n"
if debug: print("SENT:\n",message)
tn.write(message.encode('ascii'))
#Wait till AMI responds
out = tn.read_until(b"fish",2)
if debug: print("RECEIVED:",out)
print("Done. Terminating.")
if you use PJSIP for your entension you want the call to quit let is like this.
if you have chan_sip extension, change this line 49
from
matchtext = 'Channel: PJSIP/' + str(extension) + '-........'
to
matchtext = 'Channel: SIP/' + str(extension) + '-........'
hop it helps
Yeah, that works great. Thank you.
Hello TECHFox. First of all thank you for the integration and also I have a question. My card works without error only if I log in with person number [100] If I log in with person number [101] or [102] I get the error person not cofigured! There is no extension configured for this person.
Where did I miss something? And the question is: the card does not work with video yet? Video call works in PostSIP mobile app but chrome only audio call. Thanks
the person not configured error is because there isn’t a (valid) extension configured in the card for that current logged in person.
The card does work with video, but the codecs of your devices can give problems. Know that android doesn’t support as many codecs as a desktop browser, so check which codecs are supported for your devices/browsers. Also make sure your webcam works in your browser.
But check the browser logs for a clue as to what the issue could be.
thanks for the answer but the card is configured with both [101] person and [102] person. And if I enter under them, then an error. Only [100] without error
Did you configure then using the card editor? Make sure the person entities are correct.
No, I set it up in yaml. Can there be an error due to the fact that the integration version is main
? The card receives information about persons from the integration ? But why then with [100] everything is OK
if you run the latest integration you can use a template of the extension sensor
service: asterisk.hangup
data:
channel: >
{{ states('sensor.8000_channel') }}
My guess is because the persons are incorrect. Try setting it with the editor so it’s configured correctly.
Btw, there is a bug with the editor that some inputs could be gone. To fix that first open another card editor so the elements are loaded.
well i will post here… the integration broke for me using freepbx. only binary sensors are published not the sensors. anyone using freepbx and have same iisue?
Fantastic! Now everything works, even the video. One problem to solve is to make it work outdoors. The card works, but neither audio nor video can be heard. Do I need to configure other ports?
You need port 8089, and the rtp ports 10000-20000.
And maybe 5060 or 5061 if it still doesn’t work.
apologies for the delay - here’s the code. You will need to download TECHFoxs card and replace the typescript
import { Ripple } from '@material/mwc-ripple';
import { RippleHandlers } from '@material/mwc-ripple/ripple-handlers';
import { UA, WebSocketInterface } from "jssip/lib/JsSIP";
import { RTCSessionEvent } from "jssip/lib/UA";
import { EndEvent, PeerConnectionEvent, IncomingEvent, OutgoingEvent, IceCandidateEvent, RTCSession, SessionStatus } from "jssip/lib/RTCSession";
import {
LitElement,
html,
css,
unsafeCSS
} from "lit";
import "./editor";
import { customElement, queryAsync } from "lit/decorators.js";
import "./audioVisualizer";
import { AudioVisualizer } from "./audioVisualizer";
enum callState {
Idle = "Idle",
Connecting = "Connecting",
Connected = "Connected"
}
@customElement('sipjs-doorbell-card')
class SipJsDoorbellCard extends LitElement {
sipPhone: UA | undefined;
sipPhoneSession: RTCSession | null;
sipCallOptions: any;
user: any;
config: any;
hass: any;
timerElement: any;
renderRoot: any;
popup: boolean = false;
currentCamera: any;
intervalId!: number;
error: any = null;
audioVisualizer: any;
constructor() {
super();
this.sipPhoneSession = null;
}
static get properties() {
return {
hass: {},
config: {}
};
}
private _shouldRenderRipple = false;
@queryAsync('mwc-ripple') private _ripple!: Promise<Ripple | null>;
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
// this._shouldRenderRipple = true;
return this._ripple;
});
private handleRippleActivate(evt?: Event): void {
this._ripple.then((r) => r && r.startPress && this._rippleHandlers.startPress(evt));
}
private handleRippleDeactivate(): void {
this._ripple.then((r) => r && r.endPress && this._rippleHandlers.endPress());
}
private handleRippleFocus(): void {
this._ripple.then((r) => r && r.startFocus && this._rippleHandlers.startFocus());
}
private handleRippleBlur(): void {
this._ripple.then((r) => r && r.endFocus && this._rippleHandlers.endFocus());
}
static get styles() {
return css `
ha-card {
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 4% 0;
font-size: 1.2rem;
height: 100%;
box-sizing: border-box;
justify-content: center;
position: relative;
overflow: hidden;
}
ha-card:focus {
outline: none;
}
ha-state-icon {
width: 40%;
height: auto;
--mdc-icon-size: 100%;
}
ha-state-icon + span {
margin-top: 8px;
}
ha-state-icon, span {
outline: none;
}
#state-icon {
color:red;
}
#state-icon.Idle {
color:green;
}
.state {
font-size: 0.9rem;
color: var(--secondary-text-color);
}
`;
}
private _callStartTime: Date;
private _callStateElement: HTMLElement;
private _callTimerInterval: NodeJS.Timer;
private _callState: callState = callState.Idle;
handleClick() {
if (this._callState == callState.Idle) {
this.call();
} else {
this.hangup();
}
}
getElapsedTime(startTime: Date) {
// Record end time
let endTime = new Date();
// Compute time difference in milliseconds
let timeDiff = endTime.getTime() - startTime.getTime();
// Convert time difference from milliseconds to seconds
timeDiff = timeDiff / 1000;
// Extract integer seconds that dont form a minute using %
let seconds = Math.floor(timeDiff % 60); //ignoring uncomplete seconds (floor)
// Pad seconds with a zero if neccessary
let secondsAsString = seconds < 10 ? "0" + seconds : seconds + "";
// Convert time difference from seconds to minutes using %
timeDiff = Math.floor(timeDiff / 60);
// Extract integer minutes that don't form an hour using %
let minutes = timeDiff % 60; //no need to floor possible incomplete minutes, becase they've been handled as seconds
// Pad minutes with a zero if neccessary
let minutesAsString = minutes < 10 ? "0" + minutes : minutes + "";
// Convert time difference from minutes to hours
timeDiff = Math.floor(timeDiff / 60);
// Extract integer hours that don't form a day using %
let hours = timeDiff % 24; //no need to floor possible incomplete hours, becase they've been handled as seconds
// Convert time difference from hours to days
timeDiff = Math.floor(timeDiff / 24);
// The rest of timeDiff is number of days
let days = timeDiff;
let totalHours = hours + (days * 24); // add days to hours
let totalHoursAsString = totalHours < 10 ? "0" + totalHours : totalHours + "";
if (totalHoursAsString === "00") {
return minutesAsString + ":" + secondsAsString;
} else {
return totalHoursAsString + ":" + minutesAsString + ":" + secondsAsString;
}
}
private _connectingCount = 0;
private _cardElement: HTMLElement;
private _stateIconElement: HTMLElement;
private _actionElement: HTMLElement;
private _extension: any;
updateCss() {
this._stateIconElement.classList.remove(callState.Idle);
this._stateIconElement.classList.remove(callState.Connected);
this._stateIconElement.classList.remove(callState.Connecting);
this._stateIconElement.classList.add(this._callState.toString());
}
call() {
this.debug("Calling " + this.config.doorbell_extension);
this._callState = callState.Connecting;
if (this._callStateElement == null) {
this._callStateElement = this.renderRoot.querySelector("#state");
}
if (this._cardElement == null) {
this._cardElement = this.renderRoot.querySelector("ha-card");
}
if (this._stateIconElement == null) {
this._stateIconElement = this.renderRoot.querySelector('#state-icon');
}
if (this._actionElement == null) {
this._actionElement = this.renderRoot.querySelector('#action');
}
this._stateIconElement.setAttribute("icon","hass:phone-hangup");
this._actionElement.innerHTML = "Hangup";
this.updateCss();
this._callTimerInterval = setInterval(() => {
this._connectingCount++;
if (this._connectingCount == 4) {
this._connectingCount = 1;
}
this._callStateElement.innerHTML = "Connecting" + " ".padEnd(this._connectingCount + 1, ".");
}
,300);
if (this.sipPhone) {
this.debug("Calling sip: " + this.config.doorbell_extension + "@" + this.config.server);
this.debug("Options: " + this.sipCallOptions);
this.sipPhone.call("sip:" + this.config.doorbell_extension + "@" + this.config.server, this.sipCallOptions);
} else {
this.debug("No phone defined");
}
}
callConnected() {
this._callState = callState.Connected;
this.updateCss();
this._callStartTime = new Date();
clearInterval(this._callTimerInterval);
this._callStateElement.innerText = "00:00";
this._callTimerInterval = setInterval(() => {
this._callStateElement.innerText = this.getElapsedTime(this._callStartTime);
},1000);
}
hangup() {
try {
if (this.sipPhoneSession?.status != 8) {
this.sipPhoneSession?.terminate();
}
} catch (error) {
console.log("Error terminating call: ");
console.log(error);
}
this.sipPhoneSession = null;
this._callState = callState.Idle;
clearInterval(this._callTimerInterval);
this.updateCss();
this._stateIconElement.setAttribute("icon","hass:phone");
this._actionElement.innerHTML = "Call";
this._callStateElement.innerHTML = " "
}
firstUpdated() {
this.connectSesson();
}
async connectSesson() {
if (this._extension == null) {
this._extension = this.config.extensions.find((ext: any) => {
if (this.hass.user.id == this.hass.states[ext.person].attributes.user_id) {
this.debug("Using extension settings : " + JSON.stringify(ext));
return true;
}
});
}
if (this._extension == null) {
this.debug("cannot find extension for this user");
}
var uri = "wss://" + this.config.server + ":" + this.config.port + "/ws";
this.debug("Opening session to sip server :" + uri);
var socket = new WebSocketInterface(uri);
var configuration = {
sockets : [ socket ],
uri : "sip:" + this._extension.extension + "@" + this.config.server,
authorization_user: this._extension.extension,
password: this._extension.secret,
register: true
};
this.debug("Opening session to sip server :" );
this.debug(JSON.stringify(configuration));
this.sipPhone = new UA(configuration);
this.sipCallOptions = {
mediaConstraints: { audio: true, video: false }
};
this.sipPhone?.start();
this.sipPhone?.on("registered", () => console.log('SIP-Card Registered with SIP Server'));
this.sipPhone?.on("unregistered", () => console.log('SIP-Card Unregistered with SIP Server'));
this.sipPhone?.on("registrationFailed", () => console.log('SIP-Card Failed Registeration with SIP Server'));
this.sipPhone?.on("newRTCSession", (event: RTCSessionEvent) => {
if (this.sipPhoneSession !== null ) {
event.session.terminate();
return;
}
console.log('Call: newRTCSession: Originator: ' + event.originator);
this.sipPhoneSession = event.session;
this.sipPhoneSession.on('getusermediafailed', (DOMError) => {
this.debug('getUserMedia() failed: ' + DOMError);
});
this.sipPhoneSession.on('peerconnection:createofferfailed', (DOMError) => {
this.debug('createOffer() failed: ' + DOMError);
});
this.sipPhoneSession.on('peerconnection:createanswerfailed', (DOMError) => {
this.debug('createAnswer() failed: ' + DOMError);
});
this.sipPhoneSession.on('peerconnection:setlocaldescriptionfailed', (DOMError) => {
this.debug('setLocalDescription() failed: ' + DOMError);
});
this.sipPhoneSession.on('peerconnection:setremotedescriptionfailed', (DOMError) => {
this.debug('setRemoteDescription() failed: ' + DOMError);
});
this.sipPhoneSession.on("confirmed", (event: IncomingEvent | OutgoingEvent) => {
this.debug('Call confirmed. Originator: ' + event.originator);
});
this.sipPhoneSession.on("failed", (event: EndEvent) =>{
this.debug('Call failed. Originator: ' + event.originator);
this.hangup();
});
this.sipPhoneSession.on("ended", (event: EndEvent) => {
this.debug('Call ended. Originator: ' + event.originator);
this.hangup();
});
this.sipPhoneSession.on("accepted", (event: IncomingEvent | OutgoingEvent) => {
this.debug('Call accepted. Originator: ' + event.originator);
this.callConnected();
});
if (this.sipPhoneSession.direction === 'outgoing') {
//Note: peerconnection seems to never fire for outgoing calls
this.sipPhoneSession.on("peerconnection", (event: PeerConnectionEvent) => {
this.debug('Call: peerconnection(outgoing)');
this.callConnected();
});
this.sipPhoneSession?.connection.addEventListener("track", (event: RTCTrackEvent): void => {
this.debug('Call: mediatrack event: kind: ' + event.track.kind);
if (event.track.kind == "audio") {
let remoteAudio = this.renderRoot.querySelector("#remoteAudio");
remoteAudio.srcObject = event.streams[0];
remoteAudio.play();
}
});
}
else {
this.debug("Unsupported - incoming calls");
}
});
}
private _debug = true;
debug(message: string) {
if (this._debug) {
console.log(message);
}
}
render() {
return html`<ha-card role="button"
@click=${this.handleClick}
@focus=${this.handleRippleFocus}
@blur=${this.handleRippleBlur}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
class="Idle"
>
<ha-state-icon id="state-icon"
tabindex="-1"
icon="hass:phone"
class="Idle"
></ha-state-icon>
<audio id="remoteAudio" style="display:none"></audio>
<span tabindex="-1" title="Call" id="action">Call</span>
<span tabindex="-1" id="state" class="state"> </span>
<mwc-ripple></mwc-ripple>
</ha-card>
`;
}
setConfig(config: { server: any; port: any; extensions: any; }): void {
if (!config.server) {
throw new Error("You need to define a server!");
}
if (!config.port) {
throw new Error("You need to define a port!");
}
if (!config.extensions) {
throw new Error("You need to define at least one extension!");
}
this.config = config;
}
getCardSize() {
return 1;
}
}
(window as any).customCards = (window as any).customCards || [];
(window as any).customCards.push({
type: "sipjs-doorbell-card",
name: "SIP Doorbell Card",
preview: false,
description: "A SIP doorbell card."
});
@Roflco Thank you so much. Sorry to ask the dumb questions, but where in Hassio do I find the card? Is it the /config/www/asterisk/sipjs-card.js?
If you want to modify the card, you will need to clone the repo and build the card. More info on that here.
Thank you again. Sorry my computer skills are limited. I am running Hassio, how do I run the commands with Hassio?
You can do it on your Windows or Linux device. Just make sure you have npm installed.
Btw, there is a discord server where you can ask questions. You get a much faster response there.
Here is the invite