DSC PowerSeries Alarm - IT-100 (RS232)

Hi

Much of the discussions around Alarm integration is via the Envisalink device, however my panel PC1864 is connected to an IT-100 Serial device, and I’d love to integrate that to my HomeAssistant set up.

The IT-100 integrations manual is here - http://cms.dsc.com/download.php?t=1&id=16238 if that helps.

Any advice/guidance on connecting to this would be much appreciated.

I would agree. OpenHAB has a binding that works with IT100, Envisalink, and a tcp socket. I purchased a Envisalink in hopes of integrating it. Home Assistant seems much harder to get up and running than OpenHAB for me anyway. I would guess because of the lack of commercial type drivers and very odd automation scripting.

Home Assistant appears to be very much geared to the tinkerer that wants to make PCBs and such.

I prefer the python so I’m trying to force myself on Home Assistant :slight_smile:

There seems to be some python code out there for the IT-100, which I’m hoping someone can work into Home Assistant.

A Python driver for the DSC IT-100 integration module



Just a follow up my earlier post to see if anyone has made any progress with getting Home Assistant to work with a DSC Alarm panel via their IT-100 (RS232) module.

It would be a great addition.

Having same problem getting home assistant to work with IT-100 module. If someone have figured it out it would be great.

I’m not sure if this helps - but while I’m not using it with HASS, I am using my IT-100 with my Vera home automation controller via a raspberry pi , USB to serial cable (running ser2net)

That allows me to connect to the it-100 which is connected to my DSC panel over Ethernet/IP like Envisalink

I am interested in implementing this. Python is not my native language but I should have no problem putting something together. That driver linked above seems pretty simple to use. I can’t commit to making this fully featured but I’m pretty sure I can get some basic functionality up for this. Has anyone already began any work here?

Hi @pho3nixf1re

I’m not aware of anyone starting on this so it would be great to have the IT-100 supported. If I can help with testing please let me know.

EDIT: Ignore the code in this post, use the github repo at https://github.com/SolidElectronics/evl-emu

I’d like to see a native driver as well, but in the meantime I wrote this.
It’s a service that connects to the serial port and emulates an Envisalink so Home Assistant can use it. I haven’t tested this extensively, and I offer no guarantees it will work, but I thought I’d share it in case it’s useful to someone. It’s written using the Python multiprocessing libraries so it spawns several processes that each perform one task. Ideally this would be rewritten in a cleaner way with asyncio but that’s currently beyond my ability.

I have this running under Hassbian on a rPi3 with the DSC panel connected via a serial-USB adapter at /dev/ttyUSB0. Note: It requires the ‘pyserial’ module for interacting with the serial port.

Include this in /etc/rc.local to automatically start after boot.
/bin/su -c '/home/homeassistant/.homeassistant/evl-emu.py >/dev/null 2>&1' homeassistant

configuration.yaml
envisalink: !include envisalink.yaml

envisalink.yaml
Change 0000 to a valid code (for arming and disarming the panel)
I only included two zones here, and it probably only works with one partition.

  host: 127.0.0.1
  panel_type: DSC
  user_name: user
  password: pass
  code: '0000'
  zones:
    1:
      name: 'Front door'
      type: 'door'
    2:
      name: 'Garage back door'
      type: 'door'
#...
  partitions:
    1:
      name: 'Alarm'

/home/homeassistant/.homeassistant/evl-emu.py
Change the SERIAL_PORT to match your setup. If possible, create a udev rule to make the USB-serial adapter always show up as /dev/it100.

#!/usr/bin/env python3

"""
Support for DSC alarm control panels using IT-100 integration module by emulating an EnvisaLink EVL-4
"""

import logging
import sys
import itertools
import os
import time
import multiprocessing
import subprocess
import signal
import serial
import inspect
import random
import socket

REQUIREMENTS = ['pyserial']

_LOGGER = logging.getLogger(__name__)

DEFAULT_PARTITIONS = 1
DEFAULT_ZONES = 64

SERIAL_PORT = '/dev/ttyUSB0'
#SERIAL_PORT='/dev/it100'
SERIAL_BAUD = 9600
NETWORK_HOST = '127.0.0.1'
NETWORK_PORT = 4025

HEX_MSG = True


# Zone state definitions
ZONE_OPEN = 0
ZONE_CLOSED = 1


