Tuxedo Touch Controller with HA

Tuxedo Touch Controller has a built-in web server and API is also available over HTTPS with the latest firmware.

It requires an “authtoken” to access the API’s but not sure how to obtain one. Anyone?

1 Like

I would also be interested in knowing about this. The smart things forums have references to a few smart apps that can talk to the TotalConnect 2.0 API for alarm control and remote management, but that means paying for monitoring.

I figured out how to obtain an authtoken and access API to some extends via REST client. It appears API is locked down to localhost. No lucks from other programming languages yet. I haven’t given up yet.

What are you trying to do?

My old house had Tuxedo. My biggest pain was limited device support on the HA side. It was great for the HS side. I have a new house, I am still looking to install a Honeywell alarm, but with HASS and this thread has details. In fact just searching the forum for info on hardware and ran across this. Getting ready to purchase the hardware to start playing.

Did you get any progress on this front? If not I might just go ahead and explore in writing this component to access the tuxedo on the same network.

I am also interested to see if anyone was successful in integrating Home Assistant with the Honeywell Tuxedo Touch panel. Any luck?

I’m also interested in this. Anyone find an answer?

I’d be interested in this plugin to…or are there any other options to integrate with Honeywell Tuxedo without investing in an IP Controller?

@kevkha, can you pls help with instructions on how to get the Token? I want to try using the local API route for integration into HomeAssistant

@hani_atassi, been over a year, but would like to take up your offer if you have made any progress with integration to Tuxedo via local API’s?

@sinbox - login to your Tuxedo and follow link System Http API . View the source page of tuxedoapi.html and look for

<input type="hidden" id="readit" value="your-tuxedo-token">

Look under tuxapi.js you can see the headers as followed

var api_key_enc;
var api_iv_enc;

function pageload() {
    api_key_enc = document.getElementById("readit").value.substring(0,64);
    api_iv_enc = document.getElementById("readit").value.substring(64, 96);
    //$.ajaxSetup({headers:{"identity":api_iv_enc}});
}
var protocol = "https";
var apibasepath = "/system_http_api/API_REV01";
var hPath="API_REV01";

The key is here.

callAPI_POST(url, dataCnt, dataCnt.length, CryptoJS.HmacSHA1(header, api_key_enc));

and
                request.setRequestHeader("authtoken", headers);
                request.setRequestHeader("identity", api_iv_enc);

REST API endpoints documented on tuxedoapi.html.
Try to use python or other language for above should do the tricks. Good lucks!

1 Like

Thanks @kevkha. I was able to get the authtoken as you described above, but no luck calling the API’s via Postman using the above authtoken. I even tried encoding my MAC address and getting getting a new AuthToken for the Postman client, but instead of returning a token in the response, it keeps spitting out HTML.

I’m a newbie, and this is one of the first things I’m trying to automate using HA. it looks like I need to spend more time trying to understand what sequence to call the API’s in. At this stage, all I’m looking to do is automatically Disarm when HA detects I arrive home.

Do you have a user account on the Tuxedo Touchpad itself? Can I test this without any authentication and/or over HTTP instead of HTTPS?

@sinbox Check out function callAPI_POST(url, data, paramlength, headers) and especially these lines


            beforeSend: function (request) {
                request.setRequestHeader("authtoken", headers);
                request.setRequestHeader("identity", api_iv_enc);
            },

data: "param=" + data + "&len=" + paramlength + "&tstamp=" + Math.random()

document.getElementById("response").innerHTML = decryptData(resp);  //JSON.stringify(decodeURIComponent(msg));

You would need to get an app token to access from anything other than the web. From the System Http API web page, there is a register command, are you able to put in your MAC ID and get a valid token?

I remember i was able to get that last year but trying again I keep getting an invalid MAC.

Actually it’s under Administration, there is an option to add device MAC address and you get back a security key that you could use for authenticating your requests.

I haven’t tried it but I am writing up a symbol alarm control panel entity for the Tuxedo right now and will let you know if I am successful.

This is a draft for the Tuxedo Alarm Control Panel entity: https://github.com/hinzo/home-assistant/tree/dev/homeassistant/components/tuxedo

