Asterisk in Home Assistant – Part 2

The Asterisk component

After looking at how other platforms were set up in Home Assistant(HA), I ended up with the conclusion that I want to configure my component like this in configuration.yaml:

asterisk:
  host: 192.168.xxx.xxx
  port: 5038
  username: admin
  password: xxxxxxxxxxxxxxxxxxxxxxxxxx

 

I now created a file: custom_components/asterisk.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
import asterisk.manager
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv

from homeassistant.const import CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD
import homeassistant.loader as loader

_LOGGER = logging.getLogger(__name__)

REQUIREMENTS = ['pyst2==0.5.0']

NOTIFICATION_ID = 'asterisk_setup'
NOTIFICATION_TITLE = 'Asterisk Setup'

DOMAIN = 'asterisk'

CONFIG_SCHEMA = vol.Schema({
    DOMAIN: vol.Schema({
        vol.Required(CONF_HOST): cv.string,
        vol.Required(CONF_USERNAME): cv.string,
        vol.Required(CONF_PASSWORD): cv.string,
        vol.Optional(CONF_PORT, default=5038): cv.positive_int,
    }),
}, extra=vol.ALLOW_EXTRA)


def setup(hass, config):
    conf = config[DOMAIN]
    host = conf.get(CONF_HOST)
    port = conf.get(CONF_PORT)
    username = conf.get(CONF_USERNAME)
    password = conf.get(CONF_PASSWORD)
    _LOGGER.info("Asterisk component is now set up")

    astmanager = asterisk.manager.Manager()
    try:
        astmanager.connect(host, port)
        astmanager.login(username, password)
        hass.data['asterisk_manager'] = astmanager # this object is used by astext. Pass it on
        return True
    except:
        _LOGGER.error("Could not connect to asterisk server")
        return False

 

One of the most important lines is line 9 where I define the logger that i relied heavily on in my debugging process! It can be used to log different log levels. It is for example used in line 34. Under my development process I logged to .error to make it easy to spot in HA logs.

In line 18 to 25 I define how the config parameters (from configuration.yaml) should be. This information tells HA how to validate the config fields. If these fields are not correctly provided, HA will fail loading this component.

In line 28 is the setup function. This is called when HA loads the component.

In line 29-33 I extract the actual values from the configuration.yaml file.

in line 36 I create the pyst2 AMI object that I will be referencing later when sending commands and getting events from Asterisk. If I succeed connecting to AMI (in line 38-39), I will store this object in the global hass.data object (line 40). It seems a descent way to pass objects an variables between HA components.

Now I have the basic setup of the Asterisk component. Now I can move on the the fun part of creating a sensor that will get actual extension status from Asterisk.

 

The Extension Sensor component

In the configuration.yaml I want to create my sensors like this:

Sensor:
  - platform: astext
    extension: 1000
  - platform: astext
    extension: 1001

I decided to call my platform “astext” for “Asterisk Extension”. In this case I will be watching two extensions.

Now I created the sensor component in custom_components/sensor/astext.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
from homeassistant.helpers.entity import Entity
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
import logging
import socket
import re

_LOGGER = logging.getLogger(__name__)

DEPENDENCIES = ['asterisk']

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required('extension'): cv.string
})


def setup_platform(hass, config, add_devices, discovery_info=None):
    extension = config.get('extension')
    _LOGGER.info("Setting up asterisk extension device for extension {}".format(extension))
    add_devices([AsteriskExtension(hass, extension)], True)

class AsteriskExtension(Entity):
    def __init__(self, hass, extension):
        self._hass = hass
        self._astmanager = hass.data.get('asterisk_manager')
        self._extension = extension
        self._state = "Unknown"
        self._astmanager.register_event('ExtensionStatus', self.handle_asterisk_event)
        _LOGGER.info("Asterisk extention device initialized")

    def handle_asterisk_event(self, event, astmanager):
        extension = event.get_header('Exten')
        status = event.get_header('StatusText')
        if (extension == self._extension):
            _LOGGER.info ("Got asterisk event for extension {}: {}".format(extension,status))
            self._state = status
            self.hass.async_add_job(self.async_update_ha_state())

    @property
    def name(self):
        return "Asterisk Extension {}".format(self._extension)

    @property
    def state(self):
        return self._state

    def update(self):
        result = self._astmanager.extension_state(self._extension,"")
        self._state = result.get_header("StatusText")

 

In line 12 I tell HA that this sensor should not be loaded unless my previously shown asterisk component is loaded correctly..