# --------------------------------------------------------------------------------
#	DSC Protcol definitions
# --------------------------------------------------------------------------------
COMMAND_POLL = '000'
COMMAND_STATUS_REQUEST = '001'
COMMAND_LABELS_REQUEST = '002'
COMMAND_SET_TIME_DATE = '010'
COMMAND_OUTPUT_CONTROL = '020'
COMMAND_PARTITION_ARM_CONTROL_AWAY = '030'
COMMAND_PARTITION_ARM_CONTROL_STAY = '031'
COMMAND_PARTITION_ARM_CONTROL_ZERO_ENTRY = '032'
COMMAND_PARTITION_ARM_CONTROL_WITH_CODE = '033'
COMMAND_PARTITION_DISARM_CONTROL = '040'
COMMAND_TIME_STAMP_CONTROL = '055'
COMMAND_TIME_DATE_BCAST_CONTROL = '056'
COMMAND_TEMPERATURE_BCAST_CONTROL = '057'
COMMAND_VIRTUAL_KEYBOARD_CONTROL = '058'
COMMAND_TRIGGER_PANIC_ALARM = '060'
COMMAND_KEY_PRESSED = '070'
COMMAND_SET_BAUD_RATE = '080'
COMMAND_CODE_SEND = '200'

NOTIFY_ACK = '500'
NOTIFY_ERROR = '501'
NOTIFY_SYSTEM_ERROR = '502'
NOTIFY_TIME_DATE_BCAST = '550'
NOTIFY_LABELS = '570'
NOTIFY_BAUD_RATE_SET = '580'
NOTIFY_ZONE_ALARM = '601'
NOTIFY_ZONE_ALARM_RESTORE = '602'
NOTIFY_ZONE_TAMPER = '603'
NOTIFY_ZONE_TAMPER_RESTORE = '604'
NOTIFY_ZONE_FAULT = '605'
NOTIFY_ZONE_FAULT_RESTORE = '606'
NOTIFY_ZONE_OPEN = '609'
NOTIFY_ZONE_RESTORED = '610'
NOTIFY_DURESS_ALARM = '620'
NOTIFY_FIRE_KEY_ALARM = '621'
NOTIFY_FIRE_KEY_RESTORED = '622'
NOTIFY_AUXILARY_KEY_ALARM = '623'
NOTIFY_AUXILARY_KEY_RESTORED = '624'
NOTIFY_PANIC_KEY_ALARM = '625'
NOTIFY_PANIC_KEY_RESTORED = '626'
NOTIFY_AUXILARY_INPUT_ALARM = '631'
NOTIFY_AUXILARY_INPUT_RESTORED = '632'
NOTIFY_PARTITION_READY = '650'
NOTIFY_PARTITION_NOT_READY = '651'
NOTIFY_PARTITION_ARMED = '652'
NOTIFY_PARTITION_READY_TO_FORCE_ARM = '653'
NOTIFY_PARTITION_IN_ALARM = '654'
NOTIFY_PARTITION_DISARMED = '655'
NOTIFY_PARTITION_EXIT_DELAY = '656'
NOTIFY_PARTITION_ENTRY_DELAY = '657'
NOTIFY_KEYPAD_LOCKOUT = '658'
NOTIFY_KEYPAD_BLANKING = '659'
NOTIFY_COMMAND_OUTPUT = '660'
NOTIFY_INVALID_CODE = '670'
NOTIFY_FUNCTION_NOT_AVAILABLE = '671'
NOTIFY_FAILED_TO_ARM = '672'
NOTIFY_PARTITION_BUSY = '673'
NOTIFY_PARTITION_USER_CLOSING = '700'
NOTIFY_PARTITION_SPECIAL_CLOSING = '701'
NOTIFY_PARTITION_PARTIAL_CLOSING = '702'
NOTIFY_PARTITION_USER_OPENING = '750'
NOTIFY_PARTITION_SPECIAL_OPENING = '751'
NOTIFY_PANEL_BATTERY_TROUBLE = '800'
NOTIFY_PANEL_BATTERY_RESTORED = '801'
NOTIFY_PANEL_AC_TROUBLE = '802'
NOTIFY_PANEL_AC_RESTORED = '803'
NOTIFY_SYSTEM_BELL_TROUBLE = '806'
NOTIFY_SYSTEM_BELL_RESTORED = '807'
NOTIFY_GENERAL_DEV_LOW_BATTERY = '821'
NOTIFY_GENERAL_DEV_LOW_BATTERY_RESTORED = '822'
NOTIFY_GENERAL_SYSTEM_TAMPER = '829'
NOTIFY_GENERAL_SYSTEM_TAMPER_RESTORED = '830'
NOTIFY_PARTITION_TROUBLE = '840'
NOTIFY_PARTITION_TROUBLE_RESTORED = '841'
NOTIFY_FIRE_TROUBLE_ALARM = '842'
NOTIFY_FIRE_TROUBLE_RESTORED = '843'
NOTIFY_KEYBUS_FAULT = '896'
NOTIFY_KEYBUS_RESTORED = '897'
NOTIFY_CODE_REQUIRED = '900'
NOTIFY_BEEP_STATUS = '904'
NOTIFY_VERSION = '908'