It works to arm/disarm and show status. I still need to test it throughly, add tests and documentation before submitting a merge request.

Would be great if you could help testing it in the meantime.

This is a sample of the configuration.yaml:

alarm_control_panel:
  - platform: tuxedo
    mac: <MAC ID of where you are running HA, i.e., xx-xx-xx-xx-xx-xx>
    url: https://<HOST of Tuxedo, must be in the local net, i.e., 192.168.1.5>
    private_key: <retrieved from https://<tuxedo host>/tuxedoapi.html (Administration/AddDeviceMAC)>
    code: <Code used to disarm, ie. 1234>
1 Like

I am getting the following error while installing the component:

2020-01-01 23:03:08 ERROR (MainThread) [homeassistant.components.alarm_control_panel] Error while setting up platform tuxedo
Traceback (most recent call last):
  File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/helpers/entity_platform.py", line 150, in _async_setup_platform
    await asyncio.wait_for(asyncio.shield(task), SLOW_SETUP_MAX_WAIT)
  File "/usr/lib/python3.7/asyncio/tasks.py", line 416, in wait_for
    return fut.result()
  File "/usr/lib/python3.7/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/components/tuxedo/alarm_control_panel.py", line 83, in setup_platform
    tuxedo = TuxedoPanel(name, code, mac, private_key, url)
TypeError: Can't instantiate abstract class TuxedoPanel with abstract methods supported_features

I have created a tuxedo folder on /srv/homeassistant/…/components to copy the files of the tuxedo component.

Thanks in advance for your help.

This seemed promising and what I was looking for to integrate my tuxedo. Was there any progress on this?

I get the same error - I think:

Error while setting up tuxedo platform for alarm_control_panel
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/entity_platform.py", line 179, in _async_setup_platform
    await asyncio.wait_for(asyncio.shield(task), SLOW_SETUP_MAX_WAIT)
  File "/usr/local/lib/python3.7/asyncio/tasks.py", line 442, in wait_for
    return fut.result()
  File "/usr/local/lib/python3.7/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/config/custom_components/tuxedo/alarm_control_panel.py", line 83, in setup_platform
    tuxedo = TuxedoPanel(name, code, mac, private_key, url)
TypeError: Can't instantiate abstract class TuxedoPanel with abstract methods supported_features
Connection lost. Reconnecting…

Hi,

For those who stumble across this like I did (also have a Tuxedo at home). You need to update the code a bit to make it work. The core AlarmPanel code was updated in HA, so that’s why this is not working anymore. The old AlarmPanel class is also deprecated, so I updated it to use the new AlarmPanelEntity as well.

Anyway, here’s the new alarm_control_panel.py file. You will notice that the alarm_trigger and alarm_arm_custom_bypass both do nothing because the Tuxedo API doesn’t support doing these things but the AlarmPanelEntity class wants it implemented.

"""Interfaces with Tuxedo control panel."""
import logging
import random

import base64
import hmac
import requests
import voluptuous as vol
import urllib
import json
import re
import asyncio

from datetime import timedelta
from hashlib import sha1
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Util.Padding import unpad

from aiohttp.hdrs import CACHE_CONTROL, PRAGMA

from homeassistant.core import callback
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.const import (
    CONF_CODE,
    CONF_URL,
    CONF_NAME,
    CONF_MAC,
    STATE_ALARM_PENDING,
    STATE_ALARM_ARMING,
    STATE_ALARM_ARMED_AWAY,
    STATE_ALARM_ARMED_HOME,
    STATE_ALARM_ARMED_NIGHT,
    STATE_ALARM_DISARMED,
    STATE_UNAVAILABLE,
)
import homeassistant.helpers.config_validation as cv

_LOGGER = logging.getLogger(__name__)

DEFAULT_NAME = "Tuxedo"

REGISTER_INTERVAL = timedelta(minutes=30)

CONF_PRIVATE_KEY = "private_key"

API_REV = "API_REV01"
API_BASE_PATH = "/system_http_api/" + API_REV