Line 14-16 tells HA to expect the parameter “extension” in the platform setup of the astext sensor.

Line 19-22 is called when each astext sensor is set up. In my case twice (extension 1000 and extension 1001)

Line 22 registres the AsteriskExtension class (defined in line 24). It passes the hass object and the extension number that was fetched from the sensor platform setup. The “true” parameter tells HA to update the state of sensor on startup.

Line 24-51 is the class the handles the astext sensor. An instance of this class is instanciated for each sensor defined (in my case 2).

Line 27 gets the AMI object that was setup in part1 of this blog. This object is used for communication with AMI.

Line 30 registers the eventhandler for incoming AMI events from Asterisk. We are only listening to events of type ‘ExtensionStatus’. The registration is pointing to the class method defined in line 33.

Line 34-35 gets the extension number and the extension state from AMI. Because we receive events for ALL extensions in Asterosl, we make sure it is for the extension that this instance handles. This is Checked in line 36.

Line 38 keeps the state of the Asterisk extension in a local class variable called “_state”

Line 39 tells HA that the state of this class instance has changed. After HA gets this message, it will go fetch the state. It will get it by calling method in line 45-47

Line 41-43 tells HA how to name the instances of the astext sensor. In my case they will be called “sensor.asterisk_extension_1000” and “sensor.asterisk_extension_1001”

Line 49-51 is called regularly by HA (around every half minute). It tells this class to fetch the status from Asterisk.

 

What do we have now?

After restarting HA and checking the logs you should see this in the developer tools/states:

In the last part I will glue it all together in an automation, that makes things happen.

Part 3

3 thoughts to “Asterisk in Home Assistant – Part 2”

  1. Hello Alex!

    Awesome write up! I have been trying to figure out how to integrate FreePBX into Home Assistant for quite some time now…. I want to pause whatever is on the TV when a phone call comes in 🙂

    I am running into problems when trying to telnet into my FreePBX box. I keep getting the following error:

    Response: Error
    Message: Authentication failed

    I have updated the file /etc/asterisk/manager.conf so that it allows connections from my network and I cut and paste the secret for the admin user from the same file. I seem to get an initial telnet connection because it shows:

    Connected to freepbx.domain.com.
    Escape character is ‘^]’.
    Asterisk Call Manager/2.10.0

    and then I paste in the following:

    Action: Login
    Username: admin
    Secret: **secret from /etc/asterisk/manager.conf**

    and hit the ‘return’ key twice to send the command. Then I get the authentication failed error…. but I’m not sure why since admin user and the secret are in the manager.conf file. I read up on Asterisk 13 AMI commands (https://wiki.asterisk.org/wiki/display/AST/Asterisk+13+AMI+Actions) and it looks like the ‘Login’ command
    should work. I am running FreePBX 14.0.1.1 with Asterisk version 13.15.1.

    I have also tried creating a new user in the manager.conf file an then restarting Asterisk but still no luck. Am I missing something silly???

    1. Did you check that the binaddr in manager.conf is 0.0.0.0 ?

      You could also try to do the telnet from the asterisk box itself like:
      telnet 127.0.0.1 5038

      Just to see if that changes anything.

      1. bindaddr = 0.0.0.0 in my manager.conf file.

        I never though to try telnet locally. Surprisingly enough it works!

        telnet 127.0.0.1 5038
        Trying 127.0.0.1…
        Connected to 127.0.0.1.
        Escape character is ‘^]’.
        Asterisk Call Manager/2.10.0
        Action: Login
        Username: homeassistant
        Secret: ******

        Response: Success
        Message: Authentication accepted

        Event: FullyBooted
        Privilege: system,all
        Status: Fully Booted

        So I played around with the ‘deny’ and ‘permit’ lines and narrowed it down to a potential bug with the AMI.

        permit=192.168.1.10/255.255.255.255
        Allows me to connect just fine from my HomeAssistant box

        permit=127.0.0.1/255.255.255.0
        Allows me to connect just fine locally on FreePBX

        permit=127.0.0.1/255.255.255.0 192.168.1.10/255.255.255.255
        I can connect locally on FreePBX but not from my HomeAssistant box

        It seems that Asterisk isn’t reading in the additional ‘permit’ address. Anyway, for my purposes, I just made a new user ‘HomeAssistant’ and made the ‘permit’ line equal to the IP address of the HomeAssistant box.

        Thanks for the suggestion about trying locally… I probably would have been banging my head on the wall for a while trying to figure out this problem!

Leave a Reply

Your email address will not be published. Required fields are marked *