# --------------------------------------------------------------------------------
#	Envisalink Protcol definitions
# --------------------------------------------------------------------------------
EVL_LOGIN_REQUEST = '005'
EVL_DUMP_TIMERS = '008'
EVL_KEY_STRING = '071'

EVL_LOGIN_INTERACTION = '505'
EVL_DUMP_TIMER_RESPONSE = '615'

"""
Command codes not handled by DSC
005
008
071
072
073
074
80 ??

Response codes not handled by DSC
505
510
511
615
616
663
664
674
680
815
849
912
921
922
"""

# --------------------------------------------------------------------------------
#	Classes
# --------------------------------------------------------------------------------


class dsc_zone():
	def __init__(self, zone):
		self.zone = zone
		self.state = ZONE_CLOSED
		self.description = ""
		self.close_time = 0

	def getZone(self):
		return self.zone

	def setState(self, newstate):
		# Update zone timer if zone is going from open to closed
		if (self.state == ZONE_OPEN and newstate == ZONE_CLOSED):
			self.close_time = time.time()
		# Set new state
		self.state = newstate
			
	def getState(self):
		return self.state

	def setDescription(self, desc):
		self.description = desc
	def getDescription(self):
		return self.description

	"""
    Report zone timer as 4-byte little-endian string
      FFFF = open
      When closed, start counting down from 0xFFFF every five seconds
      eg. after ten seconds the value is 0xFFFD, return little-endian as FDFF

      Implementation note:
      If HA polls the zone timers within five seconds of closing and sees 0xFFFF because it hasn't decremented yet, it assumes the zone has re-opened.
      None of the documentation mentions this.

      NOTE: any result less than 30 seconds is treated as still open.  just wow.
      https://community.home-assistant.io/t/dsc-alarm-integration/409/390
      It's in pyenvisalink/envisalink_base_client.py
      
      Solution is to start counting down from 0xFFFA
	"""
	def getTimer(self):
		if (self.state == ZONE_OPEN):
			return "FFFF"
		else:
			timedelta = int((time.time() - self.close_time) / 5)
			timedeltastring = format(max(0, 0xFFFF - 6 - timedelta), '04X')
			timedeltastringLE = timedeltastring[2:4] + timedeltastring[0:2]
			return timedeltastringLE

		





# --------------------------------------------------------------------------------
#	Serial I/O Routines
# --------------------------------------------------------------------------------

"""
	Listen to serial port and put incoming messages into the read queue
	- Only passes message content.  Checksum and CR/LF are removed.
"""
def serialRead(readQueueSer, port):
	print("Starting {} ({})".format(inspect.stack()[0][3], os.getpid()))

	try:
		lastdatatime = time.time()
		msgbuf = ''
		
		while True:
			read_byte = port.read(1)

			# If there is a long delay between messages while data is in the buffer, assume something went wrong and throw out the old data
			thisdatatime = time.time()
			if ( ((thisdatatime - lastdatatime) > 0.5) and (msgbuf.__len__() > 0) ):
				print ("ERROR: flushing stale data from receive buffer {}".format(msgbuf))
				msgbuf=''

			# Add each byte of received data to message buffer			
			msgbuf += read_byte.decode('ASCII')
			lastdatatime = time.time()

			# If we have enough characters for a full message, start checking for CR/LR terminator
			if (msgbuf.__len__() >= 7):
				if (ord(msgbuf[msgbuf.__len__()-2]) == 0x0D and ord(msgbuf[msgbuf.__len__()-1]) == 0x0A):
					# Found terminator, message is complete.
					if (HEX_MSG):
						timestamp=time.strftime("[%H:%M:%S]", time.localtime())
						print ("{} DSC In  > {}   {}".format(timestamp, ":".join("{:02X}".format(ord(c)) for c in msgbuf), msgbuf[0:msgbuf.__len__() - 2] ))
					# Queue message if checksum OK
					msgdata = msgbuf[0:msgbuf.__len__() - 4]
					msgchksum = msgbuf[msgbuf.__len__() - 4:msgbuf.__len__() - 2]
					if (msgchksum == dsc_checksum(msgdata)):
						readQueueSer.put(msgdata)
					else:
						print ("{} DSC In  > Checksum error".format(timestamp))
					msgbuf = ''

	except KeyboardInterrupt:
		pass
	except:
		print("Caught exception in {}: {}".format(inspect.stack()[0][3], sys.exc_info()[0]))
		raise
	print("Exiting {}".format(inspect.stack()[0][3]))
	return None