STATUS_MSG_ARMED_STAY = r"Armed Stay"
STATUS_MSG_ARMED_AWAY = r"Armed Away"
STATUS_MSG_ARMED_INSTANT = r"Armed Instant"
STATUS_MSG_DISARMED = r"Ready To Arm"
STATUS_MSG_TIMER = r"\d\s? Secs Remaining"
STATUS_MSG_NOT_AVAILABLE = r"Not available"
STATUS_MSG_NOT_READY = r"Not Ready Fault"

ARMING_NAME_STAY = "STAY"
ARMING_NAME_AWAY = "AWAY"
ARMING_NAME_NIGHT = "NIGHT"

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
    {
        vol.Required(CONF_URL): cv.string,
        vol.Required(CONF_MAC): cv.string,
        vol.Required(CONF_PRIVATE_KEY): cv.string,
        vol.Optional(CONF_CODE): cv.positive_int,
        vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
    }
)


def setup_platform(hass, config, add_entities, discovery_info=None):
    """Set up Tuxedo control panel."""
    name = config.get(CONF_NAME)
    code = config.get(CONF_CODE)
    mac = config.get(CONF_MAC).replace(":", "-")
    private_key = config.get(CONF_PRIVATE_KEY)
    url = config.get(CONF_URL)

    tuxedo = TuxedoPanel(name, code, mac, private_key, url)
    add_entities([tuxedo], True)

def _sign_string(value, secret_key):
    return hmac.new(bytes(secret_key, "utf-8"), bytes(value, "utf-8"), sha1).hexdigest()


def _encrypt_data(data, key_string, iv_string):
    key = bytes.fromhex(key_string)
    iv = bytes.fromhex(iv_string)
    encryptor = AES.new(key, AES.MODE_CBC, IV=iv)
    cipher = encryptor.encrypt(pad(data, 16))
    return str(base64.b64encode(cipher), "utf-8")


def _decrypt_data(data, key_string, iv_string):
    raw = base64.b64decode(data)
    key = bytes.fromhex(key_string)
    iv = bytes.fromhex(iv_string)
    encryptor = AES.new(key, AES.MODE_CBC, IV=iv)
    decrypted = unpad(encryptor.decrypt(raw), AES.block_size)
    return str(decrypted, "utf-8")


