cul2mqtt.py 34 KB

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