cul2mqtt.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881
  1. #!/usr/bin/python3 -u
  2. #
  3. # pip3 install pyyaml paho-mqtt pyserial
  4. import yaml
  5. from yaml.constructor import ConstructorError
  6. import serial
  7. import sys
  8. import time
  9. from time import sleep
  10. from time import localtime, strftime
  11. import datetime
  12. import paho.mqtt.client as mqtt
  13. import json
  14. import os
  15. import re
  16. import configparser
  17. version = 0.3
  18. # Change working dir to the same dir as this script
  19. os.chdir(sys.path[0])
  20. config = configparser.ConfigParser()
  21. config.read('cul2mqtt.ini')
  22. # global variables
  23. verbose = False
  24. debug = False
  25. quiet = True
  26. serialCULAvailable = False
  27. devdata = {}
  28. RXcodesToDevFunction_IT = {}
  29. RXcodesToDevFunction_RAW = {}
  30. InTopicsToDevIds = {}
  31. # config vars
  32. deviceConfigFile = config['main'].get('devices_config_yml')
  33. log_enable = config['main'].getboolean('log_enable')
  34. log_path = config['main'].get('log_path')
  35. if not os.path.exists(log_path):
  36. os.makedirs(log_path)
  37. mqtt_server = config['mqtt'].get('server')
  38. mqtt_port = config['mqtt'].getint('port')
  39. mqtt_user = config['mqtt'].get('user')
  40. mqtt_password = config['mqtt'].get('password')
  41. TX_interface_prefer = config['cul'].get('TX_interface_prefer') # UART or MQTT
  42. if TX_interface_prefer is None: # compatibility with old ini file structure
  43. TX_interface_prefer = config['main'].get('TX_interface_prefer') # UART or MQTT
  44. repeat_received_commands = False # not yet implemented
  45. if len(sys.argv) >= 2:
  46. if sys.argv[1] == "-q":
  47. verbose = False
  48. debug = False
  49. quiet = True
  50. elif sys.argv[1] == "-v":
  51. verbose = True
  52. debug = False
  53. quiet = False
  54. elif sys.argv[1] == "-d":
  55. verbose = True
  56. debug = True
  57. quiet = False
  58. # serial (USB) CUL device
  59. receive_from_serial_cul = config['cul'].getboolean('receive_from_serial_cul')
  60. send_on_serial_cul = config['cul'].getboolean('send_on_serial_cul')
  61. serialPort = config['cul'].get('serialPort')
  62. serialBaudrate = config['cul'].getint('serialBaudrate')
  63. serialTimeout = config['cul'].getint('serialTimeout')
  64. # CUL init command for normal operation, i.E. X21, X05
  65. culInitCmd = config['cul'].get('culInitCmd') + '\r\n'
  66. culSendsRSSI = config['cul'].getboolean('culSendsRSSI') # set depending on culInitCmd chosen
  67. serialCulInitTimeout = config['cul'].getint('serialCulInitTimeout')
  68. forceSerialCULConnected = config['cul'].getboolean('forceSerialCULConnected')
  69. # MQTT CUL
  70. receive_from_mqtt_cul = config['cul'].getboolean('receive_from_mqtt_cul')
  71. send_on_mqtt_cul = config['cul'].getboolean('send_on_mqtt_cul')
  72. mqtt_cul_topic_received = config['cul'].get('mqtt_cul_topic_received')
  73. mqtt_cul_topic_send = config['cul'].get('mqtt_cul_topic_send')
  74. filterSelfSentIncomingTimeout = config['main'].get('filterSelfSentIncomingTimeout')
  75. try:
  76. from yaml import CLoader as Loader
  77. except ImportError:
  78. from yaml import Loader
  79. def no_duplicates_constructor(loader, node, deep=False):
  80. """Check for duplicate keys."""
  81. mapping = {}
  82. for key_node, value_node in node.value:
  83. key = loader.construct_object(key_node, deep=deep)
  84. value = loader.construct_object(value_node, deep=deep)
  85. if key in mapping:
  86. raise ConstructorError("while constructing a mapping", node.start_mark, "found duplicate key (%s)" % key, key_node.start_mark)
  87. mapping[key] = value
  88. return loader.construct_mapping(node, deep)
  89. yaml.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, no_duplicates_constructor)
  90. log_last_date = None
  91. logfilehandle = False
  92. def log_start():
  93. global logfilehandle, log_last_date
  94. if log_enable:
  95. if not os.path.exists(log_path):
  96. os.makedirs(log_path)
  97. try:
  98. _log_current_date = strftime("%Y%m%d")
  99. _logfilename = _log_current_date + ".log"
  100. logfilehandle = open(log_path + '/' + _logfilename, 'a')
  101. log_last_date = _log_current_date
  102. except:
  103. pass
  104. def log_rotate():
  105. global logfilehandle, log_last_date
  106. if log_enable:
  107. _log_current_date = strftime("%Y%m%d")
  108. if log_last_date != _log_current_date:
  109. try:
  110. logfilehandle.close()
  111. _logfilename = _log_current_date + ".log"
  112. logfilehandle = open(log_path + '/' + _logfilename, 'a')
  113. log_last_date = _log_current_date
  114. except:
  115. pass
  116. def log_write(_msg):
  117. global logfilehandle
  118. if not quiet: print(_msg)
  119. log_rotate()
  120. if log_enable:
  121. try:
  122. logfilehandle.write("[" + str(datetime.datetime.now()) + "] " + _msg + "\n")
  123. logfilehandle.flush()
  124. except:
  125. # guat dann hoit ned...
  126. pass
  127. log_start()
  128. log_write("CUL2MQTT v" + str(version))
  129. log_write("=====================================")
  130. try:
  131. with open(deviceConfigFile) as devfile:
  132. devdata = yaml.load(devfile, Loader=yaml.FullLoader)
  133. if debug:
  134. log_write("")
  135. log_write("")
  136. log_write("==== parsing config file ====")
  137. log_write("")
  138. log_write(devdata)
  139. log_write("")
  140. for deviceid in devdata:
  141. if debug:
  142. log_write("")
  143. log_write("")
  144. log_write("Device: " + deviceid)
  145. log_write(devdata[deviceid])
  146. if "RX" in devdata[deviceid].keys():
  147. if devdata[deviceid]["RX"] != "":
  148. if debug:
  149. log_write("RX codes:")
  150. for key, value in devdata[deviceid]["RX"].items():
  151. if debug:
  152. log_write(str(key) + "->" + str(value))
  153. if value.startswith("i"):
  154. # is Intertechno RX code
  155. if value in RXcodesToDevFunction_IT.keys():
  156. log_write("")
  157. log_write("")
  158. log_write("ERROR: RX-string '" + str(value) + "' is already defined for another device! Must be unique.")
  159. raise
  160. else:
  161. RXcodesToDevFunction_IT[value] = deviceid, key
  162. else:
  163. # is other RX code - lets call it RAW
  164. if value in RXcodesToDevFunction_RAW.keys():
  165. log_write("")
  166. log_write("")
  167. log_write("ERROR: RX-string '" + str(value) + "' is already defined for another device! Must be unique.")
  168. raise
  169. else:
  170. RXcodesToDevFunction_RAW[value] = deviceid, key
  171. if "cmdTopic" in devdata[deviceid].keys():
  172. if devdata[deviceid]["cmdTopic"] != "":
  173. cmdTopic = devdata[deviceid]["cmdTopic"]
  174. if debug:
  175. log_write("cmdTopic: " + cmdTopic)
  176. if cmdTopic in InTopicsToDevIds.keys():
  177. log_write("")
  178. log_write("")
  179. log_write("ERROR: cmdTopic '" + str(cmdTopic) + "' is already defined for another device! Must be unique.")
  180. raise
  181. else:
  182. InTopicsToDevIds[cmdTopic] = deviceid
  183. if debug:
  184. log_write("")
  185. log_write("")
  186. log_write("")
  187. log_write("RXcodesToDevFunction_IT:")
  188. log_write(RXcodesToDevFunction_IT)
  189. log_write("")
  190. log_write("")
  191. log_write("RXcodesToDevFunction_RAW:")
  192. log_write(RXcodesToDevFunction_RAW)
  193. log_write("")
  194. log_write("")
  195. log_write("InTopicsToDevIds:")
  196. log_write(InTopicsToDevIds)
  197. log_write("")
  198. log_write("")
  199. log_write("InTopicsToDevIds.keys():")
  200. log_write(InTopicsToDevIds.keys())
  201. log_write("")
  202. log_write("")
  203. log_write("devdata.keys():")
  204. log_write(devdata.keys())
  205. log_write("")
  206. log_write("")
  207. log_write("==== parsing config file complete ====")
  208. log_write("")
  209. log_write("")
  210. log_write("")
  211. log_write("")
  212. except ConstructorError as err:
  213. log_write("ERROR on parsing configfile:")
  214. log_write(err)
  215. exit(1)
  216. except:
  217. log_write("ERROR opening configfile")
  218. log_write("Unexpected error: " + str(sys.exc_info()[0]))
  219. exit(1)
  220. lastReceivedMaxAge = config['main'].getint('lastReceivedMaxAge') # ignore repeated messages when they are younger than x ms
  221. lastReceivedTime = dict()
  222. lastSentTime = {}
  223. lastSentMinInterval = config['main'].getint('lastSentMinInterval') # ignore repeated messages when they are younger than x ms, should be > 1500
  224. lastSentCmd = ""
  225. lastSentCmdTime = 0
  226. lastSentDev = ""
  227. lastSentDevCmd = ""
  228. def touch(fname, times=None):
  229. with open(fname, 'a'):
  230. os.utime(fname, times)
  231. def on_connect(client, userdata, flags, rc):
  232. if verbose:
  233. log_write("MQTT connected with result code " + str(rc))
  234. if receive_from_mqtt_cul:
  235. if mqtt_cul_topic_received != "":
  236. client.subscribe(mqtt_cul_topic_received)
  237. if mqtt_cul_topic_send != "":
  238. #client.publish
  239. mqttc.publish(mqtt_cul_topic_send, culInitCmd, qos=0, retain=False)
  240. for in_topic in InTopicsToDevIds.keys():
  241. if in_topic != "":
  242. client.subscribe(in_topic)
  243. if verbose:
  244. log_write("MQTT subscribed: " + in_topic)
  245. def on_disconnect(client, userdata, rc):
  246. if rc != 0:
  247. log_write("Unexpected MQTT disconnection. Will auto-reconnect")
  248. def on_message(client, userdata, msg):
  249. #print(msg.topic + ": " + str(msg.payload))
  250. payload = msg.payload.decode("utf-8")
  251. if verbose:
  252. log_write("")
  253. log_write("MQTT received: " + msg.topic + " -> " + str(payload))
  254. # MQTT message is output from CUL
  255. if receive_from_mqtt_cul and msg.topic == mqtt_cul_topic_received:
  256. payload = payload.rstrip()
  257. # support output from Tasmota SerialBridge on tele/[DEVICE]/RESULT topic
  258. # example: {"SerialReceived":"p10 144 80 480 48 32 1360 28 1 3 4 160 6336 0 735C1470"}
  259. if payload.startswith('{"SerialReceived":'):
  260. payload_json = json.loads(payload)
  261. payload = payload_json.get('SerialReceived')
  262. if payload != None:
  263. cul_received(payload, "MQTT") # to lower case as MQTT CUL via Tasmota SerialBridge with Rule changes output to all uppercase
  264. if verbose:
  265. log_write("MQTT-CUL RX: '" + payload + "'")
  266. elif not payload.startswith('{'):
  267. # ignore everything different starting with {
  268. cul_received(payload.lower(), "MQTT") # to lower case as MQTT CUL via Tasmota SerialBridge with Rule changes output to all uppercase
  269. if verbose:
  270. log_write("MQTT-CUL RX: '" + payload.lower() + "'")
  271. else:
  272. for in_topic, dev in InTopicsToDevIds.items():
  273. if msg.topic == in_topic:
  274. if verbose: log_write("MQTT received - '" + msg.topic + "' = '" + payload + "' => DEV: " + dev)
  275. if 'name' in devdata[dev].keys():
  276. log_write('devName: ' + devdata[dev]['name'])
  277. global lastSentDev, lastSentDevCmd, lastSentCmdTime
  278. now = int(round(time.time() * 1000))
  279. if debug:
  280. log_write("dev="+dev+", lastSentDevCmd="+lastSentDevCmd)
  281. if dev == lastSentDev and payload == lastSentDevCmd and (now - lastSentCmdTime) < 1000:
  282. if verbose:
  283. log_write("MQTT: ignored command as we just sent this.")
  284. else:
  285. cul_send(dev, payload)
  286. if 'statTopic' in devdata[dev].keys():
  287. if verbose: log_write('statTopic: ' + devdata[dev]['statTopic'])
  288. mqttc.publish(devdata[dev]['statTopic'], payload, qos=0, retain=False)
  289. if 'add_statTopics_on' in devdata[dev].keys():
  290. if verbose: log_write("add_statTopics_on:")
  291. for res in devdata[dev]['add_statTopics_on']:
  292. if 'on_payload' in res and 'topic' in res and 'payload' in res:
  293. if payload == res['on_payload'] and payload != "" and res['topic'] != "" and res['payload'] != "":
  294. if verbose: log_write(" on '" + payload + "': '" + res['payload'] + "' => '" + res['topic'] + "'")
  295. mqttc.publish(res['topic'], res['payload'], qos=0, retain=False)
  296. def publish_device_statusupdate(device, cmd, value):
  297. if device in devdata.keys():
  298. if 'statTopic' in devdata[device].keys():
  299. statTopic = devdata[device].get('statTopic')
  300. if value is not None and cmd == "%VALUE%":
  301. if verbose: log_write(" MQTT publish: '" + value + "' -> '" + statTopic + "'")
  302. mqttc.publish(statTopic, value, qos=0, retain=False)
  303. else:
  304. if verbose: log_write(" MQTT publish: '" + cmd + "' -> '" + statTopic + "'")
  305. mqttc.publish(statTopic, cmd, qos=0, retain=False)
  306. if 'add_statTopics_on' in devdata[device].keys():
  307. if verbose: log_write(" MQTT publish add_statTopics_on:")
  308. for res in devdata[device].get('add_statTopics_on'):
  309. if 'on_payload' in res and 'topic' in res and 'payload' in res:
  310. if cmd == res['on_payload']:
  311. if verbose: log_write(" on '" + res['on_payload'] + "' -> publish '" + res['payload'] + "' on topic '" + res['topic'] + "'")
  312. mqttc.publish(res['topic'], res['payload'], qos=0, retain=False)
  313. if 'add_statTopics' in devdata[device].keys():
  314. if verbose: log_write(" MQTT publish on add_statTopics:")
  315. for res in devdata[device]['add_statTopics']:
  316. if verbose: log_write(" '" + cmd + "' -> '" + res + "'")
  317. mqttc.publish(res, cmd, qos=0, retain=False)
  318. def parseRXCode(rx_code, source_cul):
  319. receivedForDevice = None
  320. receivedCmnd = None
  321. receivedValue = None
  322. if rx_code.startswith("i"):
  323. # parse Intertechno RX code
  324. if debug: log_write(" PROTOCOL: Intertechno")
  325. sucessfullyParsedITCode = False
  326. # look if this code is in the device_config
  327. if rx_code in RXcodesToDevFunction_IT:
  328. if debug: log_write(" code found in device config")
  329. receivedForDevice = RXcodesToDevFunction_IT[rx_code][0]
  330. receivedCmnd = RXcodesToDevFunction_IT[rx_code][1]
  331. sucessfullyParsedITCode = True
  332. if debug: log_write(" DEV: " + receivedForDevice + ", CMD: " + receivedCmnd + ", RX: " + rx_code)
  333. if verbose:
  334. #log_write("")
  335. log_write(" CUL '" + source_cul + "' received '" + rx_code + "' => DEV: " + receivedForDevice + ", CMD: " + receivedCmnd)
  336. # if this had no result -> try again with last 16 bit stripped, as in many cases this is only RSSI and not part of the code
  337. # in this case treat this last 16bit as sensor value
  338. rx_code_stripped = rx_code[0:-2]
  339. if not sucessfullyParsedITCode and rx_code_stripped in RXcodesToDevFunction_IT:
  340. if debug: log_write(" code found in device config with last 16 bit stripped")
  341. receivedForDevice = RXcodesToDevFunction_IT[rx_code_stripped][0]
  342. receivedCmnd = RXcodesToDevFunction_IT[rx_code_stripped][1]
  343. receivedValue = rx_code[-2:]
  344. # convert value to decimal if enabled for this device
  345. if "convertValueToDecimal" in devdata[receivedForDevice]:
  346. if devdata[receivedForDevice]["convertValueToDecimal"] == True:
  347. if debug: log_write(" converting value to decimal")
  348. receivedValue = str(int(receivedValue, base=16))
  349. sucessfullyParsedITCode = True
  350. if debug: log_write(" DEV: " + receivedForDevice + ", CMD: " + receivedCmnd + ", VALUE: " + receivedValue + ", RX: " + rx_code_stripped)
  351. if verbose:
  352. #log_write("")
  353. log_write(" CUL '" + source_cul + "' received '" + rx_code_stripped + "' => DEV: " + receivedForDevice + ", CMD: " + receivedCmnd + ", VALUE: " + receivedValue)
  354. # if this also did not work, try to parse it as "default/cheap" Intertechno code...
  355. if not sucessfullyParsedITCode:
  356. if debug: log_write(" treat code as 'default/cheap' Intertechno code...")
  357. try:
  358. receivedForDevice, receivedCmnd = decodeInterTechnoRX(rx_code_stripped)
  359. except:
  360. log_write(" Error parsing code as 'default/cheap' Intertechno code.")
  361. if debug:
  362. log_write(str(receivedForDevice) + ", " + str(receivedCmnd))
  363. else:
  364. # parse other/RAW RX code
  365. if debug: log_write(" PROTOCOL: OTHER/RAW")
  366. if rx_code in RXcodesToDevFunction_RAW:
  367. receivedForDevice = RXcodesToDevFunction_RAW[rx_code][0]
  368. receivedCmnd = RXcodesToDevFunction_RAW[rx_code][1]
  369. if debug: log_write(" DEV: " + receivedForDevice + ", CMD: " + receivedCmnd + ", RX: " + rx_code)
  370. if verbose:
  371. #log_write("")
  372. log_write(" CUL '" + source_cul + "' received '" + rx_code + "' => DEV: " + receivedForDevice + ", CMD: " + receivedCmnd)
  373. #if debug:
  374. # log_write(" DEV: " + str(receivedForDevice) + ", CMD: " + str(receivedCmnd) + ", RX: " + rx_code)
  375. if receivedForDevice != None and receivedCmnd != None and receivedForDevice != False and receivedCmnd != False:
  376. publish_device_statusupdate(receivedForDevice, receivedCmnd, receivedValue)
  377. def decodeInterTechnoRX(rx_code):
  378. # decode old fixed code from Intertechno remotes
  379. _housecode = None
  380. _devaddr = None
  381. _command = None
  382. _itname = None
  383. #print(rx_code[0:1])
  384. #print(rx_code[1:3])
  385. #print(rx_code[3:5])
  386. #print(rx_code[5:7])
  387. if rx_code[0:1] == "i":
  388. if rx_code[1:3] == "00": _housecode = "A"
  389. elif rx_code[1:3] == "40": _housecode = "B"
  390. elif rx_code[1:3] == "10": _housecode = "C"
  391. elif rx_code[1:3] == "50": _housecode = "D"
  392. elif rx_code[1:3] == "04": _housecode = "E"
  393. elif rx_code[1:3] == "44": _housecode = "F"
  394. elif rx_code[1:3] == "14": _housecode = "G"
  395. elif rx_code[1:3] == "54": _housecode = "H"
  396. elif rx_code[1:3] == "01": _housecode = "I"
  397. elif rx_code[1:3] == "41": _housecode = "J"
  398. elif rx_code[1:3] == "11": _housecode = "K"
  399. elif rx_code[1:3] == "51": _housecode = "L"
  400. elif rx_code[1:3] == "05": _housecode = "M"
  401. elif rx_code[1:3] == "45": _housecode = "N"
  402. elif rx_code[1:3] == "15": _housecode = "O"
  403. elif rx_code[1:3] == "55": _housecode = "P"
  404. if rx_code[3:5] == "00": _devaddr = "1"
  405. elif rx_code[3:5] == "40": _devaddr = "2"
  406. elif rx_code[3:5] == "10": _devaddr = "3"
  407. elif rx_code[3:5] == "50": _devaddr = "4"
  408. elif rx_code[3:5] == "04": _devaddr = "5"
  409. elif rx_code[3:5] == "44": _devaddr = "6"
  410. elif rx_code[3:5] == "14": _devaddr = "7"
  411. elif rx_code[3:5] == "54": _devaddr = "8"
  412. elif rx_code[3:5] == "01": _devaddr = "9"
  413. elif rx_code[3:5] == "41": _devaddr = "10"
  414. elif rx_code[3:5] == "11": _devaddr = "11"
  415. elif rx_code[3:5] == "51": _devaddr = "12"
  416. elif rx_code[3:5] == "05": _devaddr = "13"
  417. elif rx_code[3:5] == "45": _devaddr = "14"
  418. elif rx_code[3:5] == "15": _devaddr = "15"
  419. elif rx_code[3:5] == "55": _devaddr = "16"
  420. if rx_code[5:7] == "15": _command = "ON"
  421. elif rx_code[5:7] == "14": _command = "OFF"
  422. if _housecode != None and _devaddr != None and _command != None:
  423. _itname = "IT_" + _housecode + _devaddr
  424. if debug: log_write("valid IT code: '" + _itname + "' => '" + _command + "'")
  425. return _itname, _command
  426. else:
  427. if debug: log_write("unknown or invalid IT code '" + rx_code + "'")
  428. return False, False
  429. else:
  430. if debug: log_write("unknown or invalid IT code '" + rx_code + "'")
  431. def encodeInterTechnoRX(itname, cmd):
  432. # decode old fixed code from InterTechno remotes
  433. _housecode = None
  434. _devaddr = None
  435. _command = None
  436. #print(itname[0:3])
  437. #print(itname[3:4])
  438. #print(itname[4:])
  439. if itname[0:3] == "IT_":
  440. if itname[3:4] == "A": _housecode = "00"
  441. elif itname[3:4] == "B": _housecode = "40"
  442. elif itname[3:4] == "C": _housecode = "10"
  443. elif itname[3:4] == "D": _housecode = "50"
  444. elif itname[3:4] == "E": _housecode = "04"
  445. elif itname[3:4] == "F": _housecode = "44"
  446. elif itname[3:4] == "G": _housecode = "14"
  447. elif itname[3:4] == "H": _housecode = "54"
  448. elif itname[3:4] == "I": _housecode = "01"
  449. elif itname[3:4] == "J": _housecode = "41"
  450. elif itname[3:4] == "K": _housecode = "11"
  451. elif itname[3:4] == "L": _housecode = "51"
  452. elif itname[3:4] == "M": _housecode = "05"
  453. elif itname[3:4] == "N": _housecode = "45"
  454. elif itname[3:4] == "O": _housecode = "15"
  455. elif itname[3:4] == "P": _housecode = "55"
  456. if itname[4:] == "1": _devaddr = "00"
  457. elif itname[4:] == "2": _devaddr = "40"
  458. elif itname[4:] == "3": _devaddr = "10"
  459. elif itname[4:] == "4": _devaddr = "50"
  460. elif itname[4:] == "5": _devaddr = "04"
  461. elif itname[4:] == "6": _devaddr = "44"
  462. elif itname[4:] == "7": _devaddr = "14"
  463. elif itname[4:] == "8": _devaddr = "54"
  464. elif itname[4:] == "9": _devaddr = "01"
  465. elif itname[4:] == "10": _devaddr = "41"
  466. elif itname[4:] == "11": _devaddr = "11"
  467. elif itname[4:] == "12": _devaddr = "51"
  468. elif itname[4:] == "13": _devaddr = "05"
  469. elif itname[4:] == "14": _devaddr = "45"
  470. elif itname[4:] == "15": _devaddr = "15"
  471. elif itname[4:] == "16": _devaddr = "55"
  472. if cmd == "ON": _command = "15"
  473. elif cmd == "OFF": _command = "14"
  474. if debug: print("IT housecode=", _housecode, "- devaddr=", _devaddr, "- command=", _command)
  475. if _housecode != None and _devaddr != None and _command != None:
  476. _rxcode = "i" + _housecode + _devaddr + _command
  477. #print("encoded IT RX code: '" + itname + "' => '" + cmd + "' = '" + _rxcode)
  478. return _rxcode
  479. else:
  480. if debug: log_write(" unknown or invalid IT code '" + rx_code + "'")
  481. return False
  482. else:
  483. if debug: log_write(" unknown or invalid IT code '" + rx_code + "'")
  484. def cul_received(payload, source_cul):
  485. global lastReceivedTime, lastReceivedMaxAge
  486. if payload[:2] == 'is': # msg is reply from CUL to raw send command
  487. pass
  488. elif payload[:1] == 'i': # is a IT compatible code - so look it up in the code table
  489. if culSendsRSSI:
  490. inCmd = payload[:-2] # strip last 2 chars, depending on used CUL receive mode - if enabled this is only RSSI
  491. else:
  492. inCmd = payload
  493. if verbose:
  494. log_write(" inCmd: " + inCmd + ", receiving CUL: " + source_cul)
  495. # filter fast repeated codes (strip first char on Intertechno codes as the repetation will come in as RAW without "i" prefix)
  496. ignoreCommand = False
  497. if inCmd in lastReceivedTime.keys():
  498. lastTime = int(lastReceivedTime[inCmd])
  499. now = int(round(time.time() * 1000))
  500. tdelta = (now - lastTime)
  501. if debug: log_write(" TDELTA = " + str(tdelta))
  502. if debug: log_write(" lastTime = " + str(lastTime))
  503. if tdelta < lastReceivedMaxAge:
  504. if verbose: log_write(" ignoring code from CUL '" + source_cul + "', CMD: '" + inCmd + "' - already received " + str(tdelta) + " ms ago")
  505. ignoreCommand = True
  506. if not ignoreCommand:
  507. lastReceivedTime[inCmd] = int(round(time.time() * 1000))
  508. parseRXCode(inCmd, source_cul)
  509. elif payload[:1] == 'p': # is RAW data
  510. # example: "p11 288 864 800 320 288 832 33 1 4 1 288 10224 0 A4CEF09580"
  511. # split string and extract last row as we dont need the rest
  512. splitPayload = payload.split(' ')
  513. actualPayload = splitPayload[len(splitPayload)-1]
  514. if debug: log_write(" actualPayload: '" + actualPayload)
  515. ignoreCommand = False
  516. # handle/filter repetations of Intertechno codes
  517. isITrepetation = False
  518. if ('i'+actualPayload) in lastReceivedTime.keys():
  519. isITrepetation = True
  520. if debug: log_write(" skipping repeated Intertechno code")
  521. lastTime = int(lastReceivedTime['i'+actualPayload])
  522. now = int(round(time.time() * 1000))
  523. tdelta = (now - lastTime)
  524. if debug: log_write(" TDELTA = " + str(tdelta))
  525. if tdelta < lastReceivedMaxAge:
  526. if verbose: log_write(" ignoring code from CUL '" + source_cul + "', CMD: '" + 'i'+actualPayload + "' - already received " + str(tdelta) + " ms ago")
  527. ignoreCommand = True
  528. # filter fast repeated codes
  529. if not isITrepetation:
  530. if actualPayload in lastReceivedTime.keys():
  531. lastTime = int(lastReceivedTime[actualPayload])
  532. now = int(round(time.time() * 1000))
  533. tdelta = (now - lastTime)
  534. if debug: log_write(" TDELTA = " + str(tdelta))
  535. if tdelta < lastReceivedMaxAge:
  536. if verbose: log_write(" ignoring code from CUL '" + source_cul + "', CMD: '" + actualPayload + "' - already received " + str(tdelta) + " ms ago")
  537. ignoreCommand = True
  538. if not ignoreCommand:
  539. if isITrepetation:
  540. # treat as Intertechno code
  541. lastReceivedTime['i'+actualPayload] = int(round(time.time() * 1000))
  542. parseRXCode('i'+actualPayload, source_cul)
  543. else:
  544. lastReceivedTime[actualPayload] = int(round(time.time() * 1000))
  545. parseRXCode(actualPayload, source_cul)
  546. #if repeat_received_commands:
  547. # lastSentLength = len(lastSent)
  548. # i = 0
  549. # dontRepeat = False
  550. # while i < lastSentLength:
  551. # #print(str(i) + ": " + lastReceived[i])
  552. # if lastSent[i] == decCmd:
  553. # lastTime = int(lastSentTime[i])
  554. # now = int(round(time.time() * 1000))
  555. # tdelta = (now - lastTime)
  556. # #print("TDELTA = " + str(tdelta))
  557. # if tdelta < lastSentMaxAge:
  558. # print("ignoring command as it originated from ourselfs " + inCmd + " " + str(tdelta) + " ms ago")
  559. # dontRepeat = True
  560. # #break
  561. # i += 1
  562. # #cmdToSend = culSendCmds[decCmd]
  563. # if not dontRepeat:
  564. # if device != "" and cmd != "":
  565. # print("REPEATING COMMAND: " + cmd + " TO DEVICE " + device)
  566. # cul_send(device, cmd)
  567. def IT_RXtoTXCode(itReceiveCode):
  568. if debug:
  569. statusstr = "IT_RXtoTXCode "
  570. statusstr += "RX: "
  571. statusstr += itReceiveCode
  572. #print("IT_RXtoTXCode ReceiveCode: " + itReceiveCode)
  573. itReceiveCode = itReceiveCode[1:] # remove first character "i"
  574. itReceiveCodeLengthBytes = int(len(itReceiveCode)/2)
  575. itReceiveCodeBytes = []
  576. itTransmitTristate = "is"
  577. for x in range(itReceiveCodeLengthBytes):
  578. itReceiveCodeBytes.append(bin(int(itReceiveCode[x*2:(x*2+2)],16))[2:].zfill(8))
  579. for x in range(len(itReceiveCodeBytes)):
  580. #print("IT REC byte " + str(x) + " = " + str(itReceiveCodeBytes[x]))
  581. for y in range(4):
  582. quarterbyte = str(itReceiveCodeBytes[x][y*2:y*2+2])
  583. if quarterbyte == "00":
  584. tmpTristate = "0";
  585. elif quarterbyte == "01":
  586. tmpTristate = "F";
  587. elif quarterbyte == "10":
  588. tmpTristate = "D";
  589. elif quarterbyte == "11":
  590. tmpTristate = "1";
  591. #print(quarterbyte + " -> " + tmpTristate)
  592. itTransmitTristate = itTransmitTristate + tmpTristate
  593. if debug:
  594. statusstr += " -> TX: "
  595. statusstr += itTransmitTristate
  596. #print("IT_RXtoTXCode TransmitCode: " + itTransmitTristate)
  597. log_write(statusstr)
  598. return(itTransmitTristate)
  599. def cul_send(device, cmd):
  600. global lastSentTime
  601. culCmd = ""
  602. culSendCmdsKeyName = device + ' ' + cmd
  603. if debug: log_write("CUL send '" + cmd + "' to device '" + device + "'")
  604. tx_code = False
  605. if 'TX' in devdata[device]:
  606. if debug: print("TX data available, cmd="+cmd)
  607. if debug: print(devdata[device]['TX'])
  608. if cmd in devdata[device]['TX'].keys():
  609. tx_code = devdata[device]['TX'][cmd]
  610. if verbose: log_write(" TX code for '" + cmd + "': " + tx_code)
  611. if not tx_code:
  612. if verbose: log_write(" deviceID: " + device)
  613. if 'RX' in devdata[device].keys():
  614. if verbose:
  615. log_write(" RX code configured, cmd=" + cmd)
  616. log_write(devdata[device]['RX'])
  617. if cmd in devdata[device]['RX'].keys():
  618. rx_code = devdata[device]['RX'][cmd]
  619. if debug: log_write(" RX code for '" + cmd + "': " + rx_code)
  620. tx_code = IT_RXtoTXCode(rx_code)
  621. if verbose: log_write(" TX code for '" + cmd + "': " + tx_code)
  622. else:
  623. log_write(" RX code for '" + cmd + "' NOT FOUND")
  624. elif device.startswith("IT_"):
  625. # InterTechno device with fixed code - encode RX code for IT device name and convert to TX code
  626. rx_code = encodeInterTechnoRX(device, cmd)
  627. if rx_code:
  628. if debug: log_write(" RX code for '" + cmd + "': " + rx_code)
  629. tx_code = IT_RXtoTXCode(rx_code)
  630. if verbose: log_write(" TX code for '" + cmd + "': " + tx_code)
  631. if not tx_code:
  632. if verbose: log_write(" no valid TX code for this device/command")
  633. else:
  634. now = int(round(time.time() * 1000))
  635. # look if this command has been sent in the past, and when
  636. if culSendCmdsKeyName in lastSentTime.keys():
  637. lastTime = lastSentTime[culSendCmdsKeyName]
  638. else:
  639. lastTime = 0
  640. lastTimeAge = now - lastTime
  641. if verbose: log_write(' lastTime: ' + str(lastTimeAge) + 'ms ago')
  642. if lastTimeAge > lastSentMinInterval: # only send if last time + min interval is exceeded
  643. lastSentTime[culSendCmdsKeyName] = now # save what we send, so that we dont repeat our own sent messages if repeating is enabled
  644. TX_interface = TX_interface_prefer
  645. if 'TX_interface' in devdata[device].keys():
  646. if verbose: log_write(" TX_interface: " + devdata[device]['TX_interface'])
  647. if TX_interface == "UART" and not serialCULAvailable:
  648. TX_interface = "MQTT"
  649. global lastSentCmd, lastSentCmdTime, lastSentDev, lastSentDevCmd
  650. lastSentCmd = tx_code
  651. lastSentCmdTime = now
  652. lastSentDev = device
  653. lastSentDevCmd = cmd
  654. if send_on_mqtt_cul and (TX_interface == "MQTT" or TX_interface == "both"):
  655. log_write(" TX via MQTT: " + tx_code)
  656. mqttc.publish(mqtt_cul_topic_send, tx_code, qos=0, retain=False)
  657. if serialCULAvailable and send_on_serial_cul and (TX_interface == "UART" or TX_interface == "both"):
  658. log_write(" TX via UART: " + tx_code)
  659. culCmd = tx_code + '\r\n'
  660. ser.write(culCmd.encode('ascii'))
  661. else:
  662. log_write("WARNING: CUL send command repeated too quickly.")
  663. publish_device_statusupdate(device, cmd, None)
  664. # main
  665. if receive_from_serial_cul or send_on_serial_cul:
  666. if not os.path.exists(serialPort):
  667. log_write("ERROR opening connection to serial CUL... device '" + serialPort + "' does not exist.")
  668. if log_enable: log_write("CUL2MQTT v"+str(version)+" starting")
  669. if receive_from_mqtt_cul:
  670. if forceSerialCULConnected:
  671. exit(2)
  672. else:
  673. log_write("resuming in MQTT-CUL only mode...")
  674. TX_interface_prefer = "MQTT"
  675. receive_from_serial_cul = False
  676. send_on_serial_cul = False
  677. serialCULAvailable = False
  678. log_write("")
  679. log_write("")
  680. else:
  681. log_write("opening connection to UART-CUL...")
  682. serLine = ""
  683. ser = serial.Serial(port=serialPort,baudrate=serialBaudrate,parity=serial.PARITY_NONE,stopbits=serial.STOPBITS_ONE,bytesize=serial.EIGHTBITS,timeout=serialTimeout)
  684. sleep(serialCulInitTimeout)
  685. ser.write('V\r\n'.encode('ascii')) # get CUL version info
  686. serLine = ser.readline()
  687. serLine = serLine.decode('ascii').rstrip('\r\n')
  688. if serLine.startswith("V ") and serLine.find("culfw") != -1:
  689. log_write("connected. CUL version: " + serLine)
  690. serialCULAvailable = True
  691. sleep(0.1)
  692. log_write('Initializing CUL with command: ' + culInitCmd.rstrip('\r\n'))
  693. ser.write(culInitCmd.encode('ascii')) # initialize CUL in normal receive mode
  694. sleep(0.5)
  695. else:
  696. log_write("WARNING: could not connect UART-CUL")
  697. receive_from_serial_cul = False
  698. send_on_serial_cul = False
  699. serialCULAvailable = False
  700. TX_interface_prefer = "MQTT"
  701. if forceSerialCULConnected:
  702. exit(2)
  703. mqttc = mqtt.Client()
  704. mqttc.on_connect = on_connect
  705. mqttc.on_disconnect = on_disconnect
  706. mqttc.on_message = on_message
  707. if mqtt_user is not None and mqtt_password is not None:
  708. if len(mqtt_user) > 0 and len(mqtt_password) > 0:
  709. mqttc.username_pw_set(mqtt_user, mqtt_password)
  710. mqttc.connect(mqtt_server, mqtt_port, 60)
  711. mqttc.loop_start()
  712. while True:
  713. if receive_from_serial_cul:
  714. serLine = ser.readline()
  715. if len(serLine) > 0:
  716. now = int(round(time.time() * 1000))
  717. recvCmd = serLine.decode('ascii')
  718. recvCmd = recvCmd.rstrip('\r\n')
  719. #if debug:
  720. # print("lastSentCmd: " + lastSentCmd + ", lastSentCmdTime=" + str(lastSentCmdTime))
  721. if recvCmd == lastSentCmd and (now - lastSentCmdTime) < filterSelfSentIncomingTimeout:
  722. pass
  723. else:
  724. if verbose:
  725. log_write("")
  726. log_write("UART-CUL RX: '" + recvCmd + "'")
  727. cul_received(recvCmd, "UART")
  728. sleep(0.05)
  729. ## #print "test"
  730. ## #touch("/tmp/culagent_running")
  731. #except KeyboardInterrupt:
  732. # print("\n")