class TuxedoPanel(alarm.AlarmControlPanelEntity):
    """The Tuxedo panel definition."""

    def __init__(self, name, code, mac, private_key, url):
        """Initialize the Tuxedo status."""

        self._name = name
        self._code = str(code) if code else None
        self._mac = mac
        self._api_key_enc = private_key[0:64]
        self._api_iv_enc = private_key[64:]
        self._url = url
        self._state = STATE_ALARM_PENDING

    async def async_added_to_hass(self):
        """Hookup registration callback."""

        @callback
        def async_register(event_time=None):
            result = self._api_request(
                "/Registration/Register", {"mac": self._mac, "operation": "set"}
            )
            _LOGGER.info("register result: %s", result)

        async_register()
        async_track_time_interval(self.hass, async_register, REGISTER_INTERVAL)

    @property
    def name(self):
        """Return the name of the device."""
        return self._name

    @property
    def code_format(self):
        """Regex for code format or None if no code is required."""
        if self._code is None:
            return alarm.FORMAT_NUMBER
        return None
    
    @property
    def changed_by(self):
        return None

    @property
    def should_poll(self):
        """We only support polling."""
        return True

    @property
    def state(self):
        """Return the state of the device."""
        return self._state
    
    @property
    def supported_features(self) -> int:
        """Return the list of supported features."""
        return alarm.SUPPORT_ALARM_ARM_HOME | alarm.SUPPORT_ALARM_ARM_AWAY | alarm.SUPPORT_ALARM_ARM_NIGHT

    def update(self):
        """Return the state of the device."""
        state = None
        result = self._api_request("/GetSecurityStatus", {"operation": "get"})
        if result:
            _LOGGER.info("update result: %s", result)
            status_msg = result["Status"]
            if re.search(STATUS_MSG_DISARMED, status_msg):
                state = STATE_ALARM_DISARMED
            elif re.search(STATUS_MSG_ARMED_STAY, status_msg):
                state = STATE_ALARM_ARMED_HOME
            elif re.search(STATUS_MSG_ARMED_AWAY, status_msg):
                state = STATE_ALARM_ARMED_AWAY
            elif re.search(STATUS_MSG_ARMED_INSTANT, status_msg):
                state = STATE_ALARM_ARMED_NIGHT
            elif re.search(STATUS_MSG_TIMER, status_msg):
                state = STATE_ALARM_ARMING
            elif re.search(STATUS_MSG_NOT_AVAILABLE, status_msg) or re.search(
                STATUS_MSG_NOT_READY, status_msg
            ):
                state = STATE_UNAVAILABLE
        else:
            _LOGGER.error("couldn't fetch status")
        self._state = state

    async def async_alarm_disarm(self, code=None):
        """Send disarm command."""
        await self._api_disarm_request(code)

    async def async_alarm_arm_away(self, code=None):
        """Send arm away command."""
        await self._api_arm_request(ARMING_NAME_AWAY, code)

    async def async_alarm_arm_home(self, code=None):
        """Send arm home command."""
        await self._api_arm_request(ARMING_NAME_STAY, code)

    async def async_alarm_arm_night(self, code=None):
        """Send arm night command."""
        await self._api_arm_request(ARMING_NAME_NIGHT, code)

    async def alarm_trigger(self, code=None):
        """Send arm trigger command.  Tuxedo doesn't have this API"""
        return None

    async def alarm_arm_custom_bypass(self, code=None):
        """Send arm custom bypass command.  Tuxedo doesn't have this API"""
        return None

    async def _api_disarm_request(self, code=None):
        result = self._api_request(
            "/AdvancedSecurity/DisarmWithCode",
            {"pID": "1", "ucode": code or self._code, "operation": "set"},
        )
        _LOGGER.info("api_disarm_request result: %s", result)
        if result:
            await asyncio.sleep(2)
            self.async_schedule_update_ha_state()

    async def _api_arm_request(self, arm_name, code=None):
        result = self._api_request(
            "/AdvancedSecurity/ArmWithCode",
            {
                "arming": arm_name,
                "pID": "1",
                "ucode": code or self._code,
                "operation": "set",
            },
        )
        _LOGGER.info("api_arm_request %s result: %s", arm_name, result)
        if result:
            await asyncio.sleep(2)
            self.async_schedule_update_ha_state()

    def _api_request(self, api_name, params):
        uri_params = urllib.parse.urlencode(params)
        _LOGGER.info("api_name: %s, uri_params: %s", api_name, uri_params)
        uri_params_encrypted = _encrypt_data(
            bytes(uri_params, "utf-8"), self._api_key_enc, self._api_iv_enc
        )

        full_url = self._url + API_BASE_PATH + api_name
        header = "MACID:" + self._mac + ",Path:" + API_REV + api_name
        authtoken = _sign_string(header, self._api_key_enc)

        response = requests.post(
            full_url,
            data={
                "param": uri_params_encrypted,
                "len": len(urllib.parse.quote_plus(uri_params_encrypted)),
                "tstamp": random.random(),
            },
            headers={
                "authtoken": authtoken,
                "identity": self._api_iv_enc,
                PRAGMA: "no-cache",
                CACHE_CONTROL: "no-cache",
            },
            verify=False,
        )

        result = None
        if response.status_code == 200:
            content = json.loads(response.content)
            result = _decrypt_data(
                content["Result"], self._api_key_enc, self._api_iv_enc
            )
            result = json.loads(result)
        elif response.status_code == 401:
            # Unauthorized
            _LOGGER.error("Unauthorized, API: %s", api_name)
        elif response.status_code == 405:
            # Method Not Allowed
            _LOGGER.error("Method Not Allowed, API: %s", api_name)
        else:
            # Unknown
            _LOGGER.error(
                "Unknown error, status_code: %s, API: %s",
                response.status_code,
                api_name,
            )
        return result

Happy integrating!

Anyone still using this? Currently have an AlarmDecoder setup but looking at this as an alternative.

New to HA and the alarm system as a whole (just moved so switching home automation software from Domoticz and installing new alarm- Vista 20p). Currently trying to figure out how to go from the above python script and yaml to having something on my dashboard.