SIP client card, as intercom

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 :slight_smile:

1 Like

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.")
2 Likes

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 :slight_smile:

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

@Roflco Wow you modified card sounds exactly what I need. Are you able to show me the code please?

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') }}
1 Like

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 = "&nbsp;"
    }


    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">&nbsp;</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."
});

1 Like

@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