"""
	Pull messages from write queue and send them to the serial port
	- Does not add checksum or CR/LF
"""
def serialWrite(writeQueueSer, port):
	print("Starting {} ({})".format(inspect.stack()[0][3], os.getpid()))

	try:
		while True:
			# Block until something is in the queue
			msg = writeQueueSer.get(True, None)

			# Send message
			if (HEX_MSG):
				timestamp=time.strftime("[%H:%M:%S]", time.localtime())
				print("{} DSC Out < {}   {}".format(timestamp, ":".join("{:02X}".format(ord(c)) for c in msg), msg[0:msg.__len__() - 2] ))
			port.write(bytes(msg, 'UTF-8'))

	except KeyboardInterrupt:
		pass
	except:
		print("Caught exception in {}: {}".format(inspect.stack()[0][3], sys.exc_info()[0]))
		raise
	print("Exiting {}".format(inspect.stack()[0][3]))
	return None



# --------------------------------------------------------------------------------
#	Network Functions
# --------------------------------------------------------------------------------

"""
	Listen to socket connection and put incoming messages into the read queue
	This routine does not handle the actual connection, just interacting with the existing connection
	- Only passes message content.  Checksum and CR/LF are removed.
"""
def networkRead(readQueueNet, conn):
	print("Starting {} ({})".format(inspect.stack()[0][3], os.getpid()))
	
	try:
		lastdatatime = time.time()
		msgbuf = ''
		
		while True:
			read_byte = conn.recv(1)

			if (read_byte == b''):
				raise NameError('Connection closed by client')

			# If there is a long delay between messages while data is in the buffer, assume something went wrong and throw out the old data
			thisdatatime = time.time()
			if ( ((thisdatatime - lastdatatime) > 0.5) and (msgbuf.__len__() > 0) ):
				print ("ERROR: flushing stale data from receive buffer {}".format(msgbuf))
				msgbuf=''

			# Add each byte of received data to message buffer			
			msgbuf += read_byte.decode('ASCII')
			lastdatatime = time.time()

			# If we have enough characters for a full message, start checking for CR/LR terminator
			if (msgbuf.__len__() >= 7):
				if (ord(msgbuf[msgbuf.__len__()-2]) == 0x0D and ord(msgbuf[msgbuf.__len__()-1]) == 0x0A):
					# Found terminator, message is complete.
					if (HEX_MSG):
						timestamp=time.strftime("[%H:%M:%S]", time.localtime())
						print ("{} EVL In  > {}   {}".format(timestamp, ":".join("{:02X}".format(ord(c)) for c in msgbuf), msgbuf[0:msgbuf.__len__() - 2] ))
					# Queue message if checksum OK
					msgdata = msgbuf[0:msgbuf.__len__() - 4]
					msgchksum = msgbuf[msgbuf.__len__() - 4:msgbuf.__len__() - 2]
					if (msgchksum == dsc_checksum(msgdata)):
						readQueueNet.put(msgdata)
					else:
						print ("{} EVL In  > Checksum error".format(timestamp))
					msgbuf = ''

	except NameError:
		print ("Connection closed by client, terminating networkRead thread.")
		conn.close()
		return None
	except OSError:
		print ("OSError: {}".format(inspect.stack()[0][3]))
		conn.close()
		return None
	except KeyboardInterrupt:
		pass
	except:
		print("Caught exception in {}: {}".format(inspect.stack()[0][3], sys.exc_info()[0]))
		raise
	print("Exiting {}".format(inspect.stack()[0][3]))
	return None


