Index: /tags/0.2/S_EnOceanGateway1.xml
===================================================================
--- /tags/0.2/S_EnOceanGateway1.xml	(revision 30)
+++ /tags/0.2/S_EnOceanGateway1.xml	(revision 30)
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<scpd xmlns="urn:schemas-upnp-org:service-1-0">
+	<specVersion>
+		<major>1</major>
+		<minor>0</minor>
+	</specVersion>
+	<serviceStateTable>
+		<stateVariable>
+			<name>LearnMode</name>
+			<dataType>boolean</dataType>
+		</stateVariable>
+		<stateVariable>
+			<name>MaxID</name>
+			<dataType>ui4</dataType>
+		</stateVariable>
+	</serviceStateTable>
+	<actionList>
+		<action>
+			<name>SetLearnMode</name>
+			<argumentList>
+				<argument>
+					<name>learnMode</name>
+					<direction>in</direction>
+					<relatedStateVariable>LearnMode</relatedStateVariable>
+				</argument>
+			</argumentList>
+		</action>
+	</actionList>
+</scpd>
Index: /tags/0.2/D_EnOceanGateway1.xml
===================================================================
--- /tags/0.2/D_EnOceanGateway1.xml	(revision 30)
+++ /tags/0.2/D_EnOceanGateway1.xml	(revision 30)
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<root xmlns="urn:schemas-upnp-org:device-1-0">
+	<specVersion>
+		<major>1</major>
+		<minor>0</minor>
+	</specVersion>
+	<device>
+		<deviceType>urn:schemas-micasaverde-com:device:EnOceanGateway:1</deviceType>
+		<staticJson>D_EnOceanGateway1.json</staticJson>
+		<friendlyName>EnOcean Gateway</friendlyName>
+		<manufacturer>EnOcean</manufacturer>
+		<manufacturerURL>http://www.enocean.com</manufacturerURL>
+		<modelName>USB 300(C)</modelName>
+		<modelURL>http://www.enocean.com/en/enocean_modules/usb-300-oem/</modelURL>
+		<commUse>rs232</commUse>
+		<protocol>raw</protocol>
+		<handleChildren>1</handleChildren>
+		<serviceList>
+			<service>
+				<serviceType>urn:schemas-micasaverde-com:service:EnOceanGateway:1</serviceType>
+				<serviceId>urn:micasaverde-com:serviceId:EnOceanGateway1</serviceId>
+				<SCPDURL>S_EnOceanGateway1.xml</SCPDURL>
+			</service>
+		</serviceList>
+		<implementationList>
+			<implementationFile>I_EnOceanGateway1.xml</implementationFile>
+		</implementationList>
+	</device>
+</root>
Index: /tags/0.2/I_EnOceanGateway1.xml
===================================================================
--- /tags/0.2/I_EnOceanGateway1.xml	(revision 30)
+++ /tags/0.2/I_EnOceanGateway1.xml	(revision 30)
@@ -0,0 +1,43 @@
+<?xml version="1.0"?>
+<implementation>
+	<functions>
+		-- Sometimes the 'incoming' function is executed before the 'startup'
+		-- function has run, and therefore, before the plugin initialization.
+		-- Most of the time this happens when the protocol is 'raw',
+		-- which this plugin uses.
+		local fc = 0 -- failure counter
+
+		function enocean_startup (lul_device)
+			luup.log( "EnOcean ESP3 Gateway startup" )
+			package.loaded.L_EnOcean1 = nil
+			enoceanPlugin = require( "L_EnOceanGateway1" )
+
+			-- Callbacks
+			ClearSysMessage = enoceanPlugin.ClearSysMessage
+
+			return enoceanPlugin.Init( lul_device )
+		end
+	</functions>
+	<startup>enocean_startup</startup>
+	<incoming>
+		<lua>
+			-- If the 'startup' function hasn't run yet,
+			-- 'enoceanPlugin' and 'HandleIncoming' are not defined.
+			if not enoceanPlugin.HandleIncoming then
+				fc = fc + 1
+				luup.log( "(EnOceanPlugin::incoming) fc=".. fc )
+			else
+				enoceanPlugin.HandleIncoming( lul_data )
+			end
+		</lua>
+	</incoming>
+	<actionList>
+		<action>
+			<serviceId>urn:micasaverde-com:serviceId:EnOceanGateway1</serviceId>
+			<name>SetLearnMode</name>
+			<run>
+				enoceanPlugin.SetLearnMode( lul_settings.learnMode )
+			</run>
+		</action>
+	</actionList>
+</implementation>
Index: /tags/0.2/L_EnOceanGateway1.lua
===================================================================
--- /tags/0.2/L_EnOceanGateway1.lua	(revision 30)
+++ /tags/0.2/L_EnOceanGateway1.lua	(revision 30)
@@ -0,0 +1,1060 @@
+module("L_EnOceanGateway1", package.seeall)
+
+local bit = require("nixio").bit
+
+------------------------------------------------------------------------------------------------------------------------
+-- Flags
+------------------------------------------------------------------------------------------------------------------------
+
+local F_DEBUG_MODE          = true
+local F_LEARN_MODE          = false
+local F_WAIT_FOR_RESPONSE   = false -- If this is 'true', no further messages will be sent
+local F_SENDING_IN_PROGRESS = false
+
+------------------------------------------------------------------------------------------------------------------------
+-- Constants
+------------------------------------------------------------------------------------------------------------------------
+
+-- Services
+local SID = {
+	ENOCEAN  = "urn:micasaverde-com:serviceId:EnOceanGateway1",
+	SW_POWER = "urn:upnp-org:serviceId:SwitchPower1",
+	SECURITY = "urn:micasaverde-com:serviceId:SecuritySensor1",
+	TEMP     = "urn:upnp-org:serviceId:TemperatureSensor1",
+	HUMIDITY = "urn:micasaverde-com:serviceId:HumiditySensor1",
+	LIGHT    = "urn:micasaverde-com:serviceId:LightSensor1"
+}
+
+local DEVICE_TYPES = {
+	BINARY_LIGHT       = "urn:schemas-upnp-org:device:BinaryLight:1",
+	DOOR_SENSOR        = "urn:schemas-micasaverde-com:device:DoorSensor:1",
+	TEMPERATURE_SENSOR = "urn:schemas-micasaverde-com:device:TemperatureSensor:1",
+	HUMIDITY_SENSOR    = "urn:schemas-micasaverde-com:device:HumiditySensor:1",
+	LIGHT_SENSOR       = "urn:schemas-micasaverde-com:device:LightSensor:1",
+	MOTION_SENSOR      = "urn:schemas-micasaverde-com:device:MotionSensor:1"
+}
+
+local DEVICE_FILES = {
+	BINARY_LIGHT       = "D_BinaryLight1.xml",
+	DOOR_SENSOR        = "D_DoorSensor1.xml",
+	TEMPERATURE_SENSOR = "D_TemperatureSensor1.xml",
+	HUMIDITY_SENSOR    = "D_HumiditySensor1.xml",
+	LIGHT_SENSOR       = "D_LightSensor1.xml",
+	MOTION_SENSOR      = "D_MotionSensor1.xml"
+}
+
+-- Message types
+local SYS_MESSAGE_TYPES = {
+	BUSY    =  1,
+	ERROR   =  2,
+	SUCCESS =  4
+}
+
+-- Delays for luup.delay, in seconds
+local DELAYS = {
+	SEND_NEXT_MESSAGE = 2
+}
+
+------------------------------------------------------------------------------------------------------------------------
+
+local SYNC_BYTE = 0x55
+
+local STATE = {
+	GET_SYNC_STATE    = 0,
+	GET_HEADER_STATE  = 1,
+	CHECK_CRC8H_STATE = 2,
+	GET_DATA_STATE    = 3,
+	CHECK_CRC8D_STATE = 4
+}
+
+local PACKET_TYPES = {
+	RADIO          = 0x1,
+	RESPONSE       = 0x2,
+	EVENT          = 0x4,
+	COMMON_COMMAND = 0x5
+}
+
+local TELEGRAM_TYPES = {
+	["RPS"] = 0xF6,
+	["1BS"] = 0xD5,
+	["4BS"] = 0xA5
+}
+
+-- { offset, size }
+local LOCATIONS = {
+	["RPS_R1"]           = {  0,  3 },
+	["RPS_EB"]           = {  3,  1 },
+	["RPS_T21"]          = {  2,  1 },
+	["RPS_NU"]           = {  3,  1 },
+	["1BS_LRN"]          = {  4,  1 },
+	["1BS_CO"]           = {  7,  1 },
+	["4BS_LRN"]          = { 28,  1 },
+	["4BS_LRN_TYPE"]     = { 24,  1 },
+	["4BS_LRN_EEP_FUNC"] = {  0,  6 },
+	["4BS_LRN_EEP_TYPE"] = {  6,  7 },
+	["4BS_LRN_MANUF_ID"] = { 13, 11 },
+	["4BS_02_TMP8"]      = { 16,  8 },
+	["4BS_02_TMP10"]     = { 14, 10 },
+	["4BS_04_HUM"]       = {  8,  8 },
+	["4BS_04_TMP"]       = { 16,  8 },
+	["4BS_04_TSN"]       = { 30,  1 },
+	["4BS_06_RS"]        = { 31,  1 },
+	["4BS_06_ILL1"]      = { 16,  8 },
+	["4BS_06_ILL2"]      = {  8,  8 },
+	["4BS_07_PIRS"]      = { 16,  8 }
+}
+
+local ROCKER_BUTTONS = {
+	[0] = "AI",
+	[1] = "AO",
+	[2] = "BI",
+	[3] = "BO",
+	[4] = "CI",
+	[5] = "CO",
+	[6] = "DI",
+	[7] = "DO",
+}
+
+local CRC8_TABLE = {
+	0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15,
+	0x38, 0x3f, 0x36, 0x31, 0x24, 0x23, 0x2a, 0x2d,
+	0x70, 0x77, 0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65,
+	0x48, 0x4f, 0x46, 0x41, 0x54, 0x53, 0x5a, 0x5d,
+	0xe0, 0xe7, 0xee, 0xe9, 0xfc, 0xfb, 0xf2, 0xf5,
+	0xd8, 0xdf, 0xd6, 0xd1, 0xc4, 0xc3, 0xca, 0xcd,
+	0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b, 0x82, 0x85,
+	0xa8, 0xaf, 0xa6, 0xa1, 0xb4, 0xb3, 0xba, 0xbd,
+	0xc7, 0xc0, 0xc9, 0xce, 0xdb, 0xdc, 0xd5, 0xd2,
+	0xff, 0xf8, 0xf1, 0xf6, 0xe3, 0xe4, 0xed, 0xea,
+	0xb7, 0xb0, 0xb9, 0xbe, 0xab, 0xac, 0xa5, 0xa2,
+	0x8f, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9d, 0x9a,
+	0x27, 0x20, 0x29, 0x2e, 0x3b, 0x3c, 0x35, 0x32,
+	0x1f, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0d, 0x0a,
+	0x57, 0x50, 0x59, 0x5e, 0x4b, 0x4c, 0x45, 0x42,
+	0x6f, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7d, 0x7a,
+	0x89, 0x8e, 0x87, 0x80, 0x95, 0x92, 0x9b, 0x9c,
+	0xb1, 0xb6, 0xbf, 0xb8, 0xad, 0xaa, 0xa3, 0xa4,
+	0xf9, 0xfe, 0xf7, 0xf0, 0xe5, 0xe2, 0xeb, 0xec,
+	0xc1, 0xc6, 0xcf, 0xc8, 0xdd, 0xda, 0xd3, 0xd4,
+	0x69, 0x6e, 0x67, 0x60, 0x75, 0x72, 0x7b, 0x7c,
+	0x51, 0x56, 0x5f, 0x58, 0x4d, 0x4a, 0x43, 0x44,
+	0x19, 0x1e, 0x17, 0x10, 0x05, 0x02, 0x0b, 0x0c,
+	0x21, 0x26, 0x2f, 0x28, 0x3d, 0x3a, 0x33, 0x34,
+	0x4e, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5c, 0x5b,
+	0x76, 0x71, 0x78, 0x7f, 0x6A, 0x6d, 0x64, 0x63,
+	0x3e, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2c, 0x2b,
+	0x06, 0x01, 0x08, 0x0f, 0x1a, 0x1d, 0x14, 0x13,
+	0xae, 0xa9, 0xa0, 0xa7, 0xb2, 0xb5, 0xbc, 0xbb,
+	0x96, 0x91, 0x98, 0x9f, 0x8a, 0x8D, 0x84, 0x83,
+	0xde, 0xd9, 0xd0, 0xd7, 0xc2, 0xc5, 0xcc, 0xcb,
+	0xe6, 0xe1, 0xe8, 0xef, 0xfa, 0xfd, 0xf4, 0xf3
+}
+
+------------------------------------------------------------------------------------------------------------------------
+-- Globals
+------------------------------------------------------------------------------------------------------------------------
+
+local g_parentDevice        -- The device # of the parent device
+local g_ptr                 -- Pointer used to create devices
+
+local g_taskHandle = -1     -- Handle for the system messages
+local g_lastSysMessage = 0  -- Timestamp of the last status message
+
+local g_messageQueue = {}   -- The outbound message queue
+
+local g_childDevices = {}   -- The list of all our child devices
+local g_MaxId = 0           -- A number that increments with every device learned.
+
+local g_knownSenderIds = {} -- The list of the newly learned EnOcean devices while in Learn Mode.
+local g_newDevices = {      -- The list of the newly appended Luup devices while in Learn Mode.
+	-- altid
+	-- description
+	-- deviceType
+	-- deviceFile
+	-- implementationFile
+	-- parameters
+}
+
+------------------------------------------------------------------------------------------------------------------------
+-- Generic utilities
+------------------------------------------------------------------------------------------------------------------------
+
+local log      = luup.log
+-- Get the 'getinfo' function from the 'debug' module before overwriting the 'debug' keyword.
+local getinfo  = debug.getinfo
+local function debug() end
+
+
+local function ShowSysMessage (message, mode, critical)
+	mode = mode or SYS_MESSAGE_TYPES.BUSY
+	critical = critical or false
+	log( "(EnOceanPlugin::ShowSysMessage-"..(getinfo(2).name or "N/A")..") mode: ".. mode
+		..", critical: ".. tostring( critical ) ..", message: ".. message )
+
+	luup.task( message, mode, "EnOcean Plugin", g_taskHandle )
+	g_lastSysMessage = tostring( os.time() )
+
+	if not critical then
+		-- Clear the previous system message, since it's transient.
+		luup.call_delay( "ClearSysMessage", 30, g_lastSysMessage )
+	else
+		luup.set_failure( true, g_parentDevice )
+	end
+end
+
+
+function ClearSysMessage (messageTime)
+	-- 'messageTime' is nil if the function is called by the user.
+	if (messageTime == g_lastSysMessage) or (messageTime == nil) then
+		luup.task( "Clearing...", SYS_MESSAGE_TYPES.SUCCESS, "EnOcean Plugin", g_taskHandle )
+	end
+end
+
+
+local function PadLeft (s, length, c)
+	s = tostring(s)
+	length = length or 2
+	c = c or "0"
+	return c:rep( length - #s ) .. s
+end
+
+
+-- Formats a number or character as hex.
+local function ToHex (n)
+	if type(n) == "number" then
+		return string.format( "%02X", n )
+	end
+end
+
+
+-- Check if a table contains the given item.
+-- Returns true and the key/index of the item if found,
+-- or false if not found.
+local function TableContains (t, item)
+	for k, v in pairs(t) do
+		if v == item then
+			return true, k
+		end
+	end
+	return false
+end
+
+
+-- Extracts a subtable from the given table.
+local function ExtractSubtable (t, start, length)
+	if start < 0 then
+		start = #t + start + 1
+	end
+	length = length or (#t - start + 1)
+
+	local t1 = {}
+	for i = start, start + length - 1 do
+		t1[#t1 + 1] = t[i]
+	end
+	return t1
+end
+
+
+-- Concatenates a table of numbers into a string separated by the given separator.
+local function ConcatTableHex( t, sep, start, length )
+	sep = sep or "-"
+	start = start or 1
+	if start < 0 then
+		start = #t + start + 1
+	end
+	length = length or (#t - start + 1)
+	local s = ToHex( t[start] )
+	if length > 1 then
+		for i = start + 1, start + length - 1 do
+			s = s .. sep .. ToHex( t[i] )
+		end
+	end
+	return s
+end
+
+
+-- Set "class".
+local set = {
+	add = function (s, item)
+		if not TableContains( s, item ) then
+			table.insert( s, item )
+		end
+	end,
+	remove = function (s, item)
+		local itemExists, pos = TableContains( t, item )
+		if itemExists then
+			table.remove( t, pos )
+		end
+	end,
+	concat = table.concat
+}
+
+
+------------------------------------------------------------------------------------------------------------------------
+-- Misc plugin functions
+------------------------------------------------------------------------------------------------------------------------
+
+local function ComputeCrc (data, start, length)
+	start = start or 1
+	length = length or #data
+
+	local crc = 0
+	for i = start, start + length - 1 do
+		crc = CRC8_TABLE[bit.bxor( crc, data[i] ) + 1]
+	end
+	debug( "(EnOceanPlugin::ComputeCrc) crc=".. ToHex( crc ))
+	return crc
+end
+
+
+-- Returns the numerical value made of the 'size' number of bits starting at 'offset'.
+-- The offset starts from the last bit of the last byte. The bit at the smallest offset is the MSB.
+-- The data is a table where the first byte (according to the EnOcean docs) is on the last position in the table.
+-- So, for 4 bytes: data[1]=byte3, data[2]=byte2, data[3]=byte1, data[4]=byte0
+local function GetValueAtLocation (data, location)
+	local offset = location[1]
+	local size   = location[2]
+	local value  = 0 -- The return value
+
+	local startingOffset = offset
+	local maxOffset = #data * 8 - 1
+
+	local inverseOffset
+	local byteIndex
+	local bitIndex
+	for offset = startingOffset, startingOffset + size - 1 do
+		inverseOffset = maxOffset - offset
+		byteIndex = math.floor( offset / 8 ) + 1
+		bitIndex = inverseOffset % 8
+		value = bit.lshift( value, 1 )
+		if bit.check( data[byteIndex], 2 ^ bitIndex ) then
+			value = bit.set( value, 1 )
+		end
+	end
+
+	return value
+end
+
+
+local function IsLearned (senderId)
+	senderId = senderId:gsub( "%-", "%%%-" ) -- Escape '-' because it's a magic character.
+	for altid in pairs( g_childDevices ) do
+		if altid:find( "^".. senderId ) then
+			return true
+		end
+	end
+	return false
+end
+
+
+local function GetEepFuncAndType (senderId)
+	senderId = senderId:gsub( "%-", "%%%-" ) -- Escape '-' because it's a magic character.
+	for altid in pairs( g_childDevices ) do
+		if altid:find( "^".. senderId ) then
+			local eepFunc, eepType = altid:match( "^".. senderId .."%-(%x%x)%-(%x%x)" )
+			if eepFunc and eepType then
+				return tonumber( eepFunc, 16 ), tonumber( eepType, 16 )
+			end
+		end
+	end
+end
+
+
+------------------------------------------------------------------------------------------------------------------------
+-- Radio telegram handlers
+------------------------------------------------------------------------------------------------------------------------
+
+local RADIO_TELEGRAM_HANDLERS = {
+
+	--------------------------------------------------------------------------------------------------------------------
+	-- RPS
+	--------------------------------------------------------------------------------------------------------------------
+	[TELEGRAM_TYPES["RPS"]] = function (senderId, data, status)
+		debug( "(EnOceanPlugin::RPS) RPS telegram handler" )
+
+		local firstAction = GetValueAtLocation( data, LOCATIONS["RPS_R1"] )
+		local altid = senderId ..'-'.. ROCKER_BUTTONS[firstAction]:sub( 1, 1 )
+		debug( "(EnOceanPlugin::RPS) altid: ".. altid )
+
+		--------------------------------------------------------------------------------------------
+		-- The device is learned and it's not a "number of buttons pressed" telegram.
+		--------------------------------------------------------------------------------------------
+		if g_childDevices[altid] then
+			log( "(EnOceanPlugin::RPS) Device known: ".. g_childDevices[altid] )
+			-- Ignore "number of buttons pressed" telegrams.
+			if GetValueAtLocation( { status }, LOCATIONS["RPS_NU"] ) == 0 then
+				debug( "(EnOceanPlugin::RPS) 'Number of buttons pressed' telegram. Return" )
+				return
+			end
+			-- Ignore "button released" telegrams.
+			if GetValueAtLocation( data, LOCATIONS["RPS_EB"] ) == 0 then
+				debug( "(EnOceanPlugin::RPS) 'Button released' telegram. Return" )
+				return
+			end
+			local pressed = ( ROCKER_BUTTONS[firstAction]:sub( 2, 2 ) == "I" ) and "1" or "0"
+			luup.variable_set( SID.SW_POWER, "Status", pressed, g_childDevices[altid] )
+
+		--------------------------------------------------------------------------------------------
+		-- The device is not learned, and we're in Learn Mode.
+		-- RPS devices don't have Teach-in telegrams.
+		--------------------------------------------------------------------------------------------
+		elseif F_LEARN_MODE and not TableContains( g_knownSenderIds, senderId ) then
+			-- We don't want to add the same device twice.
+			set.add( g_knownSenderIds, senderId )
+			g_MaxId = g_MaxId + 1
+
+			-- Find the device type. If T21 is 1, this is a 2 rocker switch, otherwise it's a 4 rocker switch.
+			local t21 = GetValueAtLocation( { status }, LOCATIONS["RPS_T21"] )
+			local numRockers = (t21 == 1) and 2 or 4
+
+			-- Create the devices.
+			log( "(EnOceanPlugin::RPS) T21: ".. t21 ..", num rockers: ".. numRockers )
+			ShowSysMessage( "Found ".. numRockers .." Rocker Switch (EO-".. PadLeft( g_MaxId ) ..")" )
+			-- Create a switch for each rocker.
+			local deviceName
+			local parameters = SID.SW_POWER ..",Status=0"
+			for i = 0, numRockers - 1 do
+				-- Get only the button, ignore the state.
+				local button = ROCKER_BUTTONS[2 * i]:sub( 1, 1 )
+				altid = senderId ..'-'.. button
+				deviceName = "(EO-".. PadLeft( g_MaxId ) ..") Button ".. button
+				log(  "(EnOceanPlugin::RPS) New device: ".. deviceName  )
+				table.insert( g_newDevices, {
+					["altid"]      = altid,
+					["deviceName"] = deviceName,
+					["deviceType"] = DEVICE_TYPES.BINARY_LIGHT,
+					["deviceFile"] = DEVICE_FILES.BINARY_LIGHT,
+					["implementationFile"] = "",
+					["parameters"] = parameters
+				} )
+			end
+		else
+			log( "(EnOceanPlugin::RPS) Unkown device" )
+		end
+	end,
+
+	--------------------------------------------------------------------------------------------------------------------
+	-- 1BS
+	--------------------------------------------------------------------------------------------------------------------
+	[TELEGRAM_TYPES["1BS"]] = function (senderId, data, status)
+		debug(  "(EnOceanPlugin::1BS) 1BS telegram handler"  )
+
+		local isDataTelegram = GetValueAtLocation( data, LOCATIONS["1BS_LRN"] ) == 1
+		debug(  "(EnOceanPlugin::1BS) ".. (isDataTelegram and "Data" or "Teach-in") .." telegram"  )
+
+		--------------------------------------------------------------------------------------------
+		-- The device is learned and this is a Data telegram.
+		--------------------------------------------------------------------------------------------
+		if g_childDevices[senderId] and isDataTelegram then
+			log(  "(EnOceanPlugin::1BS) Device known: ".. g_childDevices[senderId]  )
+			local tripped = 1 - GetValueAtLocation( data, LOCATIONS["1BS_CO"] )
+			luup.variable_set( SID.SECURITY, "Tripped", tripped, g_childDevices[senderId] )
+
+		--------------------------------------------------------------------------------------------
+		-- The device is not learned, and we're in Learn Mode.
+		-- Only learn the device if this is a Teach-in telegram, even if it doesn't contain any useful information.
+		-- We don't want to accidentaly learn devices that the user doesn't want.
+		--------------------------------------------------------------------------------------------
+		elseif F_LEARN_MODE and not isDataTelegram and not g_childDevices[senderId] and not TableContains( g_knownSenderIds, senderId ) then
+			-- We don't want to add the same device twice.
+			set.add( g_knownSenderIds, senderId )
+			g_MaxId = g_MaxId + 1
+			log( "(EnOceanPlugin::1BS) New device: ".. deviceName  )
+			ShowSysMessage( "Found Single Input Contact (EO-".. PadLeft( g_MaxId ) ..")" )
+			deviceName = "(EO-".. PadLeft( g_MaxId ) ..") Single Input Contact"
+			table.insert( g_newDevices, {
+				["altid"]      = senderId,
+				["deviceName"] = deviceName,
+				["deviceType"] = DEVICE_TYPES.DOOR_SENSOR,
+				["deviceFile"] = DEVICE_FILES.DOOR_SENSOR,
+				["implementationFile"] = "",
+				["parameters"] = SID.SECURITY ..",Tripped=0\n".. SID.SECURITY ..",Armed=0"
+			} )
+		else
+			log( "(EnOceanPlugin::1BS) Unkown device or Teach-in telegram" )
+		end
+	end,
+
+	--------------------------------------------------------------------------------------------------------------------
+	-- 4BS
+	--------------------------------------------------------------------------------------------------------------------
+	[TELEGRAM_TYPES["4BS"]] = function (senderId, data, status)
+		debug(  "(EnOceanPlugin::4BS) 4BS telegram handler"  )
+
+		local isDataTelegram = GetValueAtLocation( data, LOCATIONS["4BS_LRN"] ) == 1
+		debug(  "(EnOceanPlugin::4BS) ".. (isDataTelegram and "Data" or "Teach-in") .." telegram"  )
+
+		--------------------------------------------------------------------------------------------
+		-- The device is learned and it's handled, and this is a Data telegram.
+		--------------------------------------------------------------------------------------------
+		if IsLearned( senderId ) and isDataTelegram then
+			local eepFunc, eepType = GetEepFuncAndType( senderId )
+			local altid = senderId .."-".. ToHex( eepFunc ) .."-".. ToHex( eepType )
+			debug( "(EnOceanPlugin::4BS) altid: ".. altid )
+			--
+			-- Temperature sensor
+			--
+			if eepFunc == 0x02 then
+				if not g_childDevices[altid] then
+					log( "(EnOceanPlugin::4BS) Device not known. Return" )
+					return
+				end
+				log( "(EnOceanPlugin::4BS) Device known: ".. g_childDevices[altid] )
+
+				local sensorType = math.floor( eepType / 0x10 )
+				-- Sensor types:
+				-- 0 :  8 bit : 40 degrees range
+				-- 1 :  8 bit : 80 degrees range
+				-- 2 : 10 bit : -10...+41.2 degrees
+				-- 3 : 10 bit : -40...+62.3 degrees
+				local temp
+				if sensorType == 0 then
+					local minTemp = -50 + 10 * (eepType % 0x10)
+					local value = GetValueAtLocation( data, LOCATIONS["4BS_02_TMP8"] ) -- 255...0
+					local temp = math.floor( minTemp + (255 - value) * 40 / 255 ) -- minTemp..minTemp+40
+				elseif sensorType == 1 then
+					local minTemp = -60 + 10 * (eepType % 0x10)
+					local value = GetValueAtLocation( data, LOCATIONS["4BS_02_TMP8"] ) -- 255...0
+					local temp = math.floor( minTemp + (255 - value) * 80 / 255 ) -- minTemp...minTemp+80
+				elseif sensorType == 2 then
+					local minTemp = -10
+					local maxTemp = 41.2
+					local value = GetValueAtLocation( data, LOCATIONS["4BS_02_TMP10"] ) -- 1023...0
+					local temp = math.floor( minTemp + (1023 - value) * (maxTemp - minTemp) / 1023 ) -- minTemp...maxTemp
+				elseif sensorType == 3 then
+					local minTemp = -40
+					local maxTemp = 62.3
+					local value = GetValueAtLocation( data, LOCATIONS["4BS_02_TMP10"] ) -- 1023...0
+					local temp = math.floor( minTemp + (1023 - value) * (maxTemp - minTemp) / 1023 ) -- minTemp...maxTemp
+				end
+				if temp then
+					luup.variable_set( SID.TEMP, "CurrentTemperature", temp, g_childDevices[altid] )
+				end
+			--
+			-- Humidity and temperature sensors
+			--
+			elseif eepFunc == 0x04 then
+				local altidHum = altid .."-hum"
+				local altidTemp = altid .."-temp"
+				if not g_childDevices[altidHum] then
+					log( "(EnOceanPlugin::4BS) Device not known. Return" )
+					return
+				end
+				log( "(EnOceanPlugin::4BS) Device known: hum ".. g_childDevices[altidHum] ..", temp "..( g_childDevices[altidTemp] or "NONE" ))
+
+				-- Get the humidity.
+				local hum = GetValueAtLocation( data, LOCATIONS["4BS_04_HUM"] ) -- 0...250
+				hum = math.floor( hum * 100 / 250 ) -- 0...100
+				luup.variable_set( SID.HUMIDITY, "CurrentLevel", hum, g_childDevices[altidHum] )
+				-- Check if the temperature sensor is available, and get the temperature if it is.
+				local tempSensorAvailable = GetValueAtLocation( data, LOCATIONS["4BS_04_TSN"] )
+				if tempSensorAvailable == 1 then
+					local temp = GetValueAtLocation( data, LOCATIONS["4BS_04_TMP"] ) -- 0...250
+					temp = math.floor( temp * 40 / 250 ) -- 0...40
+					luup.variable_set( SID.TEMP, "CurrentTemperature", temp, g_childDevices[altidTemp] )
+				end
+			--
+			-- Light sensor
+			--
+			elseif eepFunc == 0x06 then
+				if not g_childDevices[altid] then
+					log( "(EnOceanPlugin::4BS) Device not known. Return" )
+					return
+				end
+				log( "(EnOceanPlugin::4BS) Device known: ".. g_childDevices[altid] )
+
+				local range = GetValueAtLocation( data, LOCATIONS["4BS_06_RS"] )
+				local location = (range == 0) and LOCATIONS["4BS_06_ILL1"] or LOCATIONS["4BS_06_ILL2"]
+				local lux = GetValueAtLocation( data, location ) -- 0...255
+				-- Light sensor types:
+				-- 1 : 600...60000 / 300...30000 lux
+				-- 2 :   0... 1020 /   0...  510 lux
+				local minLux = 0
+				local maxLux = 0
+				if eepType == 0x01 then
+					minLux = (range == 0) and 600 or 300
+					maxLux = (range == 0) and 60000 or 30000
+				elseif eepType == 0x02 then
+					maxLux = (range == 0) and 1020 or 510
+				end
+				lux = math.floor( minLux + lux * (maxLux - minLux) / 255 ) -- minLux...maxLux
+				luup.variable_set( SID.TEMP, "CurrentLevel", lux, g_childDevices[altid] )
+			--
+			-- Occupancy sensor
+			--
+			elseif eepFunc == 0x07 then
+				if not g_childDevices[altid] then
+					log( "(EnOceanPlugin::4BS) Device not known. Return" )
+					return
+				end
+				log( "(EnOceanPlugin::4BS) Device known: ".. g_childDevices[altid] )
+
+				local tripped = GetValueAtLocation( data, LOCATIONS["4BS_07_PIRS"] ) -- 0...255
+				tripped = (tripped > 127) and 1 or 0
+				luup.variable_set( SID.SECURITY, "Tripped", tripped, g_childDevices[altid] )
+			end
+		--------------------------------------------------------------------------------------------
+		-- The device is not learned, we're in Learn Mode, and we received a Teach-in telegram.
+		--------------------------------------------------------------------------------------------
+		elseif F_LEARN_MODE and not isDataTelegram and not g_childDevices[senderId] and not TableContains( g_knownSenderIds, senderId ) then
+			local lrnType = GetValueAtLocation( data, LOCATIONS["4BS_LRN_TYPE"] )
+			if lrnType ~= 1 and lrnType ~= 2 then
+				log( "(EnOceanPlugin::4BS) Teach-in telegram variation 1, discard it" )
+				return
+			end
+
+			-- We don't want to add the same device twice.
+			set.add( g_knownSenderIds, senderId )
+			g_MaxId = g_MaxId + 1
+
+			local eepFunc = GetValueAtLocation( data, LOCATIONS["4BS_LRN_EEP_FUNC"] )
+			local eepType = GetValueAtLocation( data, LOCATIONS["4BS_LRN_EEP_TYPE"] )
+			local manufId = GetValueAtLocation( data, LOCATIONS["4BS_LRN_MANUF_ID"] )
+
+			log( "(EnOceanPlugin::4BS) Func ".. ToHex( eepFunc ) ..", Type ".. ToHex( eepType ))
+			senderId = senderId .."-".. ToHex( eepFunc ) .."-".. ToHex( eepType )
+
+			--
+			-- Temperature sensor
+			--
+			if eepFunc == 0x02 then
+				local deviceName = "(EO-".. PadLeft( g_MaxId ) ..") Temperature Sensor"
+				log( "(EnOceanPlugin::4BS) New device: ".. deviceName )
+				ShowSysMessage( "Found Temperature sensor (EO-".. PadLeft( g_MaxId ) ..")" )
+				table.insert( g_newDevices, {
+					["altid"]      = senderId,
+					["deviceName"] = deviceName,
+					["deviceType"] = DEVICE_TYPES.TEMPERATURE_SENSOR,
+					["deviceFile"] = DEVICE_FILES.TEMPERATURE_SENSOR,
+					["implementationFile"] = "",
+					["parameters"] = SID.TEMP ..",CurrentTemperature=0"
+				} )
+			--
+			-- Temperature and humidity sensors
+			--
+			elseif eepFunc == 0x04 then
+				ShowSysMessage( "Found Temperature and Humidity sensor (EO-".. PadLeft( g_MaxId ) ..")" )
+				-- Create the humidity sensor.
+				deviceName = "(EO-".. PadLeft( g_MaxId ) ..") Humidity Sensor"
+				log( "(EnOceanPlugin::4BS) New device: ".. deviceName )
+				table.insert( g_newDevices, {
+					["altid"]      = senderId .."-hum",
+					["deviceName"] = deviceName,
+					["deviceType"] = DEVICE_TYPES.HUMIDITY_SENSOR,
+					["deviceFile"] = DEVICE_FILES.HUMIDITY_SENSOR,
+					["implementationFile"] = "",
+					["parameters"] = SID.HUMIDITY ..",CurrentLevel=0"
+				} )
+				-- Create the temperature sensor.
+				local deviceName = "(EO-".. PadLeft( g_MaxId ) ..") Temperature Sensor"
+				log( "(EnOceanPlugin::4BS) New device: ".. deviceName )
+				table.insert( g_newDevices, {
+					["altid"]      = senderId .."-temp",
+					["deviceName"] = deviceName,
+					["deviceType"] = DEVICE_TYPES.TEMPERATURE_SENSOR,
+					["deviceFile"] = DEVICE_FILES.TEMPERATURE_SENSOR,
+					["implementationFile"] = "",
+					["parameters"] = SID.TEMP ..",CurrentTemperature=0"
+				} )
+			--
+			-- Light sensor
+			--
+			elseif eepFunc == 0x06 then
+				local deviceName = "(EO-".. PadLeft( g_MaxId ) ..") Light Sensor"
+				log( "(EnOceanPlugin::4BS) New device: ".. deviceName )
+				ShowSysMessage( "Found Light sensor (EO-".. PadLeft( g_MaxId ) ..")" )
+				table.insert( g_newDevices, {
+					["altid"]      = senderId,
+					["deviceName"] = deviceName,
+					["deviceType"] = DEVICE_TYPES.LIGHT_SENSOR,
+					["deviceFile"] = DEVICE_FILES.LIGHT_SENSOR,
+					["implementationFile"] = "",
+					["parameters"] = SID.LIGHT ..",CurrentLevel=0"
+				} )
+			--
+			-- Occupancy sensor
+			--
+			elseif eepFunc == 0x07 then
+				local deviceName = "(EO-".. PadLeft( g_MaxId ) ..") Occupancy Sensor"
+				log( "(EnOceanPlugin::4BS) New device: ".. deviceName )
+				ShowSysMessage( "Found Occupancy sensor (EO-".. PadLeft( g_MaxId ) ..")" )
+				table.insert( g_newDevices, {
+					["altid"]      = senderId,
+					["deviceName"] = deviceName,
+					["deviceType"] = DEVICE_TYPES.MOTION_SENSOR,
+					["deviceFile"] = DEVICE_FILES.MOTION_SENSOR,
+					["implementationFile"] = "",
+					["parameters"] = SID.SECURITY ..",Tripped=0\n".. SID.SECURITY ..",Armed=0"
+				} )
+			else
+				log( "(EnOceanPlugin::4BS) Found unknown device" )
+				ShowSysMessage( "Found unknown device A5-".. ToHex( eepFunc ) .."-".. ToHex( eepType ) )
+			end
+		else
+			log( "(EnOceanPlugin::4BS) Unkown device or Teach-in telegram" )
+		end
+	end
+}
+
+
+------------------------------------------------------------------------------------------------------------------------
+-- Packet handler functions
+------------------------------------------------------------------------------------------------------------------------
+
+local PACKET_HANDLERS = {
+
+	--------------------------------------------------------------------------------------------------------------------
+	-- RADIO
+	--------------------------------------------------------------------------------------------------------------------
+	[PACKET_TYPES.RADIO] = function (data, optData)
+		local radioTelegramType = data[1]
+		local senderId = ConcatTableHex( data, '-', -5, 4 )
+		local status = data[#data]
+		data = ExtractSubtable( data, 2, #data - 6 ) -- Get only the user data.
+		log( "(EnOceanPlugin::RADIO) Handle radio telegram type: ".. ToHex( radioTelegramType )
+			.. ", senderId: ".. senderId
+			.. ", status: ".. ToHex( status )
+			.. ", data: ".. ConcatTableHex( data, '-' ))
+
+		if RADIO_TELEGRAM_HANDLERS[radioTelegramType] then
+			RADIO_TELEGRAM_HANDLERS[radioTelegramType]( senderId, data, status )
+		else
+			log( "(EnOceanPlugin::RADIO) Unhandled radio telegram type ".. ToHex( radioTelegramType ))
+		end
+	end,
+
+	--------------------------------------------------------------------------------------------------------------------
+	-- RESPONSE
+	--------------------------------------------------------------------------------------------------------------------
+	[PACKET_TYPES.RESPONSE] = function (data, optData)
+		debug( "(EnOceanPlugin::RESPONSE) Handle response not implemented" )
+	end
+}
+
+
+------------------------------------------------------------------------------------------------------------------------
+-- Handle incoming data
+------------------------------------------------------------------------------------------------------------------------
+
+-- 4BS
+-- 0x55 | 0x0 0xa 0x7 0x1 | 0xeb | 0xa5 - 0xce 0x79 0x0 0xf - 0x0 0x81 0x1 0x75 - 0x0 | 0x1 0xff 0xff 0xff 0xff 0x44 0x0 | 0x6e
+-- 0x55 | 0x0 0xa 0x7 0x1 | 0xeb | 0xa5 - 0xc6 0x79 0x0 0xf - 0x0 0x81 0x1 0x75 - 0x0 | 0x1 0xff 0xff 0xff 0xff 0x43 0x0 | 0x15
+-- 0x55 | 0x0 0xa 0x7 0x1 | 0xeb | 0xa5 - 0xc6 0x79 0x0 0xf - 0x0 0x81 0x1 0x75 - 0x0 | 0x1 0xff 0xff 0xff 0xff 0x43 0x0 | 0x15
+
+-- 1BS
+-- 0x55 | 0x0 0x7 0x7 0x1 | 0x7a | 0xd5 - 0x8 - 0x0 0x1 0x8a 0xbb - 0x0 | 0x1 0xff 0xff 0xff 0xff 0x3a 0x0 | 0xbc
+-- 0x55 | 0x0 0x7 0x7 0x1 | 0x7a | 0xd5 - 0x8 - 0x0 0x0 0x85 0xe - 0x0 | 0x1 0xff 0xff 0xff 0xff 0x37 0x0 | 0x3a
+
+local state        = STATE.GET_SYNC_STATE -- The current state
+local rxBuf        = {} -- The received data buffer
+local rxCount      = 0  -- The number of bytes in the data buffer
+local rxDataLen    = 0  -- The length of the data field
+local rxOptDataLen = 0  -- The length of the optional data field
+local rxPacketType = 0  -- The packet type
+
+-- State machine to load incoming packet bytes
+local STATE_MACHINE = {
+
+	-- Waiting for packet sync byte 0x55
+	[STATE.GET_SYNC_STATE] = function (rxByte)
+		if rxByte == SYNC_BYTE then
+			--debug( "(EnOceanPlugin::GET_SYNC_STATE) Got sync byte, go to GET_HEADER_STATE" )
+			state = STATE.GET_HEADER_STATE
+			crc   = 0
+			rxCount = 0
+		end
+	end,
+
+	-- Read the header bytes.
+	[STATE.GET_HEADER_STATE] = function (rxByte)
+		-- Add the received byte in the buffer.
+		rxCount = rxCount + 1
+		rxBuf[rxCount] = rxByte
+		-- All the header bytes received?
+		if rxCount == 4 then
+			--debug( "(EnOceanPlugin::GET_HEADER_STATE) Received all header bytes, go to CHECK_CRC8H_STATE" )
+			state = STATE.CHECK_CRC8H_STATE
+		end
+	end,
+
+	-- Check the header checksum & try to resynchronize if an error occurred.
+	[STATE.CHECK_CRC8H_STATE] = function (rxByte)
+		-- Header CRC correct?
+        if ComputeCrc( rxBuf, 1, rxCount ) ~= rxByte then
+			--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Incorrect header CRC" )
+			-- No. Check if there is a sync byte (0x55) in the header.
+			local pos = -1
+			for i = 1, 4 do
+				if rxBuf[i] == SYNC_BYTE then
+					-- Indicates the position of the sync byte
+					pos = i
+					break
+				end
+			end
+
+			if pos == -1 and rxByte ~= SYNC_BYTE then
+				-- Neither the header nor the CRC8H contain the sync byte
+				--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Neither the header nor the CRC8H contain the sync byte, go to GET_SYNC_STATE" )
+				state = STATE.GET_SYNC_STATE
+				return
+			elseif pos == -1 and rxByte == SYNC_BYTE then
+				-- The header does not contain the sync byte but the CRC8H does
+				-- The sync byte could be the beginning of a packet
+				--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) The header does not contain the sync byte but the CRC8H does, go to GET_HEADER_STATE" )
+				state = STATE.GET_HEADER_STATE
+				rxCount = 0
+				return
+			end
+
+			-- The header contains the sync byte. It could be a new packed.
+			-- Shift all the bytes from the 0x55 byte in the buffer.
+			-- Recalculate CRC8 for those bytes.
+			for i = 1, 4 - pos do
+				rxBuf[i] = rxBuf[pos + i]
+			end
+			rxCount = 4 - pos
+
+			-- Copy the received byte to buffer
+			rxCount = rxCount + 1
+			rxBuf[rxCount] = rxByte
+
+			if rxCount < 4 then
+				--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) There are still header bytes to receive, go to GET_HEADER_STATE" )
+				state = STATE.GET_HEADER_STATE
+			end
+
+			--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) All header bytes received, stay in CHECK_CRC8H_STATE" )
+			return
+        end
+
+		-- CRC8H correct. Are the Length fields values valid?
+		rxDataLen = rxBuf[1] * 0x100 + rxBuf[2]
+		rxOptDataLen = rxBuf[3]
+		--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Header CRC correct, rxDataLen=".. rxDataLen ..", rxOptDataLen=".. rxOptDataLen )
+		if (rxDataLen + rxOptDataLen) == 0 then
+			-- No. Sync byte received?
+			if rxByte == SYNC_BYTE then
+				-- Yes
+				--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Header CRC incorrect, sync byte received, go to GET_HEADER_STATE" )
+				state = STATE.GET_HEADER_STATE
+				rxCount = 0
+				return
+			end
+
+			-- Packet with correct CRC8H, but wrong length fields
+			--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Packet with correct CRC8H, but wrong length fields, go to GET_SYNC_STATE" )
+			state = STATE.GET_SYNC_STATE
+			return
+		end
+
+		-- Correct header CRC8. Go to the reception of data.
+		--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Correct header CRC8, go to GET_DATA_STATE" )
+		state = STATE.GET_DATA_STATE
+		rxCount = 0
+		rxPacketType = rxBuf[4]
+	end,
+
+	-- Copy the information bytes.
+	[STATE.GET_DATA_STATE] = function (rxByte)
+		rxCount = rxCount + 1
+		rxBuf[rxCount] = rxByte
+
+		-- When all the expected bytes are received, go to calculate data checksum.
+		if rxCount == (rxDataLen + rxOptDataLen) then
+			--debug( "(EnOceanPlugin::GET_DATA_STATE) All expected bytes received, go to CHECK_CRC8D_STATE" )
+			state = STATE.CHECK_CRC8D_STATE
+		end
+	end,
+
+	-- Check the data CRC8
+	[STATE.CHECK_CRC8D_STATE] = function (rxByte)
+		-- In all cases the state returns to the first state: waiting for next sync byte.
+		state = STATE.GET_SYNC_STATE
+
+		if ComputeCrc( rxBuf, 1, rxCount ) == rxByte then
+			-- Correct packet received
+			--debug( "(EnOceanPlugin::CHECK_CRC8D_STATE) Correct packet received, handle it and go to GET_SYNC_STATE" )
+			if PACKET_HANDLERS[rxPacketType] then
+				PACKET_HANDLERS[rxPacketType]( ExtractSubtable( rxBuf, 1, rxDataLen ), ExtractSubtable( rxBuf, rxDataLen + 1, rxOptDataLen ))
+			else
+				log( "(EnOceanPlugin::CHECK_CRC8D_STATE) Unhandled packet type ".. ToHex( rxPacketType ))
+			end
+			return
+		end
+
+		-- Incorrect CRC8
+		-- If the received byte is the sync byte, then it could be the sync byte for next packet.
+		if rxByte == SYNC_BYTE then
+			--debug( "(EnOceanPlugin::CHECK_CRC8D_STATE) Incorrect CRC8, the received byte is the sync byte, go to GET_HEADER_STATE" )
+			state = STATE.GET_HEADER_STATE
+			rxCount = 0
+		end
+
+		log( "(EnOceanPlugin::CHECK_CRC8D_STATE) Error: incorrect data checksum" )
+	end
+}
+
+
+function HandleIncoming (lul_data)
+	local rxByte = string.byte( lul_data )
+	--debug( "(EnOceanPlugin::HandleIncoming) state ".. state ..", rxByte=".. ToHex( rxByte ))
+	if STATE_MACHINE[state] then
+		STATE_MACHINE[state]( rxByte )
+	else
+		-- Shouldn't get here.
+		-- Wait for the sync byte.
+		state = STATE.GET_SYNC_STATE
+	end
+end
+
+
+------------------------------------------------------------------------------------------------------------------------
+-- Message sending functions
+------------------------------------------------------------------------------------------------------------------------
+
+local function AddToSendQueue (packetType, data, optData, waitForResponse)
+	local insert = table.insert
+	local char   = string.char
+
+	local packet = { SYNC_BYTE }              -- The sync byte
+
+	insert( packet, char( #data / 0x100 ))    -- First byte of the data length
+	insert( packet, char( #data % 0x100 ))    -- Seconds byte of the data length
+	insert( packet, char( #optData ))         -- Length of the optional data
+	insert( packet, packetType )              -- The packet type
+	insert( packet, ComputeCrc(packet, 2, 4)) -- The header CRC8
+
+	insert( packet, data )                    -- The user data
+	insert( packet, optData )                 -- The optional user data
+	insert( packet, ComputeCrc( packet, 7, #data + #optData )) -- The data CRC8
+
+	table.insert( g_messageQueue, {content = table.concat(packet), waitForResponse = (waitForResponse or false)} )
+	if not F_SENDING_IN_PROGRESS then
+		SendPacket()
+	end
+end
+
+
+function SendPacket()
+	F_SENDING_IN_PROGRESS = true
+
+	if F_WAIT_FOR_RESPONSE then
+		g_triesCount = g_triesCount + 1
+		if g_triesCount < 3 then
+			debug( "(EnOceanPlugin::SendPacket) F_WAIT_FOR_RESPONSE flag is set, g_triesCount=".. g_triesCount .."; try again in ".. DELAYS.SEND_NEXT_MESSAGE .." seconds." )
+			luup.call_delay( "SendPacket", DELAYS.SEND_NEXT_MESSAGE )
+			return
+		else
+			log( "(EnOceanPlugin::SendPacket) Didn't receive response for the last message. Stop waiting for response." )
+			F_WAIT_FOR_RESPONSE = false
+			g_triesCount = 0
+		end
+	end
+
+	local message
+	if #g_messageQueue > 0 then
+		message = g_messageQueue[1]
+	else
+		return
+	end
+
+	debug( "(EnOceanPlugin::SendPacket) Wait for response: ".. message.waitForResponse )
+
+	if not luup.io.write( message.content ) then
+		ShowSysMessage( "Failed to send message", SYS_MESSAGE_TYPES.ERROR )
+		return
+	end
+
+	F_WAIT_FOR_RESPONSE = message.waitForResponse
+	table.remove( g_messageQueue, 1 )
+
+	-- Send the next message in the queue.
+	if #g_messageQueue > 0 then
+		luup.call_delay( "SendPacket", DELAYS.SEND_NEXT_MESSAGE )
+	else
+		F_SENDING_IN_PROGRESS = false
+	end
+end
+
+
+------------------------------------------------------------------------------------------------------------------------
+-- Action implementations
+------------------------------------------------------------------------------------------------------------------------
+
+function SetLearnMode (learnMode)
+	luup.variable_set( SID.ENOCEAN, "LearnMode", learnMode, g_parentDevice )
+	F_LEARN_MODE = (learnMode == "1")
+	-- We entered Learn Mode. Display a message and do nothing.
+	if F_LEARN_MODE then
+		ShowSysMessage( "Entered Learn Mode" )
+	-- We exited Learn Mode.
+	-- If we learned any new device, create its Luup counterpart and update the number of EnOcean devices learned.
+	elseif #g_newDevices > 0 then
+		luup.variable_set( SID.ENOCEAN, "MaxID", g_MaxId, g_parentDevice )
+		local ptr = luup.chdev.start( g_parentDevice )
+		for i, v in ipairs( g_newDevices ) do
+			luup.chdev.append( g_parentDevice, ptr, v.altid, v.deviceName, v.deviceType,
+				v.deviceFile, v.implementationFile, v.parameters, false )
+		end
+		luup.chdev.sync( g_parentDevice, ptr )
+	-- We exited Learn Mode and no new device was learned. Display a message and do nothing.
+	else
+		ShowSysMessage( "Exited Learn Mode. No new device was learned." )
+	end
+end
+
+
+------------------------------------------------------------------------------------------------------------------------
+-- Startup functions
+------------------------------------------------------------------------------------------------------------------------
+
+local function GetChildDevices()
+	-- Get the number of learned EnOcean devices.
+	g_MaxId = luup.variable_get( SID.ENOCEAN, "MaxID", g_parentDevice )
+	g_MaxId = tonumber( g_MaxId, 10 ) or 0
+	-- Get a list with all our child devices.
+	for k, v in pairs( luup.devices ) do
+		if v.device_num_parent == g_parentDevice then
+			g_childDevices[v.id] = k
+			debug( "(EnOceanPlugin::GetChildDevices) Found device #".. k .." - ".. v.description ..", sender ID: ".. v.id )
+		end
+	end
+end
+
+
+------------------------------------------------------------------------------------------------------------------------
+-- Init
+------------------------------------------------------------------------------------------------------------------------
+
+function Init (lul_device)
+	log( "(EnOceanPlugin::Init) EnOcean ESP3 Gateway plugin" )
+
+	-- Get the master device.
+	g_parentDevice = lul_device
+
+	-- Get a handle for system messages.
+	g_taskHandle = luup.task( "Starting up...", 1, "EnOcean plugin", -1 )
+
+	-- Update the Debug Mode.
+	local debugMode = luup.variable_get( SID.ENOCEAN, "DebugMode", lul_device ) or ""
+	if debugMode == "" then
+		luup.variable_set( SID.ENOCEAN, "DebugMode", (F_DEBUG_MODE and "1" or "0"), lul_device )
+	else
+		F_DEBUG_MODE = (debugMode == "1") and true or false
+	end
+
+	if F_DEBUG_MODE then
+		debug = log
+	end
+
+	-- Disable Learn Mode.
+	luup.variable_set( SID.ENOCEAN, "LearnMode", "0", lul_device )
+
+	GetChildDevices()
+
+	-- Get the sender ID of the TCM300.
+	--AddToSendQueue( PACKET_TYPES.COMMON_COMMAND, COMMON_COMMAND.CO_RD_IDBASE, "", true )
+
+	debug( "(EnOceanPlugin::Init) Success: startup successful" )
+	return true, "startup successful", "EnOcean plugin"
+end
Index: /tags/0.2/D_EnOceanGateway1.json
===================================================================
--- /tags/0.2/D_EnOceanGateway1.json	(revision 30)
+++ /tags/0.2/D_EnOceanGateway1.json	(revision 30)
@@ -0,0 +1,138 @@
+{
+	"flashicon": "icons/device.png",
+	"x": "2",
+	"y": "4",
+	"inScene": "1",
+	"Tabs": [
+		{
+			"Label": {
+				"lang_tag": "tabname_control",
+				"text": "Control"
+			},
+			"Position": "0",
+			"TabType": "flash",
+			"ControlGroup": [
+				{
+					"id": "1",
+					"isSingle": "1",
+					"scenegroup": "1"
+				},
+				{
+					"id": "2",
+					"isSingle": "1",
+					"scenegroup": "1"
+				}
+			],
+			"SceneGroup": [
+				{
+					"id": "1",
+					"top": "2",
+					"left": "0",
+					"x": "2",
+					"y": "1"
+				}
+			],
+			"Control": [
+				{
+					"ControlGroup": "1",
+					"ControlType": "label",
+					"top": "-1",
+					"left": "0",
+					"text_align": "left",
+					"Label": {
+						"lang_tag": "mode",
+						"text": "Mode:"
+					},
+					"Display": {
+						"Top": 30,
+						"Left": 50,
+						"Width": 75,
+						"Height": 20
+					}
+				},
+				{
+					"ControlGroup": "2",
+					"ControlType": "button",
+					"top": "0",
+					"left": "1",
+					"Label": {
+						"lang_tag": "cmd_learn_mode_on",
+						"text": "Learn"
+					},
+					"Display": {
+						"Service": "urn:micasaverde-com:serviceId:EnOceanGateway1",
+						"Variable": "LearnMode",
+						"Value": "1",
+						"Top": 60,
+						"Left": 145,
+						"Width": 75,
+						"Height": 20
+					},
+					"Command": {
+						"Service": "urn:micasaverde-com:serviceId:EnOceanGateway1",
+						"Action": "SetLearnMode",
+						"Parameters": [
+							{
+								"Name": "learnMode",
+								"Value": "1"
+							}
+						]
+					},
+					"ControlCode": "learn_mode_on"
+				},
+				{
+					"ControlGroup": "1",
+					"ControlType": "button",
+					"top": "0",
+					"left": "0",
+					"Label": {
+						"lang_tag": "cmd_learn_mode_off",
+						"text": "Normal"
+					},
+					"Display": {
+						"Service": "urn:micasaverde-com:serviceId:EnOceanGateway1",
+						"Variable": "LearnMode",
+						"Value": "0",
+						"Top": 60,
+						"Left": 50,
+						"Width": 75,
+						"Height": 20
+					},
+					"Command": {
+						"Service": "urn:micasaverde-com:serviceId:EnOceanGateway1",
+						"Action": "SetLearnMode",
+						"Parameters": [
+							{
+								"Name": "learnMode",
+								"Value": "0"
+							}
+						]
+					},
+					"ControlCode": "learn_mode_off"
+				}
+			]
+		},
+		{
+			"Label": {
+				"lang_tag": "settings",
+				"text": "Settings"
+			},
+			"Position": "1",
+			"TabType": "javascript",
+			"ScriptName": "shared.js",
+			"Function": "simple_device"
+		},
+		{
+			"Label": {
+				"lang_tag": "advanced",
+				"text": "Advanced"
+			},
+			"Position": "2",
+			"TabType": "javascript",
+			"ScriptName": "shared.js",
+			"Function": "advanced_device"
+		}
+	],
+	"DeviceType": "urn:schemas-micasaverde-com:device:EnOceanGateway:1",
+	"device_type": "urn:schemas-micasaverde-com:device:EnOceanGateway:1"
+}
Index: /trunk/S_EnOceanGateway1.xml
===================================================================
--- /trunk/S_EnOceanGateway1.xml	(revision 20)
+++ /trunk/S_EnOceanGateway1.xml	(revision 30)
@@ -11,5 +11,13 @@
 		</stateVariable>
 		<stateVariable>
-			<name>NumDevices</name>
+			<name>MaxID</name>
+			<dataType>ui4</dataType>
+		</stateVariable>
+		<stateVariable>
+			<name>TeachInStage</name>
+			<dataType>ui4</dataType>
+		</stateVariable>
+		<stateVariable>
+			<name>A_ARG_TYPE_UI4</name>
 			<dataType>ui4</dataType>
 		</stateVariable>
@@ -26,4 +34,14 @@
 			</argumentList>
 		</action>
+		<action>
+			<name>TeachIn</name>
+			<argumentList>
+				<argument>
+					<name>pinCode</name>
+					<direction>in</direction>
+					<relatedStateVariable>A_ARG_TYPE_UI4</relatedStateVariable>
+				</argument>
+			</argumentList>
+		</action>
 	</actionList>
 </scpd>
Index: /trunk/I_EnOceanGateway1.xml
===================================================================
--- /trunk/I_EnOceanGateway1.xml	(revision 20)
+++ /trunk/I_EnOceanGateway1.xml	(revision 30)
@@ -15,4 +15,5 @@
 			-- Callbacks
 			ClearSysMessage = enoceanPlugin.ClearSysMessage
+			TeachIn = enoceanPlugin.TeachIn
 
 			return enoceanPlugin.Init( lul_device )
@@ -40,4 +41,11 @@
 			</run>
 		</action>
+		<action>
+			<serviceId>urn:micasaverde-com:serviceId:EnOceanGateway1</serviceId>
+			<name>TeachIn</name>
+			<run>
+				enoceanPlugin.TeachIn( 1, lul_settings.pinCode )
+			</run>
+		</action>
 	</actionList>
 </implementation>
Index: /trunk/L_EnOceanGateway1.lua
===================================================================
--- /trunk/L_EnOceanGateway1.lua	(revision 20)
+++ /trunk/L_EnOceanGateway1.lua	(revision 30)
@@ -1,2 +1,21 @@
+-- Steps to get Vera learned into a device (e.g. an actuator), to be able to control it:
+-- 0. User enters pin code into GUI and presses the Teach button.
+-- 1. Vera transmits 'Unlock device' telegram. (DONE)
+-- 2. Vera transmits 'Query ID' telegram. (DONE)
+-- 3. Vera receives 'Query ID' answer and creates the devices with the appropriate sender ID. (DONE)
+-- 4. Vera transmits 'Enter learn mode' telegram. (DONE)
+-- 5. Vera transmits RPS teach-in telegram. (DONE)
+-- 6. Vera transmits 'Stop learn' telegram. (DONE)
+-- 7. Vera transmits 'Lock device' telegram. (DONE)
+--
+-- Requirements
+-- * Vera must have an unique sender ID. (DONE)
+--
+-- FAQ
+-- Q: When Vera broadcasts the 'Unlock device' command, won't all the devices be unlocked and learn about Vera?
+-- A: We assume that only devices which accepted the pin code will be unlocked.
+--    So, if the user has 3 actuators but only 1 accepted the PIN code,
+--    that will be the only actuator that will learn about Vera and respond to commands from Vera.
+
 module("L_EnOceanGateway1", package.seeall)
 
@@ -9,5 +28,4 @@
 local F_DEBUG_MODE          = true
 local F_LEARN_MODE          = false
-local F_WAIT_FOR_RESPONSE   = false -- If this is 'true', no further messages will be sent
 local F_SENDING_IN_PROGRESS = false
 
@@ -46,12 +64,13 @@
 -- Message types
 local SYS_MESSAGE_TYPES = {
-	BUSY    =  1,
-	ERROR   =  2,
-	SUCCESS =  4
+	BUSY    = 1,
+	ERROR   = 2,
+	SUCCESS = 4
 }
 
 -- Delays for luup.delay, in seconds
 local DELAYS = {
-	SEND_NEXT_MESSAGE = 2
+	SEND_NEXT_MESSAGE = 2,
+	TEACHIN_STAGES    = 3
 }
 
@@ -76,7 +95,9 @@
 
 local TELEGRAM_TYPES = {
-	["RPS"] = 0xF6,
-	["1BS"] = 0xD5,
-	["4BS"] = 0xA5
+	["RPS"]    = 0xF6,
+	["1BS"]    = 0xD5,
+	["4BS"]    = 0xA5,
+	["ADT"]    = 0xA6,
+	["SYS_EX"] = 0xC5
 }
 
@@ -86,4 +107,5 @@
 	["RPS_EB"]           = {  3,  1 },
 	["RPS_T21"]          = {  2,  1 },
+	["RPS_NU"]           = {  3,  1 },
 	["1BS_LRN"]          = {  4,  1 },
 	["1BS_CO"]           = {  7,  1 },
@@ -101,5 +123,7 @@
 	["4BS_06_ILL1"]      = { 16,  8 },
 	["4BS_06_ILL2"]      = {  8,  8 },
-	["4BS_07_PIRS"]      = { 16,  8 }
+	["4BS_07_PIRS"]      = { 16,  8 },
+	["SYS_EX_FUNC_CODE"] = { 36, 12 },
+	["SYS_EX_MANUF_ID"]  = { 25, 11 }
 }
 
@@ -113,4 +137,55 @@
 	[6] = "DI",
 	[7] = "DO",
+}
+
+-- Base ID related variables.
+local MIN_BASE_ID = 0xFF800000
+local MAX_BASE_ID = 0xFFFFFF00 -- The last sender IDs interval has only 127 values instead of 128,
+                               -- so don't use it, to avoid handling it separately.
+local BASE_ID_GAP = 128
+----
+
+local NUM_TEACHIN_STAGES  = 7
+
+local TEACHIN_STAGES = {
+	NONE                  = 0,
+	SEND_UNLOCK_RMCC      = 1,
+	SEND_QUERY_ID_RMCC    = 2,
+	RECV_QUERY_ID_ANSWER  = 3,
+	SEND_START_LEARN_RPC  = 4,
+	SEND_TEACHIN_TELEGRAM = 5,
+	SEND_STOP_LEARN_RPC   = 6,
+	SEND_LOCK_RMCC        = 7
+}
+
+local TEACHIN_STAGE_STRINGS = {
+	[ TEACHIN_STAGES.NONE ]                 = "NONE",
+	[ TEACHIN_STAGES.SEND_UNLOCK_RMCC ]     = "SEND UNLOCK RMCC",
+	[ TEACHIN_STAGES.SEND_QUERY_ID_RMCC ]   = "SEND QUERY ID RMCC",
+	[ TEACHIN_STAGES.RECV_QUERY_ID_ANSWER ] = "RECEIVE QUERY ID ANSWER",
+	[ TEACHIN_STAGES.SEND_START_LEARN_RPC ] = "SEND START LEARN RPC",
+	[ TEACHIN_STAGES.SEND_TEACHIN_TELEGRAM ]= "SEND TEACH-IN TELEGRAM",
+	[ TEACHIN_STAGES.SEND_STOP_LEARN_RPC ]  = "SEND STOP LEARN RPC",
+	[ TEACHIN_STAGES.SEND_LOCK_RMCC ]       = "SEND LOCK RMCC"
+}
+
+-- Remote management commands function numbers
+local RMCC = {
+	UNLOCK          = 0x001,
+	LOCK            = 0x002,
+	QUERY_ID        = 0x004,
+	QUERY_ID_ANSWER = 0x604
+}
+
+-- Remote procedure calls function numbers
+local RPC = {
+	REMOTE_LEARN = 200
+}
+
+local DATA_LENGTHS = {
+	[RMCC.UNLOCK]      = 4,
+	[RMCC.LOCK]        = 4,
+	[RMCC.QUERY_ID]    = 3,
+	[RPC.REMOTE_LEARN] = 4
 }
 
@@ -163,6 +238,5 @@
 
 local g_childDevices = {}   -- The list of all our child devices
-local g_numDevices = 0      -- The number of learned EnOcean devices. This is different than the number of child devices,
-                            -- because there can be several child devices for one EnOcean device.
+local g_maxId = 0           -- A number that increments with every device learned.
 
 local g_knownSenderIds = {} -- The list of the newly learned EnOcean devices while in Learn Mode.
@@ -180,23 +254,24 @@
 ------------------------------------------------------------------------------------------------------------------------
 
-local log      = luup.log
--- Get the 'getinfo' function from the 'debug' module before overwriting the 'debug' keyword.
-local getinfo  = debug.getinfo
+local log = luup.log
+-- Save the 'getinfo' function from the 'debug' module before overwriting the 'debug' keyword.
+local getinfo = debug.getinfo
 local function debug() end
 
 
-local function ShowSysMessage (message, mode, critical)
+local function ShowSysMessage (message, mode, permanent)
 	mode = mode or SYS_MESSAGE_TYPES.BUSY
-	critical = critical or false
+	permanent = permanent or false
 	log( "(EnOceanPlugin::ShowSysMessage-"..(getinfo(2).name or "N/A")..") mode: ".. mode
-		..", critical: ".. tostring( critical ) ..", message: ".. message )
+		..", permanent: ".. tostring( permanent ) ..", message: ".. message )
 
 	luup.task( message, mode, "EnOcean Plugin", g_taskHandle )
 	g_lastSysMessage = tostring( os.time() )
 
-	if not critical then
+	if not permanent then
 		-- Clear the previous system message, since it's transient.
 		luup.call_delay( "ClearSysMessage", 30, g_lastSysMessage )
-	else
+	elseif mode == SYS_MESSAGE_TYPES.ERROR then
+		-- Critical error.
 		luup.set_failure( true, g_parentDevice )
 	end
@@ -258,5 +333,5 @@
 -- Concatenates a table of numbers into a string separated by the given separator.
 local function ConcatTableHex( t, sep, start, length )
-	sep = sep or "-"
+	sep = sep or '-'
 	start = start or 1
 	if start < 0 then
@@ -291,4 +366,23 @@
 
 
+-- Appends the second array at the end of the first array.
+local function AppendArray( a1, a2 )
+	local table_insert = table.insert
+	for _, v in ipairs(a2) do
+		table_insert( a1, v )
+	end
+end
+
+
+-- Splits a string based on the given separator. Returns a table.
+local function SplitString( s, sep )
+	sep = sep or ' '
+	local t = {}
+	for token in s:gmatch( "[^"..sep.."]+" ) do
+		table.insert( t, token )
+	end
+	return t
+end
+
 ------------------------------------------------------------------------------------------------------------------------
 -- Misc plugin functions
@@ -303,6 +397,64 @@
 		crc = CRC8_TABLE[bit.bxor( crc, data[i] ) + 1]
 	end
-	debug( "(EnOceanPlugin::ComputeCrc) crc=".. ToHex( crc ))
+	--debug( "(EnOceanPlugin::ComputeCrc) crc=".. ToHex( crc ))
 	return crc
+end
+
+
+local function GetBaseIdArray()
+	-- If the base ID array is in cache, return the cached version,
+	-- otherwise compute it.
+	-- The 'c' prefix means the variable is the cache.
+	if c_baseIdArray then
+		return c_baseIdArray
+	else
+		c_baseIdArray = { 0, 0, 0, 0 }
+		for nibbleNum = 1, 4 do
+			local numBitsShift = (4 - nibbleNum) * 8
+			local mask = bit.lshift( 0xFF, numBitsShift )
+			c_baseIdArray[nibbleNum] = bit.rshift( bit.band( g_baseId, mask ), numBitsShift )
+		end
+		return c_baseIdArray
+	end
+end
+
+
+-- Splits the given number into an array of bits indexed from 1.
+-- The first bit (bit 0) is the first element in the array.
+local function SplitToBits( number, numBits )
+	local bits = {}
+
+	local table_insert = table.insert
+	while number > 0 do
+		table_insert( bits, number % 2 )
+		number = math.floor( number / 2 )
+	end
+	-- Pad with 0 up to the number of required bits.
+	for i = #bits + 1, numBits do
+		table_insert( bits, 0 )
+	end
+
+	return bits
+end
+
+
+-- Padds the value to occupy the given number of bits,
+-- and inserts it in the data, which is an array of bytes.
+local function InsertValueAtLocation (data, value, offset, numBits)
+	-- Split value into an array of bits indexed from 1.
+	local bits = SplitToBits( value, numBits )
+	-- Insert all the bits starting from the last bit.
+	for bitIdx = #bits, 1, -1 do
+		-- Find byte index, indexed from 1.
+		local byteIdx = math.floor( offset / 8 ) + 1
+		-- Find bit position, indexed from 0.
+		local bitPos = byteIdx * 8 - offset - 1
+		if bits[bitIdx] == 1 then
+			data[byteIdx] = bit.set( data[byteIdx], bit.lshift( 1, bitPos ))
+		else
+			data[byteIdx] = bit.unset( data[byteIdx], bit.lshift( 1, bitPos ))
+		end
+		offset = offset + 1
+	end
 end
 
@@ -374,23 +526,33 @@
 
 		local firstAction = GetValueAtLocation( data, LOCATIONS["RPS_R1"] )
-		local altid = senderId ..'-'.. ROCKER_BUTTONS[firstAction]
+		local altid = senderId ..'-'.. ROCKER_BUTTONS[firstAction]:sub( 1, 1 )
 		debug( "(EnOceanPlugin::RPS) altid: ".. altid )
 
 		--------------------------------------------------------------------------------------------
-		-- The device is learned.
+		-- The device is learned and it's not a "number of buttons pressed" telegram.
 		--------------------------------------------------------------------------------------------
 		if g_childDevices[altid] then
-			debug( "(EnOceanPlugin::RPS) Device known: ".. g_childDevices[altid] )
-			local pressed = GetValueAtLocation( data, LOCATIONS["RPS_EB"] )
+			log( "(EnOceanPlugin::RPS) Device known: ".. g_childDevices[altid] )
+			-- Ignore "number of buttons pressed" telegrams.
+			if GetValueAtLocation( { status }, LOCATIONS["RPS_NU"] ) == 0 then
+				debug( "(EnOceanPlugin::RPS) 'Number of buttons pressed' telegram. Return" )
+				return
+			end
+			-- Ignore "button released" telegrams.
+			if GetValueAtLocation( data, LOCATIONS["RPS_EB"] ) == 0 then
+				debug( "(EnOceanPlugin::RPS) 'Button released' telegram. Return" )
+				return
+			end
+			local pressed = ( ROCKER_BUTTONS[firstAction]:sub( 2, 2 ) == "I" ) and "1" or "0"
 			luup.variable_set( SID.SW_POWER, "Status", pressed, g_childDevices[altid] )
 
 		--------------------------------------------------------------------------------------------
 		-- The device is not learned, and we're in Learn Mode.
-		-- RPS devices don't have Teach-in telegrams.
+		-- RPS devices don't have TeachIn-in telegrams.
 		--------------------------------------------------------------------------------------------
 		elseif F_LEARN_MODE and not TableContains( g_knownSenderIds, senderId ) then
 			-- We don't want to add the same device twice.
 			set.add( g_knownSenderIds, senderId )
-			g_numDevices = g_numDevices + 1
+			g_maxId = g_maxId + 1
 
 			-- Find the device type. If T21 is 1, this is a 2 rocker switch, otherwise it's a 4 rocker switch.
@@ -400,11 +562,13 @@
 			-- Create the devices.
 			log( "(EnOceanPlugin::RPS) T21: ".. t21 ..", num rockers: ".. numRockers )
-			ShowSysMessage( "Found ".. numRockers .." Rocker Switch (EO-".. PadLeft( g_numDevices ) ..")" )
-			-- We have 2 buttons for each rocker, so we'll create a switch for each button.
+			ShowSysMessage( "Found ".. numRockers .." Rocker Switch (EO-".. PadLeft( g_maxId ) ..")" )
+			-- Create a switch for each rocker.
 			local deviceName
 			local parameters = SID.SW_POWER ..",Status=0"
-			for i = 0, 2 * numRockers - 1 do
-				altid = senderId ..'-'.. ROCKER_BUTTONS[i]
-				deviceName = "(EO-".. PadLeft( g_numDevices ) ..") Button ".. ROCKER_BUTTONS[i]
+			for i = 0, numRockers - 1 do
+				-- Get only the button, ignore the state.
+				local button = ROCKER_BUTTONS[2 * i]:sub( 1, 1 )
+				altid = senderId ..'-'.. button
+				deviceName = "(EO-".. PadLeft( g_maxId ) ..") Button ".. button
 				log(  "(EnOceanPlugin::RPS) New device: ".. deviceName  )
 				table.insert( g_newDevices, {
@@ -417,4 +581,6 @@
 				} )
 			end
+		else
+			log( "(EnOceanPlugin::RPS) Unkown device" )
 		end
 	end,
@@ -427,5 +593,5 @@
 
 		local isDataTelegram = GetValueAtLocation( data, LOCATIONS["1BS_LRN"] ) == 1
-		debug(  "(EnOceanPlugin::1BS) ".. (isDataTelegram and "Data" or "Teach-in") .." telegram"  )
+		debug(  "(EnOceanPlugin::1BS) ".. (isDataTelegram and "Data" or "TeachIn-in") .." telegram"  )
 
 		--------------------------------------------------------------------------------------------
@@ -433,5 +599,5 @@
 		--------------------------------------------------------------------------------------------
 		if g_childDevices[senderId] and isDataTelegram then
-			debug(  "(EnOceanPlugin::1BS) Device known: ".. g_childDevices[senderId]  )
+			log(  "(EnOceanPlugin::1BS) Device known: ".. g_childDevices[senderId]  )
 			local tripped = 1 - GetValueAtLocation( data, LOCATIONS["1BS_CO"] )
 			luup.variable_set( SID.SECURITY, "Tripped", tripped, g_childDevices[senderId] )
@@ -439,5 +605,5 @@
 		--------------------------------------------------------------------------------------------
 		-- The device is not learned, and we're in Learn Mode.
-		-- Only learn the device if this is a Teach-in telegram, even if it doesn't contain any useful information.
+		-- Only learn the device if this is a TeachIn-in telegram, even if it doesn't contain any useful information.
 		-- We don't want to accidentaly learn devices that the user doesn't want.
 		--------------------------------------------------------------------------------------------
@@ -445,8 +611,8 @@
 			-- We don't want to add the same device twice.
 			set.add( g_knownSenderIds, senderId )
-			g_numDevices = g_numDevices + 1
+			g_maxId = g_maxId + 1
 			log( "(EnOceanPlugin::1BS) New device: ".. deviceName  )
-			ShowSysMessage( "Found Single Input Contact (EO-".. PadLeft( g_numDevices ) ..")" )
-			deviceName = "(EO-".. PadLeft( g_numDevices ) ..") Single Input Contact"
+			ShowSysMessage( "Found Single Input Contact (EO-".. PadLeft( g_maxId ) ..")" )
+			deviceName = "(EO-".. PadLeft( g_maxId ) ..") Single Input Contact"
 			table.insert( g_newDevices, {
 				["altid"]      = senderId,
@@ -457,4 +623,6 @@
 				["parameters"] = SID.SECURITY ..",Tripped=0\n".. SID.SECURITY ..",Armed=0"
 			} )
+		else
+			log( "(EnOceanPlugin::1BS) Unkown device or TeachIn-in telegram" )
 		end
 	end,
@@ -464,8 +632,8 @@
 	--------------------------------------------------------------------------------------------------------------------
 	[TELEGRAM_TYPES["4BS"]] = function (senderId, data, status)
-		debug(  "(EnOceanPlugin::4BS) 4BS telegram handler"  )
+		debug( "(EnOceanPlugin::4BS) 4BS telegram handler" )
 
 		local isDataTelegram = GetValueAtLocation( data, LOCATIONS["4BS_LRN"] ) == 1
-		debug(  "(EnOceanPlugin::4BS) ".. (isDataTelegram and "Data" or "Teach-in") .." telegram"  )
+		debug( "(EnOceanPlugin::4BS) ".. (isDataTelegram and "Data" or "TeachIn-in") .." telegram" )
 
 		--------------------------------------------------------------------------------------------
@@ -481,8 +649,8 @@
 			if eepFunc == 0x02 then
 				if not g_childDevices[altid] then
-					debug( "(EnOceanPlugin::4BS) Device not known. Return" )
+					log( "(EnOceanPlugin::4BS) Device not known. Return" )
 					return
 				end
-				debug( "(EnOceanPlugin::4BS) Device known: ".. g_childDevices[altid] )
+				log( "(EnOceanPlugin::4BS) Device known: ".. g_childDevices[altid] )
 
 				local sensorType = math.floor( eepType / 0x10 )
@@ -522,8 +690,8 @@
 				local altidTemp = altid .."-temp"
 				if not g_childDevices[altidHum] then
-					debug( "(EnOceanPlugin::4BS) Device not known. Return" )
+					log( "(EnOceanPlugin::4BS) Device not known. Return" )
 					return
 				end
-				debug( "(EnOceanPlugin::4BS) Device known: hum ".. g_childDevices[altidHum] ..", temp "..( g_childDevices[altidTemp] or "NONE" ))
+				log( "(EnOceanPlugin::4BS) Device known: hum ".. g_childDevices[altidHum] ..", temp "..( g_childDevices[altidTemp] or "NONE" ))
 
 				-- Get the humidity.
@@ -543,8 +711,8 @@
 			elseif eepFunc == 0x06 then
 				if not g_childDevices[altid] then
-					debug( "(EnOceanPlugin::4BS) Device not known. Return" )
+					log( "(EnOceanPlugin::4BS) Device not known. Return" )
 					return
 				end
-				debug( "(EnOceanPlugin::4BS) Device known: ".. g_childDevices[altid] )
+				log( "(EnOceanPlugin::4BS) Device known: ".. g_childDevices[altid] )
 
 				local range = GetValueAtLocation( data, LOCATIONS["4BS_06_RS"] )
@@ -569,8 +737,8 @@
 			elseif eepFunc == 0x07 then
 				if not g_childDevices[altid] then
-					debug( "(EnOceanPlugin::4BS) Device not known. Return" )
+					log( "(EnOceanPlugin::4BS) Device not known. Return" )
 					return
 				end
-				debug( "(EnOceanPlugin::4BS) Device known: ".. g_childDevices[altid] )
+				log( "(EnOceanPlugin::4BS) Device known: ".. g_childDevices[altid] )
 
 				local tripped = GetValueAtLocation( data, LOCATIONS["4BS_07_PIRS"] ) -- 0...255
@@ -579,10 +747,10 @@
 			end
 		--------------------------------------------------------------------------------------------
-		-- The device is not learned, we're in Learn Mode, and we received a Teach-in telegram.
+		-- The device is not learned, we're in Learn Mode, and we received a TeachIn-in telegram.
 		--------------------------------------------------------------------------------------------
 		elseif F_LEARN_MODE and not isDataTelegram and not g_childDevices[senderId] and not TableContains( g_knownSenderIds, senderId ) then
 			local lrnType = GetValueAtLocation( data, LOCATIONS["4BS_LRN_TYPE"] )
 			if lrnType ~= 1 and lrnType ~= 2 then
-				log( "(EnOceanPlugin::4BS) Teach-in telegram variation 1, discard it" )
+				log( "(EnOceanPlugin::4BS) TeachIn-in telegram variation 1, discard it" )
 				return
 			end
@@ -590,9 +758,9 @@
 			-- We don't want to add the same device twice.
 			set.add( g_knownSenderIds, senderId )
-			g_numDevices = g_numDevices + 1
+			g_maxId = g_maxId + 1
 
 			local eepFunc = GetValueAtLocation( data, LOCATIONS["4BS_LRN_EEP_FUNC"] )
 			local eepType = GetValueAtLocation( data, LOCATIONS["4BS_LRN_EEP_TYPE"] )
-			local manufId = GetValueAtLocation( data, LOCATIONS["4BS_LRN_MANUF_ID"] )
+			local manufId = GetValueAtLocation( data, LOCATIONS["4BS_LRN_MANUF_ID"] ) -- not used in this version
 
 			log( "(EnOceanPlugin::4BS) Func ".. ToHex( eepFunc ) ..", Type ".. ToHex( eepType ))
@@ -603,7 +771,7 @@
 			--
 			if eepFunc == 0x02 then
-				local deviceName = "(EO-".. PadLeft( g_numDevices ) ..") Temperature Sensor"
+				local deviceName = "(EO-".. PadLeft( g_maxId ) ..") Temperature Sensor"
 				log( "(EnOceanPlugin::4BS) New device: ".. deviceName )
-				ShowSysMessage( "Found Temperature sensor (EO-".. PadLeft( g_numDevices ) ..")" )
+				ShowSysMessage( "Found Temperature sensor (EO-".. PadLeft( g_maxId ) ..")" )
 				table.insert( g_newDevices, {
 					["altid"]      = senderId,
@@ -618,7 +786,7 @@
 			--
 			elseif eepFunc == 0x04 then
-				ShowSysMessage( "Found Temperature and Humidity sensor (EO-".. PadLeft( g_numDevices ) ..")" )
+				ShowSysMessage( "Found Temperature and Humidity sensor (EO-".. PadLeft( g_maxId ) ..")" )
 				-- Create the humidity sensor.
-				deviceName = "(EO-".. PadLeft( g_numDevices ) ..") Humidity Sensor"
+				deviceName = "(EO-".. PadLeft( g_maxId ) ..") Humidity Sensor"
 				log( "(EnOceanPlugin::4BS) New device: ".. deviceName )
 				table.insert( g_newDevices, {
@@ -631,5 +799,5 @@
 				} )
 				-- Create the temperature sensor.
-				local deviceName = "(EO-".. PadLeft( g_numDevices ) ..") Temperature Sensor"
+				local deviceName = "(EO-".. PadLeft( g_maxId ) ..") Temperature Sensor"
 				log( "(EnOceanPlugin::4BS) New device: ".. deviceName )
 				table.insert( g_newDevices, {
@@ -645,7 +813,7 @@
 			--
 			elseif eepFunc == 0x06 then
-				local deviceName = "(EO-".. PadLeft( g_numDevices ) ..") Light Sensor"
+				local deviceName = "(EO-".. PadLeft( g_maxId ) ..") Light Sensor"
 				log( "(EnOceanPlugin::4BS) New device: ".. deviceName )
-				ShowSysMessage( "Found Light sensor (EO-".. PadLeft( g_numDevices ) ..")" )
+				ShowSysMessage( "Found Light sensor (EO-".. PadLeft( g_maxId ) ..")" )
 				table.insert( g_newDevices, {
 					["altid"]      = senderId,
@@ -660,7 +828,7 @@
 			--
 			elseif eepFunc == 0x07 then
-				local deviceName = "(EO-".. PadLeft( g_numDevices ) ..") Occupancy Sensor"
+				local deviceName = "(EO-".. PadLeft( g_maxId ) ..") Occupancy Sensor"
 				log( "(EnOceanPlugin::4BS) New device: ".. deviceName )
-				ShowSysMessage( "Found Occupancy sensor (EO-".. PadLeft( g_numDevices ) ..")" )
+				ShowSysMessage( "Found Occupancy sensor (EO-".. PadLeft( g_maxId ) ..")" )
 				table.insert( g_newDevices, {
 					["altid"]      = senderId,
@@ -675,4 +843,45 @@
 				ShowSysMessage( "Found unknown device A5-".. ToHex( eepFunc ) .."-".. ToHex( eepType ) )
 			end
+		else
+			log( "(EnOceanPlugin::4BS) Unkown device or TeachIn-in telegram" )
+		end
+	end,
+
+	--------------------------------------------------------------------------------------------------------------------
+	-- ADT
+	--------------------------------------------------------------------------------------------------------------------
+	[TELEGRAM_TYPES["ADT"]] = function (senderId, data, status)
+		debug( "(EnOceanPlugin::ADT) ADT telegram handler" )
+
+		-- Check if we're in a teach-in procedure and we're expecting a Query ID answer.
+		if g_teachInStage == TEACHIN_STAGES.RECV_QUERY_ID_ANSWER then
+			-- Check if this telegram is a Query ID answer telegram:
+			-- 1. Check if the encapsulated RORG is the one we expect.
+			if data[1] ~= TELEGRAM_TYPES.SYS_EX then
+				log( "(EnOceanPlugin::ADT) Unhandled encapsulated RORG: ".. data[1] )
+				return
+			end
+			-- 2. Check if the RMC function code is the one we expect.
+			local functionCode = GetValueAtLocation( data, LOCATIONS["SYS_EX_FUNC_CODE"] )
+			if functionCode ~= RMCC.QUERY_ID_ANSWER then
+				log( "(EnOceanPlugin::ADT) Unhandled remote management control command: ".. functionCode )
+				return
+			end
+			-- Get the manufacturer ID. (not used in this version)
+			local manufId = GetValueAtLocation( data, LOCATIONS["SYS_EX_MANUF_ID"] )
+
+			--------------------------------------------------------------------
+			-- Create the device and move to the next stage.
+			--------------------------------------------------------------------
+			g_teachInStage = TEACHIN_STAGES.SEND_START_LEARN_RPC
+			luup.variable_set( SID.ENOCEAN, "TeachInStage", g_teachInStage, g_parentDevice )
+
+			g_maxId = g_maxId + 1
+			luup.variable_set( SID.ENOCEAN, "MaxID", g_maxId, g_parentDevice )
+
+			local ptr = luup.chdev.start( g_parentDevice )
+			luup.chdev.append( g_parentDevice, ptr, senderId, "(EO-".. PadLeft( g_maxId ) ..") Actuator",
+				DEVICE_TYPES.BINARY_LIGHT, DEVICE_FILES.BINARY_LIGHT, "", SID.SW_POWER ..",Status=0", false )
+			luup.chdev.sync( g_parentDevice, ptr )
 		end
 	end
@@ -697,5 +906,5 @@
 			.. ", senderId: ".. senderId
 			.. ", status: ".. ToHex( status )
-			.. ", data: ".. ConcatTableHex( data, '-' ))
+			.. ", data: ".. ConcatTableHex( data ))
 
 		if RADIO_TELEGRAM_HANDLERS[radioTelegramType] then
@@ -741,5 +950,5 @@
 	[STATE.GET_SYNC_STATE] = function (rxByte)
 		if rxByte == SYNC_BYTE then
-			debug( "(EnOceanPlugin::GET_SYNC_STATE) Got sync byte, go to GET_HEADER_STATE" )
+			--debug( "(EnOceanPlugin::GET_SYNC_STATE) Got sync byte, go to GET_HEADER_STATE" )
 			state = STATE.GET_HEADER_STATE
 			crc   = 0
@@ -755,5 +964,5 @@
 		-- All the header bytes received?
 		if rxCount == 4 then
-			debug( "(EnOceanPlugin::GET_HEADER_STATE) Received all header bytes, go to CHECK_CRC8H_STATE" )
+			--debug( "(EnOceanPlugin::GET_HEADER_STATE) Received all header bytes, go to CHECK_CRC8H_STATE" )
 			state = STATE.CHECK_CRC8H_STATE
 		end
@@ -764,5 +973,5 @@
 		-- Header CRC correct?
         if ComputeCrc( rxBuf, 1, rxCount ) ~= rxByte then
-			debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Incorrect header CRC" )
+			--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Incorrect header CRC" )
 			-- No. Check if there is a sync byte (0x55) in the header.
 			local pos = -1
@@ -777,5 +986,5 @@
 			if pos == -1 and rxByte ~= SYNC_BYTE then
 				-- Neither the header nor the CRC8H contain the sync byte
-				debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Neither the header nor the CRC8H contain the sync byte, go to GET_SYNC_STATE" )
+				--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Neither the header nor the CRC8H contain the sync byte, go to GET_SYNC_STATE" )
 				state = STATE.GET_SYNC_STATE
 				return
@@ -783,5 +992,5 @@
 				-- The header does not contain the sync byte but the CRC8H does
 				-- The sync byte could be the beginning of a packet
-				debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) The header does not contain the sync byte but the CRC8H does, go to GET_HEADER_STATE" )
+				--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) The header does not contain the sync byte but the CRC8H does, go to GET_HEADER_STATE" )
 				state = STATE.GET_HEADER_STATE
 				rxCount = 0
@@ -802,9 +1011,9 @@
 
 			if rxCount < 4 then
-				debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) There are still header bytes to receive, go to GET_HEADER_STATE" )
+				--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) There are still header bytes to receive, go to GET_HEADER_STATE" )
 				state = STATE.GET_HEADER_STATE
 			end
 
-			debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) All header bytes received, stay in CHECK_CRC8H_STATE" )
+			--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) All header bytes received, stay in CHECK_CRC8H_STATE" )
 			return
         end
@@ -813,10 +1022,10 @@
 		rxDataLen = rxBuf[1] * 0x100 + rxBuf[2]
 		rxOptDataLen = rxBuf[3]
-		debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Header CRC correct, rxDataLen=".. rxDataLen ..", rxOptDataLen=".. rxOptDataLen )
+		--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Header CRC correct, rxDataLen=".. rxDataLen ..", rxOptDataLen=".. rxOptDataLen )
 		if (rxDataLen + rxOptDataLen) == 0 then
 			-- No. Sync byte received?
 			if rxByte == SYNC_BYTE then
 				-- Yes
-				debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Header CRC incorrect, sync byte received, go to GET_HEADER_STATE" )
+				--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Header CRC incorrect, sync byte received, go to GET_HEADER_STATE" )
 				state = STATE.GET_HEADER_STATE
 				rxCount = 0
@@ -825,5 +1034,5 @@
 
 			-- Packet with correct CRC8H, but wrong length fields
-			debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Packet with correct CRC8H, but wrong length fields, go to GET_SYNC_STATE" )
+			--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Packet with correct CRC8H, but wrong length fields, go to GET_SYNC_STATE" )
 			state = STATE.GET_SYNC_STATE
 			return
@@ -831,5 +1040,5 @@
 
 		-- Correct header CRC8. Go to the reception of data.
-		debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Correct header CRC8, go to GET_DATA_STATE" )
+		--debug( "(EnOceanPlugin::CHECK_CRC8H_STATE) Correct header CRC8, go to GET_DATA_STATE" )
 		state = STATE.GET_DATA_STATE
 		rxCount = 0
@@ -844,5 +1053,5 @@
 		-- When all the expected bytes are received, go to calculate data checksum.
 		if rxCount == (rxDataLen + rxOptDataLen) then
-			debug( "(EnOceanPlugin::GET_DATA_STATE) All expected bytes received, go to CHECK_CRC8D_STATE" )
+			--debug( "(EnOceanPlugin::GET_DATA_STATE) All expected bytes received, go to CHECK_CRC8D_STATE" )
 			state = STATE.CHECK_CRC8D_STATE
 		end
@@ -856,5 +1065,5 @@
 		if ComputeCrc( rxBuf, 1, rxCount ) == rxByte then
 			-- Correct packet received
-			debug( "(EnOceanPlugin::CHECK_CRC8D_STATE) Correct packet received, handle it and go to GET_SYNC_STATE" )
+			--debug( "(EnOceanPlugin::CHECK_CRC8D_STATE) Correct packet received, handle it and go to GET_SYNC_STATE" )
 			if PACKET_HANDLERS[rxPacketType] then
 				PACKET_HANDLERS[rxPacketType]( ExtractSubtable( rxBuf, 1, rxDataLen ), ExtractSubtable( rxBuf, rxDataLen + 1, rxOptDataLen ))
@@ -868,5 +1077,5 @@
 		-- If the received byte is the sync byte, then it could be the sync byte for next packet.
 		if rxByte == SYNC_BYTE then
-			debug( "(EnOceanPlugin::CHECK_CRC8D_STATE) Incorrect CRC8, the received byte is the sync byte, go to GET_HEADER_STATE" )
+			--debug( "(EnOceanPlugin::CHECK_CRC8D_STATE) Incorrect CRC8, the received byte is the sync byte, go to GET_HEADER_STATE" )
 			state = STATE.GET_HEADER_STATE
 			rxCount = 0
@@ -880,5 +1089,5 @@
 function HandleIncoming (lul_data)
 	local rxByte = string.byte( lul_data )
-	debug( "(EnOceanPlugin::HandleIncoming) state ".. state ..", rxByte=".. ToHex( rxByte ))
+	--debug( "(EnOceanPlugin::HandleIncoming) state ".. state ..", rxByte=".. ToHex( rxByte ))
 	if STATE_MACHINE[state] then
 		STATE_MACHINE[state]( rxByte )
@@ -895,21 +1104,66 @@
 ------------------------------------------------------------------------------------------------------------------------
 
-local function AddToSendQueue (packetType, data, optData, waitForResponse)
-	local insert = table.insert
-	local char   = string.char
-
-	local packet = { SYNC_BYTE }              -- The sync byte
-
-	insert( packet, char( #data / 0x100 ))    -- First byte of the data length
-	insert( packet, char( #data % 0x100 ))    -- Seconds byte of the data length
-	insert( packet, char( #optData ))         -- Length of the optional data
-	insert( packet, packetType )              -- The packet type
-	insert( packet, ComputeCrc(packet, 2, 4)) -- The header CRC8
-
-	insert( packet, data )                    -- The user data
-	insert( packet, optData )                 -- The optional user data
-	insert( packet, ComputeCrc( packet, 7, #data + #optData )) -- The data CRC8
-
-	table.insert( g_messageQueue, {content = table.concat(packet), waitForResponse = (waitForResponse or false)} )
+local function CreateSysExTelegram (rmcc, data)
+	-- 9 bytes - The first byte is always the RORG.
+	local sysEx = { TELEGRAM_TYPES["SYS_EX"], 0, 0, 0, 0, 0, 0, 0, 0 }
+	local offset = 8
+
+	-- SEQ - 2 bits
+	local numBits = 2
+	math.randomseed( os.time() )
+	local seq = math.random(4) - 1
+	InsertValueAtLocation( sysEx, seq, offset, numBits )
+	offset = offset + numBits
+
+	-- IDX - 6 bits
+	numBits = 6
+	InsertValueAtLocation( sysEx, 0, offset, numBits )
+	offset = offset + numBits
+
+	-- Data length - 9 bits
+	numBits = 9
+	InsertValueAtLocation( sysEx, DATA_LENGTHS[rmcc], offset, numBits )
+	offset = offset + numBits
+
+	-- Manufacturer ID - 11 bits
+	numBits = 11
+	InsertValueAtLocation( sysEx, 0x7FF, offset, numBits )
+	offset = offset + numBits
+
+	-- Function number - 12 bits
+	numBits = 12
+	InsertValueAtLocation( sysEx, rmcc, offset, numBits )
+	offset = offset + numBits
+
+	-- Data - 32 bits
+	numBits = 32
+	InsertValueAtLocation( sysEx, data, offset, numBits )
+
+	return sysEx
+end
+
+
+local function AddToSendQueue (packetType, data, optData)
+	optData = optData or {}
+
+	-- Declare often used functions locally for better performance.
+	local table_insert = table.insert
+
+	-- The sync byte
+	local packet = { SYNC_BYTE }
+
+	-- The header
+	table_insert( packet, string.char( #data / 0x100 )) -- First byte of the data length
+	table_insert( packet, string.char( #data % 0x100 )) -- Seconds byte of the data length
+	table_insert( packet, string.char( #optData ))      -- Length of the optional data
+	table_insert( packet, packetType )                  -- The packet type
+	table_insert( packet, ComputeCrc(packet, 2, 4))     -- The header CRC8
+
+	-- The data
+	AppendArray( packet, data )    -- The user data
+	AppendArray( packet, optData ) -- The optional user data
+	table_insert( packet, ComputeCrc( packet, 7, #data + #optData )) -- The data CRC8
+
+	table_insert( g_messageQueue, packet )
 	if not F_SENDING_IN_PROGRESS then
 		SendPacket()
@@ -919,34 +1173,19 @@
 
 function SendPacket()
+	-- If we don't have any message to send, return.
+	if #g_messageQueue == 0 then
+		F_SENDING_IN_PROGRESS = false
+		return
+	end
+
 	F_SENDING_IN_PROGRESS = true
 
-	if F_WAIT_FOR_RESPONSE then
-		g_triesCount = g_triesCount + 1
-		if g_triesCount < 3 then
-			debug( "(EnOceanPlugin::SendPacket) F_WAIT_FOR_RESPONSE flag is set, g_triesCount=".. g_triesCount .."; try again in ".. DELAYS.SEND_NEXT_MESSAGE .." seconds." )
-			luup.call_delay( "SendPacket", DELAYS.SEND_NEXT_MESSAGE )
-			return
-		else
-			log( "(EnOceanPlugin::SendPacket) Didn't receive response for the last message. Stop waiting for response." )
-			F_WAIT_FOR_RESPONSE = false
-			g_triesCount = 0
-		end
-	end
-
-	local message
-	if #g_messageQueue > 0 then
-		message = g_messageQueue[1]
-	else
-		return
-	end
-
-	debug( "(EnOceanPlugin::SendPacket) Wait for response: ".. message.waitForResponse )
-
-	if not luup.io.write( message.content ) then
+	local message = g_messageQueue[1]
+	log( "(EnOceanPlugin::SendPacket) Send message: ".. ConcatTableHex( message ))
+	if not luup.io.write( table.concat( message )) then
 		ShowSysMessage( "Failed to send message", SYS_MESSAGE_TYPES.ERROR )
 		return
 	end
 
-	F_WAIT_FOR_RESPONSE = message.waitForResponse
 	table.remove( g_messageQueue, 1 )
 
@@ -973,5 +1212,5 @@
 	-- If we learned any new device, create its Luup counterpart and update the number of EnOcean devices learned.
 	elseif #g_newDevices > 0 then
-		luup.variable_set( SID.ENOCEAN, "NumDevices", g_numDevices, g_parentDevice )
+		luup.variable_set( SID.ENOCEAN, "MaxID", g_maxId, g_parentDevice )
 		local ptr = luup.chdev.start( g_parentDevice )
 		for i, v in ipairs( g_newDevices ) do
@@ -982,6 +1221,231 @@
 	-- We exited Learn Mode and no new device was learned. Display a message and do nothing.
 	else
-		ShowSysMessage( "Exited Learn Mode. No new device was learned." )
-	end
+		ShowSysMessage( "Exited Learn Mode. No new device was learned" )
+	end
+end
+
+
+function TeachIn (stage, pinCode)
+	g_teachInStage = tonumber( stage, 10 ) or TEACHIN_STAGES.NONE
+
+	log( "(EnOceanPlugin::TeachIn) Stage ".. g_teachInStage .." (".. TEACHIN_STAGE_STRINGS[g_teachInStage] ..")" )
+	luup.variable_set( SID.ENOCEAN, "TeachInStage", g_teachInStage, g_parentDevice )
+	ShowSysMessage( "Teach-in stage ".. g_teachInStage .."/".. NUM_TEACHIN_STAGES, SYS_MESSAGE_TYPES.BUSY, true )
+
+	------------------------------------------------------------------------------------------------
+	-- Stage 1: Send 'Unlock' remote management control command (RMCC)
+	------------------------------------------------------------------------------------------------
+	if g_teachInStage == TEACHIN_STAGES.SEND_UNLOCK_RMCC then
+
+		pinCode = tonumber( pinCode ) or 0
+		if pinCode == 0 then
+			luup.variable_set( SID.ENOCEAN, "TeachInStage", TEACHIN_STAGES.NONE, g_parentDevice )
+			ShowSysMessage( "ERROR: missing or invalid PIN code", SYS_MESSAGE_TYPES.ERROR )
+			return
+		end
+		-- Save the PIN code to be used for the 'Lock' command.
+		luup.variable_set( SID.ENOCEAN, "PinCode", pinCode, g_parentDevice )
+		------------------------------------------------------------------------
+		-- Compose the 'Unlock' command.
+		------------------------------------------------------------------------
+		local data = {}
+		-- RORG - 1 byte
+		table.insert( data, TELEGRAM_TYPES["ADT"] )
+		-- Encapsulate a SYS_EX telegram - 9 bytes
+		AppendArray( data, CreateSysExTelegram( RMCC.UNLOCK, pinCode ))
+		-- Destination ID - 4 bytes
+		AppendArray( data, { 0xFF, 0xFF, 0xFF, 0xFF } )
+		-- Sender ID - 4 bytes
+		AppendArray( data, GetBaseIdArray() )
+		-- Status - 1 byte
+		table.insert( data, 0xF )
+		-- Compose and send the packet.
+		AddToSendQueue( PACKET_TYPES.RADIO, data )
+		-- Wait a few seconds before moving on to the next stage.
+		log( "(EnOceanPlugin::TeachIn) 'Unlock' command added in the send queue,"..
+			" wait ".. DELAYS.TEACHIN_STAGES .." seconds and move to the next stage" )
+		luup.call_delay( "TeachIn", DELAYS.TEACHIN_STAGES, TEACHIN_STAGES.SEND_QUERY_ID_RMCC )
+	------------------------------------------------------------------------------------------------
+	-- Stage 2: Send 'Query ID' RMCC
+	------------------------------------------------------------------------------------------------
+	elseif g_teachInStage == TEACHIN_STAGES.SEND_QUERY_ID_RMCC then
+
+		local data = {}
+		-- RORG - 1 byte
+		table.insert( data, TELEGRAM_TYPES["ADT"] )
+		-- Encapsulate a SYS_EX telegram - 9 bytes
+		AppendArray( data, CreateSysExTelegram( RMCC.QUERY_ID, 0 ))
+		-- Destination ID - 4 bytes
+		AppendArray( data, { 0xFF, 0xFF, 0xFF, 0xFF } )
+		-- Sender ID - 4 bytes
+		AppendArray( data, GetBaseIdArray() )
+		-- Status - 1 byte
+		table.insert( data, 0xF )
+		-- Compose and send the packet.
+		AddToSendQueue( PACKET_TYPES.RADIO, data )
+		--------------------------------------------------------------------------------------------
+		-- Stage 3: Receive the Query ID answer and create the device.
+		--------------------------------------------------------------------------------------------
+		g_teachInStage = TEACHIN_STAGES.RECV_QUERY_ID_ANSWER
+		luup.variable_set( SID.ENOCEAN, "TeachInStage", g_teachInStage, g_parentDevice )
+		ShowSysMessage( "Teach-in stage ".. g_teachInStage .."/".. NUM_TEACHIN_STAGES, SYS_MESSAGE_TYPES.BUSY, true )
+		log( "(EnOceanPlugin::TeachIn) 'Query ID' command added in the send queue, wait for answer" )
+	------------------------------------------------------------------------------------------------
+	-- Stage 4: Send 'Enter learn mode' remote procedure call (RPC)
+	------------------------------------------------------------------------------------------------
+	elseif g_teachInStage == TEACHIN_STAGES.SEND_START_LEARN_RPC then
+
+		local data = {}
+		-- RORG - 1 byte
+		table.insert( data, TELEGRAM_TYPES["ADT"] )
+		-- Encapsulate a SYS_EX telegram - 9 bytes
+		-- Meaning of 1: EEP=0, Flag=1 (start learn)
+		AppendArray( data, CreateSysExTelegram( RPC.REMOTE_LEARN, 1 ))
+		-- Destination ID - 4 bytes
+		AppendArray( data, { 0xFF, 0xFF, 0xFF, 0xFF } )
+		-- Sender ID - 4 bytes
+		AppendArray( data, GetBaseIdArray() )
+		-- Status - 1 byte
+		table.insert( data, 0xF )
+		-- Compose and send the packet.
+		AddToSendQueue( PACKET_TYPES.RADIO, data )
+		-- Wait a few seconds before moving on to the next stage.
+		log( "(EnOceanPlugin::TeachIn) 'Remote learn' command added in the send queue,"..
+			" wait ".. DELAYS.TEACHIN_STAGES .." seconds and move to the next stage" )
+		luup.call_delay( "TeachIn", DELAYS.TEACHIN_STAGES, TEACHIN_STAGES.SEND_TEACHIN_TELEGRAM )
+	------------------------------------------------------------------------------------------------
+	-- Stage 5: Send Teach-in RPS telegram
+	------------------------------------------------------------------------------------------------
+	elseif g_teachInStage == TEACHIN_STAGES.SEND_TEACHIN_TELEGRAM then
+
+		local data = {}
+		-- RORG - 1 byte
+		table.insert( data, TELEGRAM_TYPES["RPS"] )
+		-- Data byte
+		-- Meaning of 0x10: button AI pressed, no 2nd action
+		table.insert( data, 0x10 )
+		-- Sender ID - 4 bytes
+		AppendArray( data, GetBaseIdArray() )
+		-- Status - 1 byte
+		-- Meaning of 0x30: T21=1, NU=1
+		table.insert( data, 0x30 )
+		-- Compose and send the packet.
+		AddToSendQueue( PACKET_TYPES.RADIO, data )
+		-- Wait a few seconds before moving on to the next stage.
+		log( "(EnOceanPlugin::TeachIn) RPS telegram for teach-in added in the send queue,"..
+			" wait ".. DELAYS.TEACHIN_STAGES .." seconds and move to the next stage" )
+		luup.call_delay( "TeachIn", DELAYS.TEACHIN_STAGES, TEACHIN_STAGES.SEND_STOP_LEARN_RPC )
+	------------------------------------------------------------------------------------------------
+	-- Stage 6: Send 'Stop learn' RPC
+	------------------------------------------------------------------------------------------------
+	elseif g_teachInStage == TEACHIN_STAGES.SEND_STOP_LEARN_RPC then
+
+		local data = {}
+		-- RORG - 1 byte
+		table.insert( data, TELEGRAM_TYPES["ADT"] )
+		-- Encapsulate a SYS_EX telegram - 9 bytes
+		-- Meaning of 1: EEP=0, Flag=3 (stop learn)
+		AppendArray( data, CreateSysExTelegram( RPC.REMOTE_LEARN, 3 ))
+		-- Destination ID - 4 bytes
+		AppendArray( data, { 0xFF, 0xFF, 0xFF, 0xFF } )
+		-- Sender ID - 4 bytes
+		AppendArray( data, GetBaseIdArray() )
+		-- Status - 1 byte
+		table.insert( data, 0xF )
+		-- Compose and send the packet.
+		AddToSendQueue( PACKET_TYPES.RADIO, data )
+		-- Wait a few seconds before moving on to the next stage.
+		log( "(EnOceanPlugin::TeachIn) 'Remote learn' command added in the send queue,"..
+			" wait ".. DELAYS.TEACHIN_STAGES .." seconds and move to the next stage" )
+		luup.call_delay( "TeachIn", DELAYS.TEACHIN_STAGES, TEACHIN_STAGES.SEND_LOCK_RMCC )
+	------------------------------------------------------------------------------------------------
+	-- Stage 7: Send 'Lock' RMCC
+	------------------------------------------------------------------------------------------------
+	elseif g_teachInStage == TEACHIN_STAGES.SEND_LOCK_RMCC then
+
+		local pinCode = luup.variable_get( SID.ENOCEAN, "PinCode", g_parentDevice )
+		pinCode = tonumber(  pinCode, 10 ) or 0
+		if pinCode == 0 then
+			luup.variable_set( SID.ENOCEAN, "TeachInStage", TEACHIN_STAGES.NONE, g_parentDevice )
+			ShowSysMessage( "ERROR: missing or invalid PIN code", SYS_MESSAGE_TYPES.ERROR )
+			return
+		end
+		-- Clear the PinCode variable.
+		luup.variable_set( SID.ENOCEAN, "PinCode", "", g_parentDevice )
+		------------------------------------------------------------------------
+		-- Compose the 'Lock' command.
+		------------------------------------------------------------------------
+		local data = {}
+		-- RORG - 1 byte
+		table.insert( data, TELEGRAM_TYPES["ADT"] )
+		-- Encapsulate a SYS_EX telegram - 9 bytes
+		AppendArray( data, CreateSysExTelegram( RMCC.LOCK, pinCode ))
+		-- Destination ID - 4 bytes
+		AppendArray( data, { 0xFF, 0xFF, 0xFF, 0xFF } )
+		-- Sender ID - 4 bytes
+		AppendArray( data, GetBaseIdArray() )
+		-- Status - 1 byte
+		table.insert( data, 0xF )
+		-- Compose and send the packet.
+		AddToSendQueue( PACKET_TYPES.RADIO, data )
+		-- This was the last stage.
+		log( "(EnOceanPlugin::TeachIn) 'Lock' command added in the send queue. Set stage to NONE" )
+		luup.variable_set( SID.ENOCEAN, "TeachInStage", TEACHIN_STAGES.NONE, g_parentDevice )
+	------------------------------------------------------------------------------------------------
+	else
+		log( "(EnOceanPlugin::TeachIn) ERROR: invalid stage" )
+		luup.variable_set( SID.ENOCEAN, "TeachInStage", stage, g_parentDevice )
+		luup.variable_set( SID.ENOCEAN, "PinCode", "", g_parentDevice )
+	end
+end
+
+
+-- Send a RPS radio telegram (F6-xx profile).
+function SetTarget (newTargetValue, device)
+	-- Get the destination ID.
+	local destId = luup.devices[device].id:match("^%x%x%-%x%x%-%x%x%-%x%x")
+	if not destId then
+		ShowSysMessage( "ERROR: invalid destination ID", SYS_MESSAGE_TYPES.ERROR )
+		return
+	end
+	local destIdArray = SplitString( destId, '-' )
+	-- Convert the elements of the destIdArray to numbers.
+	for i = 1, #destIdArray do
+		destIdArray[i] = tonumber( destIdArray[i], 16 ) or 0
+	end
+
+	----------------------------------------------------------------------------
+	-- Compose the data.
+	----------------------------------------------------------------------------
+	local data = {}
+	-- RORG - 1 byte
+	table.insert( data, TELEGRAM_TYPES["RPS"] )
+	-- Data byte
+	if tostring( newTargetValue ) == "1" then
+		-- 0x10: button AI pressed, no 2nd action
+		table.insert( data, 0x10 )
+	else
+		-- 0x10: button A0 pressed, no 2nd action
+		table.insert( data, 0x30 )
+	end
+	-- Sender ID - 4 bytes
+	AppendArray( data, GetBaseIdArray() )
+	-- Status - 1 byte
+	-- Meaning of 0x30: T21=1, NU=1
+	table.insert( data, 0x30 )
+	----------------------------------------------------------------------------
+	-- Compose the optional data.
+	----------------------------------------------------------------------------
+	local optData = {}
+	-- The sub-telegram number (apparently it should be 3)
+	table.insert( optData, 0x3 )
+	-- The destination ID
+	AppendArray( optData, destIdArray )
+	-- The dBm
+	table.insert( optData, 0xFF )
+	-- The security level
+	table.insert( optData, 0x0 )
+	-- Compose and send the packet.
+	AddToSendQueue( PACKET_TYPES.RADIO, data, optData )
 end
 
@@ -990,9 +1454,19 @@
 -- Startup functions
 ------------------------------------------------------------------------------------------------------------------------
+
+local function GenerateBaseId()
+	-- Generate a random number in interval [0 - 65535].
+	-- 65536 is the number of possible base IDs as of current date (20-03-2013).
+	math.randomseed( os.time() )
+	local multiplier = math.random( (MAX_BASE_ID - MIN_BASE_ID) / BASE_ID_GAP + 1 ) - 1
+	-- The base ID is a number between MIN_BASE_ID and MAX_BASE_ID, and a multiplier of BASE_ID_GAP.
+	return MIN_BASE_ID + BASE_ID_GAP * multiplier
+end
+
 
 local function GetChildDevices()
 	-- Get the number of learned EnOcean devices.
-	g_numDevices = luup.variable_get( SID.ENOCEAN, "NumDevices", g_parentDevice )
-	g_numDevices = tonumber( g_numDevices, 10 ) or 0
+	g_maxId = luup.variable_get( SID.ENOCEAN, "MaxID", g_parentDevice )
+	g_maxId = tonumber( g_maxId, 10 ) or 0
 	-- Get a list with all our child devices.
 	for k, v in pairs( luup.devices ) do
@@ -1033,8 +1507,24 @@
 	luup.variable_set( SID.ENOCEAN, "LearnMode", "0", lul_device )
 
+	-- Check if we have a base ID. Generate one if we don't.
+	g_baseId = luup.variable_get( SID.ENOCEAN, "BaseID", lul_device ) or ""
+	if g_eoDeviceId == "" then
+		g_baseId = GenerateBaseId()
+		luup.variable_set( SID.ENOCEAN, "BaseID", g_baseId, lul_device )
+	end
+
+	-- Get the list of the child devices.
 	GetChildDevices()
 
-	-- Get the sender ID of the TCM300.
-	--AddToSendQueue( PACKET_TYPES.COMMON_COMMAND, COMMON_COMMAND.CO_RD_IDBASE, "", true )
+	-- Get the TeachIn-in stage.
+	local teachInStage = luup.variable_get( SID.ENOCEAN, "TeachInStage", lul_device )
+	teachInStage = tonumber( teachInStage, 10 ) or 0
+	if teachInStage > 0 then
+		if teachInStage == TEACHIN_STAGES.RECV_QUERY_ID_ANSWER then
+			ShowSysMessage( "Teach-in stage ".. teachInStage .."/".. NUM_TEACHIN_STAGES, SYS_MESSAGE_TYPES.BUSY, true )
+		else
+			TeachIn( teachInStage )
+		end
+	end
 
 	debug( "(EnOceanPlugin::Init) Success: startup successful" )
Index: /trunk/D_EnOceanGateway1.json
===================================================================
--- /trunk/D_EnOceanGateway1.json	(revision 20)
+++ /trunk/D_EnOceanGateway1.json	(revision 30)
@@ -57,5 +57,5 @@
 					"left": "1",
 					"Label": {
-						"lang_tag": "cmd_learn_mode_on",
+						"lang_tag": "button_learn_mode_on",
 						"text": "Learn"
 					},
@@ -87,5 +87,5 @@
 					"left": "0",
 					"Label": {
-						"lang_tag": "cmd_learn_mode_off",
+						"lang_tag": "button_learn_mode_off",
 						"text": "Normal"
 					},
@@ -110,4 +110,52 @@
 					},
 					"ControlCode": "learn_mode_off"
+				},
+				{
+					"ControlType": "label",
+					"Label": {
+						"lang_tag": "label_pin_code",
+						"text": "PIN Code:"
+					},
+					"Display": {
+						"Top": 105,
+						"Left": 50,
+						"Width": 75,
+						"Height": 20
+					}
+				},
+				{
+					"ControlType": "input",
+					"ID": "pinCode",
+					"Display": {
+						"Top": 125,
+						"Left": 50,
+						"Width": 75,
+						"Height": 20
+					},
+					"ControlCode": "teach_in_input"
+				},
+				{
+					"ControlType": "button",
+					"Label": {
+						"lang_tag": "button_teach_in",
+						"text": "Teach-in"
+					},
+					"Display": {
+						"Top": 125,
+						"Left": 145,
+						"Width": 75,
+						"Height": 20
+					},
+					"Command": {
+						"Service": "urn:micasaverde-com:serviceId:EnOceanGateway1",
+						"Action": "TeachIn",
+						"Parameters": [
+							{
+								"Name": "pinCode",
+								"ID": "pinCode"
+							}
+						]
+					},
+					"ControlCode": "teach_in"
 				}
 			]
