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.
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???
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.
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!
Hello Alex. First thanks for this project. I can see all sorts of potential uses. Unfortunately trying to implement this has led me down an unexpected and quite frustrating road of getting the import function to work inside the HA virtual environment. I can run your scripts from the command line no problem from any directory by any user (I even modified your very first script to run as a command line sensor successfully), but when HA tries to load them as a component I get “ImportError: No module named ‘asterisk.manager’; ‘asterisk’ is not a package” no matter what I do. I have searched forums for days now to no avail. I’m hoping that you could possibly shed some light on the matter as I am banging my head against a wall at this point. Any insight whatsoever would be greatly appreciated.
Hi,
I am running my HA in a virtualenv: https://www.home-assistant.io/docs/installation/virtualenv
It’s installed long time ago and I just upgraded to 0.71 this morning to see if that broke anything. It didn’t.
My two files are in here:
~/.homeassistant/custom_components/asterisk.py
~/.homeassistant/custom_components/sensor/astext.py
Because of my virtual environment. From your description it sounds like you save your files the wrong place? Or maybe they have wrong rights?
Alex, thanks for the work you put into the Asterisk component. I have a situation where my Asterisk server starts some time after the computer which HASS runs and the component fails with “Could not connect to asterisk server”. Would it be possible to have the component retry connecting to the asterisk server?
Hi David,
That would most certainly be possible, but then the size of the script would double in size. I wrote it mostly to explain how modules for HA is made. Often error handling needs more code lines than the real functional code 🙂
You would probably extend the update method in astext.py to try reconnecting if it’s asked for a value. Also line 36-44 in asterisk.py has to be rewritten.
Best regards,
Alex
Great work! Is it possible to enhance and get the caller id when state=Ringing?