"""
	Pull messages from write queue and send them to the socket connection
	- Does not add checksum or CR/LF
"""
def networkWrite(writeQueueNet, conn):
	print("Starting {} ({})".format(inspect.stack()[0][3], os.getpid()))

	try:
		while True:
			# This should block until something is in the queue
			msg = writeQueueNet.get(True, None)

			# Print message and send it out the socket connection
			if (HEX_MSG):
				timestamp=time.strftime("[%H:%M:%S]", time.localtime())
				print("{} EVL Out < {}   {}".format(timestamp, ":".join("{:02X}".format(ord(c)) for c in msg), msg[0:msg.__len__() - 2] ))
			conn.send(bytes(msg, 'UTF-8'))

	except OSError:
		print ("OSError: {}".format(inspect.stack()[0][3]))
		conn.close()
		return None
	except KeyboardInterrupt:
		pass
	except:
		print("Caught exception in {}: {}".format(inspect.stack()[0][3], sys.exc_info()[0]))
		raise
	print("Exiting {}".format(inspect.stack()[0][3]))
	return None


"""
	Test function
	Open a network socket to accept fake commands that look like they're originating from the panel
	Note: Don't send checksum or CR/LF, they're not needed.
"""
def networkReadTest(readQueueSer):
	print("Starting {} ({})".format(inspect.stack()[0][3], os.getpid()))

	try:
		sock = socket.socket()
		sock.bind((NETWORK_HOST, (1 + NETWORK_PORT)))
		sock.setblocking(1)
		sock.listen(5)
	
		print ("Test listening on {}:{}".format(NETWORK_HOST, str(1 + NETWORK_PORT)))
		while True:
			conn, addr = sock.accept()
			msg = conn.recv(128).decode("UTF-8")
			if (HEX_MSG):
				timestamp=time.strftime("[%H:%M:%S]", time.localtime())
				#print ("{} Test In > {}   {}".format(timestamp, ":".join("{:02X}".format(ord(c)) for c in msg), msg ))
				print ("{} Test In > {}".format(timestamp, msg))
			readQueueSer.put(msg)
			conn.close()

	except OSError:
		print ("OSError: {}".format(inspect.stack()[0][3]))
		conn.close()
		return None
	except KeyboardInterrupt:
		pass
	except:
		print("Caught exception in {}: {}".format(inspect.stack()[0][3], sys.exc_info()[0]))
		raise
	print("Exiting {}".format(inspect.stack()[0][3]))
	return None





# --------------------------------------------------------------------------------
#	Event Processing
# --------------------------------------------------------------------------------

"""
Process messages that arrive from DSC via the serial queue.
"""
def msghandler_dsc(readQueueSer, writeQueueSer, writeQueueNet, zones):
	print("Starting {} ({})".format(inspect.stack()[0][3], os.getpid()))

	try:
		while True:

			msg = readQueueSer.get()
			if (msg.__len__() > 0):

				command = str(msg[0:3])
				data = str(msg[3:msg.__len__()])
				timestamp=time.strftime("[%H:%M:%S]", time.localtime())

				# Track zone state changes
				if (command == NOTIFY_ZONE_OPEN):
					zoneobj = zones[int(data)-1]
					zoneobj.setState(ZONE_OPEN)
					zones[int(data)-1] = zoneobj
				elif (command == NOTIFY_ZONE_RESTORED):
					zoneobj = zones[int(data)-1]
					zoneobj.setState(ZONE_CLOSED)
					zones[int(data)-1] = zoneobj

				# All other messages relay to EVL
				writeQueueNet.put(dsc_send(msg))

	except KeyboardInterrupt:
		pass
	except:
		print("Caught exception in {}: {}".format(inspect.stack()[0][3], sys.exc_info()[0]))
		raise
	print("Exiting {}".format(inspect.stack()[0][3]))
	return None


"""
Process messages that arrive from the EVL client via the network queue
"""
def msghandler_evl(readQueueNet, writeQueueNet, writeQueueSer, zones):
	print("Starting {} ({})".format(inspect.stack()[0][3], os.getpid()))

	try:
		while True:
			msg = readQueueNet.get()
			if (msg.__len__() > 0):

				# Print incoming message
				timestamp=time.strftime("[%H:%M:%S]", time.localtime())

				# Decode message and handle it as appropriate
				command = str(msg[0:3])
				data = str(msg[3:(msg.__len__())])

				# --------------------------------------------------------------------------------
				# Message handlers
				# This needs to intercept EVL-specific messages and not send those to the panel
				# --------------------------------------------------------------------------------

				timestamp=time.strftime("[%H:%M:%S]", time.localtime())
				# Login
				if (command == EVL_LOGIN_REQUEST):
					writeQueueNet.put(dsc_send(EVL_LOGIN_INTERACTION + "1"))
				# Dump timers
				elif (command == EVL_DUMP_TIMERS):
					timermsg = ""
					for z in zones:
						timermsg += z.getTimer()
					writeQueueNet.put(dsc_send(EVL_DUMP_TIMER_RESPONSE + timermsg))
				# Key sequence
				# - Enables virtual keypad only while code is being sent
				elif (command == EVL_KEY_STRING):
					writeQueueSer.put(dsc_send(COMMAND_VIRTUAL_KEYBOARD_CONTROL + '1'))
					time.sleep(0.5)
					for c in data[1:]:
						keypress = dsc_send(COMMAND_KEY_PRESSED + c)
						writeQueueSer.put(keypress)
						time.sleep(0.25)
						keypress = dsc_send(COMMAND_KEY_PRESSED + '^')
						writeQueueSer.put(keypress)
						time.sleep(0.25)
					writeQueueSer.put(dsc_send(COMMAND_VIRTUAL_KEYBOARD_CONTROL + '0'))
				# Code padding (most commands require 6 digits, 4-digit codes need two zeros appended.
				# -- Partition disarm
				elif (command == COMMAND_PARTITION_DISARM_CONTROL):
					disarm_zone = data[0]
					disarm_code = data[1:]
					if (len(disarm_code)  == 4):
						disarm_code += '00'
					writeQueueSer.put(dsc_send(command + disarm_zone + disarm_code))
				# -- Code request
				elif (command == COMMAND_CODE_SEND):
					if (len(data)  == 4):
						data += '00'
					# DSC documentation is incorrect here.  Need to send partition number ahead of code.
					# Ideally pyenvisalink would do this correctly by remembering the partition from the '900'.
					writeQueueSer.put(dsc_send(command + '1' + data))
				# Customizations
				# - Change "arm stay" to "arm zero entry delay"
				elif (command == COMMAND_PARTITION_ARM_CONTROL_STAY):
					writeQueueSer.put(dsc_send(COMMAND_PARTITION_ARM_CONTROL_ZERO_ENTRY + data))

				# All other messages just relay to DSC as-is
				else:
					writeQueueSer.put(dsc_send(msg))

	except KeyboardInterrupt:
		pass
	except:
		print("Caught exception in {}: {}".format(inspect.stack()[0][3], sys.exc_info()[0]))
		raise
	print("Exiting {}".format(inspect.stack()[0][3]))
	return None



# --------------------------------------------------------------------------------
#	Helper functions
# --------------------------------------------------------------------------------

# Return checksum string for a given message
def dsc_checksum(msg):
	total = 0
	for i in msg:
		total += ord(i)
	total = total % 256

	return "{:02X}".format(total)

# Append checksum and CR/LF for outgoing messages
def dsc_send(msg):
	msg += dsc_checksum(msg)
	msg += chr(0x0D)
	msg += chr(0x0A)
	return msg


# Signal handler
def signal_handler(signal, frame):
	print("Signal handler called with signal {}".format(signal))
	sys.exit(0)



# --------------------------------------------------------------------------------
#	MAIN
# --------------------------------------------------------------------------------

if __name__ == "__main__":
	try:
		print("Process: {}".format(os.getpid()))

		# Open serial port
		ser = None
		ser = serial.Serial(SERIAL_PORT, SERIAL_BAUD, timeout=None)

		# Create socket
		sock = socket.socket()

		# Create shared queues for inter-process message handling
		readQueueSer = multiprocessing.Queue()
		writeQueueSer = multiprocessing.Queue()
		readQueueNet = multiprocessing.Queue()
		writeQueueNet = multiprocessing.Queue()
		
		# Create shared data space
		mgr = multiprocessing.Manager()
		zones = mgr.list()

		# Allocate zone objects
		for z in range(64):
			zones.append(dsc_zone(zone=z))


		# Start worker threads
		p_serialread = multiprocessing.Process(target=serialRead, args=(readQueueSer, ser))
		p_serialwrite = multiprocessing.Process(target=serialWrite, args=(writeQueueSer, ser))
		p_msghandler_dsc = multiprocessing.Process(target=msghandler_dsc, args=(readQueueSer, writeQueueSer, writeQueueNet, zones))
		p_msghandler_evl = multiprocessing.Process(target=msghandler_evl, args=(readQueueNet, writeQueueNet, writeQueueSer, zones))

		# Startup threads
		p_serialread.start()
		p_serialwrite.start()
		p_msghandler_dsc.start()
		p_msghandler_evl.start()

		# Stop execution here until a SIGINT or SIGTERM is received.  At this point all the work is being done by subprocesses and this function is just waiting to exit
		signal.signal(signal.SIGINT, signal_handler)
		signal.signal(signal.SIGTERM, signal_handler)

		time.sleep(3)

		print("---------- Panel Initialization Start ----------")
		writeQueueSer.put(dsc_send(COMMAND_POLL))
#		time.sleep(1)
#		writeQueueSer.put(dsc_send(COMMAND_VIRTUAL_KEYBOARD_CONTROL + '1'))
		time.sleep(2)
		print("---------- Panel Initialization End ----------")

		# Startup test network connection
		p_networkreadtest = multiprocessing.Process(target=networkReadTest, args=(readQueueSer, ))
		p_networkreadtest.start()

		# Handle network connections
		sock.bind((NETWORK_HOST, NETWORK_PORT))
		sock.setblocking(1)
		sock.listen(5)
		print ("EVL listening on {}:{}".format(NETWORK_HOST, str(NETWORK_PORT)))
		while(1):
			"""
				This should only attempt to handle one client at a time.
				If a new client connects, the queues are flushed and the network read/write threads are destroyed and recreated for the new connection.
				The client should immediately be sent the login interaction message to request authentication.
			"""
			# Wait for client connection.
			conn, addr = sock.accept()
			print ("Client connected: {}".format(addr))

			# Flush queues
			while ( readQueueSer.empty() == False ): readQueueSer.get()
			while ( readQueueNet.empty() == False ): readQueueNet.get()
			while ( writeQueueSer.empty() == False ): writeQueueSer.get()
			while ( writeQueueNet.empty() == False ): writeQueueNet.get()

			# Terminate old network I/O threads if they exist
			if 'p_networkread' in locals(): p_networkread.terminate()
			if 'p_networkwrite' in locals(): p_networkwrite.terminate()

			# Create new network I/O threads for this connection
			p_networkread = multiprocessing.Process(target=networkRead, args=(readQueueNet, conn))
			p_networkwrite = multiprocessing.Process(target=networkWrite, args=(writeQueueNet, conn))
			p_networkread.start()
			p_networkwrite.start()

			# Ask client to log in.  After this happens, the message handler thread will do the remainder of the interaction with the client
			writeQueueNet.put(dsc_send(EVL_LOGIN_INTERACTION + '3'))

		signal.pause()

	except (serial.serialutil.SerialException):
		print("Can't open port")
		
	except (KeyboardInterrupt, SystemExit):
		raise
	except:
		print("Caught exception in main: {}".format(sys.exc_info()[0]))
		raise
	finally:
		print("Terminating threads")
		if 'p_serialread' in locals(): p_serialread.terminate()
		if 'p_serialwrite' in locals(): p_serialwrite.terminate()
		if 'p_networkread' in locals(): p_networkread.terminate()
		if 'p_networkwrite' in locals(): p_networkwrite.terminate()
		if 'p_msghandler_dsc' in locals(): p_msghandler_dsc.terminate()
		if 'p_msghandler_evl' in locals(): p_msghandler_evl.terminate()
		if 'p_networkreadtest' in locals(): p_networkreadtest.terminate()

		ser.close()
		sock.shutdown(socket.SHUT_RDWR)
		sock.close()

		print("Done.")
1 Like

Thank you so much @SolidElectronics! This was such a tremendous help! I am finally up and running using the DSC IT100 USB adapter connected to a Pi2 running hassbian. The emulator works great!

I had two snags in my setup:

  1. I was getting a connect error in the logs. I checked and discovered that evl-emu.py was not running (listening). This was even after I included your line in /etc/rc.local. I worked around it and piped the output to a log file.

  2. Once the emulator was listening, I got the following error: “device reports readiness to read but returned no data.” I figured another process was already using the serial-USB adapter, so I commented out alarmdecoder in configuration.yaml

envisalink: !include envisalink.yaml
#alarmdecoder: !include alarmdecoder.yaml

Restarted both the emulator (evl-emu.py) and HA, now it’s working like a charm! Thank you, again, for sharing!

@SolidElectronics - great work on this - I thought I would reach out to see if you have updated the python script at all or if you had it on a public repo?

Thanks for the suggestion, I’d been meaning to put it in a public repo but never got around to it until now.
https://github.com/SolidElectronics/evl-emu

Since the original post I switched over to running HA inside Docker and moved this script to a standalone Pi1 but there were essentially no changes to the script other than updating a couple of IP addresses and the device name.

@SolidElectronics - awesome; thanks! I am running it the same way as well (an external PI (away from HA)).

All, I wrote up the process on how I got HA working with the IT-100 using @SolidElectronics script - works great and response time is nominal for the binary sensors as well.

2 Likes

Hi @cbschuld, I followed the instructions you posted on your site using a fresh RPi 3 B+ and a known working USB to serial converter (known working, as in it was doing the same job with openHAB, which I am moving away from).

Unfortunately, I just cannot get this working! I tried a commit from Feb (9b3eb4c), considering that was closer to when you posted your instructions (after I couldn’t get the latest working), and there is definitely more output in that version.

I can see what I assume to be serial input and output (the EVL In and EVL Out) when running the script manually, and it also shows the IP address of my hass.io instance making the connection.

The defined sensors matched up with my zones are not reporting status in Home Assistant, nor are the arm actions working - when I send an arm request, I can see that the script sees it (I get an EVL In, with a bunch of data), but nothing happens.

Was hoping you or @SolidElectronics might have some pointers?

And since I posted this, I dug a little deeper… Still not working properly, but at least I know what’s going on…

My baud rate isn’t set to the default 9600, and is instead 115200. I changed that, and then was getting responses from the panel (I assumed I was getting responses from the panel before, but looks like it was just responses from the IT100 module only).

Only problem is that it is detecting all data from the panel as having a bad checksum. To confirm if the data was really bad, I commented out the lines in the script that validate the checksum, and was finally able to see the state change in Home Assistant for the zones!

However… as soon as the scheduled blob of data comes through (I guess regular polling or something?), it wipes out the current reported status. This is the chunk of data I am talking about: -

[14:09:05] EVL Out < 36:31:35:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:30:39:43:0D:0A 61500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009C

Once that is received, all zone statuses are wiped out and they go back to being “closed” or equivalent.

Would the checksum change with the baud rate? I wouldn’t think it would make a difference, but I don’t have a great deal of experience.

Hi @ebadayon

First things first, I’d really try to get the latest version of the code working, there definitely some bugs in the older version, mostly around HA not being able to reconnect after it restarts.
The EVL Out messages are the ones being sent from the script (pretending to be a real EVL) to HA, likely in response to HA occasionally polling the status. The ‘615’ message is the EVL sending the zone timer values back to HA. A value of zero means they’re basically uninitialized (EVL has never seen the zone close). When a zone closes, the value gets set to 0xFFFA and counts down by one every 30s.
The checksum is defined in the DSC IT-100 interfacing guide, it’s definitely not supposed to be dependent on baud rate.

Can you try the latest code version and send me a full debug output and I’ll see if I can make any sense of it?

@SolidElectronics, thanks for the reply! I’ll give the latest code a go when I get back home tonight (I’m in Australia). I did try the latest version first, but that was obviously before I sorted out a couple of my other local issues.

Is there a particular or preferred way to obtain the debug output?

@SolidElectronics, when I run the latest version (after updating the baud rate and device), I get the following output only: -

Process Process-2:
Traceback (most recent call last):
  File "/usr/lib/python3.7/multiprocessing/process.py", line 297, in _bootstrap
    self.run()
  File "/usr/lib/python3.7/multiprocessing/process.py", line 99, in run
    self._target(*self._args, **self._kwargs)
  File "./evl-emu.py", line 264, in serialRead
    logger.debug ("{} DSC In  > Checksum error".format(timestamp))
NameError: name 'timestamp' is not defined

Edit: When I get rid of the .format(timestamp) off the end of the logger.debug, I get no output at all after executing the script (left for 5 minutes with nothing at all happening). There’s also no status update in Home Assistant when I deliberately trip sensors, so it doesn’t appear to actually be doing anything now.

Interestingly, I seem to be able to issue commands to the DSC panel now (I was able to arm the system), but I am not getting anything FROM it (it didn’t know it was armed, it doesn’t report zone changes, etc.)

EDIT: I have a workaround, which is basically to…

  • Set “zonedump_interval: 0” in my yaml to prevent zone status from being overwritten every 30 seconds
  • Use the initial and original commit from Feb 21 which is the only version of the script that seems to work for me!
  • Comment out the checksum verification code to allow the data to actually be received

The last item is obviously less than ideal, but I have no idea why the checksums are failing for everything when the data is obviously valid. Would be great to be able to work out what’s going on at least with that last point!