| 1 | 1 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,349 @@ |
| 1 |
+# This file is part of Jeedom. |
|
| 2 |
+# |
|
| 3 |
+# Jeedom is free software: you can redistribute it and/or modify |
|
| 4 |
+# it under the terms of the GNU General Public License as published by |
|
| 5 |
+# the Free Software Foundation, either version 3 of the License, or |
|
| 6 |
+# (at your option) any later version. |
|
| 7 |
+# |
|
| 8 |
+# Jeedom is distributed in the hope that it will be useful, |
|
| 9 |
+# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 10 |
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 11 |
+# GNU General Public License for more details. |
|
| 12 |
+# |
|
| 13 |
+# You should have received a copy of the GNU General Public License |
|
| 14 |
+# along with Jeedom. If not, see <http://www.gnu.org/licenses/>. |
|
| 15 |
+# |
|
| 16 |
+ |
|
| 17 |
+import time |
|
| 18 |
+import logging |
|
| 19 |
+import threading |
|
| 20 |
+import requests |
|
| 21 |
+import datetime |
|
| 22 |
+try: |
|
| 23 |
+ from collections.abc import Mapping |
|
| 24 |
+except ImportError: |
|
| 25 |
+ from collections import Mapping |
|
| 26 |
+import serial |
|
| 27 |
+import os |
|
| 28 |
+from os.path import join |
|
| 29 |
+import socket |
|
| 30 |
+from queue import Queue |
|
| 31 |
+import socketserver |
|
| 32 |
+from socketserver import (TCPServer, StreamRequestHandler) |
|
| 33 |
+import signal |
|
| 34 |
+import unicodedata |
|
| 35 |
+import pyudev |
|
| 36 |
+ |
|
| 37 |
+# ------------------------------------------------------------------------------ |
|
| 38 |
+ |
|
| 39 |
+class jeedom_com(): |
|
| 40 |
+ def __init__(self,apikey = '',url = '',cycle = 0.5,retry = 3): |
|
| 41 |
+ self.apikey = apikey |
|
| 42 |
+ self.url = url |
|
| 43 |
+ self.cycle = cycle |
|
| 44 |
+ self.retry = retry |
|
| 45 |
+ self.changes = {}
|
|
| 46 |
+ if cycle > 0 : |
|
| 47 |
+ self.send_changes_async() |
|
| 48 |
+ logging.info('Init request module v%s' % (str(requests.__version__),))
|
|
| 49 |
+ |
|
| 50 |
+ def send_changes_async(self): |
|
| 51 |
+ try: |
|
| 52 |
+ if len(self.changes) == 0: |
|
| 53 |
+ resend_changes = threading.Timer(self.cycle, self.send_changes_async) |
|
| 54 |
+ resend_changes.start() |
|
| 55 |
+ return |
|
| 56 |
+ start_time = datetime.datetime.now() |
|
| 57 |
+ changes = self.changes |
|
| 58 |
+ self.changes = {}
|
|
| 59 |
+ logging.info('Send to jeedom : '+str(changes))
|
|
| 60 |
+ i=0 |
|
| 61 |
+ while i < self.retry: |
|
| 62 |
+ try: |
|
| 63 |
+ r = requests.post(self.url + '?apikey=' + self.apikey, json=changes, timeout=(0.5, 120), verify=False) |
|
| 64 |
+ if r.status_code == requests.codes.ok: |
|
| 65 |
+ break |
|
| 66 |
+ except Exception as error: |
|
| 67 |
+ logging.error('Error on send request to jeedom ' + str(error)+' retry : '+str(i)+'/'+str(self.retry))
|
|
| 68 |
+ i = i + 1 |
|
| 69 |
+ if r.status_code != requests.codes.ok: |
|
| 70 |
+ logging.error('Error on send request to jeedom, return code %s' % (str(r.status_code),))
|
|
| 71 |
+ dt = datetime.datetime.now() - start_time |
|
| 72 |
+ ms = (dt.days * 24 * 60 * 60 + dt.seconds) * 1000 + dt.microseconds / 1000.0 |
|
| 73 |
+ timer_duration = self.cycle - ms |
|
| 74 |
+ if timer_duration < 0.1 : |
|
| 75 |
+ timer_duration = 0.1 |
|
| 76 |
+ if timer_duration > self.cycle: |
|
| 77 |
+ timer_duration = self.cycle |
|
| 78 |
+ resend_changes = threading.Timer(timer_duration, self.send_changes_async) |
|
| 79 |
+ resend_changes.start() |
|
| 80 |
+ except Exception as error: |
|
| 81 |
+ logging.error('Critical error on send_changes_async %s' % (str(error),))
|
|
| 82 |
+ resend_changes = threading.Timer(self.cycle, self.send_changes_async) |
|
| 83 |
+ resend_changes.start() |
|
| 84 |
+ |
|
| 85 |
+ def add_changes(self,key,value): |
|
| 86 |
+ if key.find('::') != -1:
|
|
| 87 |
+ tmp_changes = {}
|
|
| 88 |
+ changes = value |
|
| 89 |
+ for k in reversed(key.split('::')):
|
|
| 90 |
+ if k not in tmp_changes: |
|
| 91 |
+ tmp_changes[k] = {}
|
|
| 92 |
+ tmp_changes[k] = changes |
|
| 93 |
+ changes = tmp_changes |
|
| 94 |
+ tmp_changes = {}
|
|
| 95 |
+ if self.cycle <= 0: |
|
| 96 |
+ self.send_change_immediate(changes) |
|
| 97 |
+ else: |
|
| 98 |
+ self.merge_dict(self.changes,changes) |
|
| 99 |
+ else: |
|
| 100 |
+ if self.cycle <= 0: |
|
| 101 |
+ self.send_change_immediate({key:value})
|
|
| 102 |
+ else: |
|
| 103 |
+ self.changes[key] = value |
|
| 104 |
+ |
|
| 105 |
+ def send_change_immediate(self,change): |
|
| 106 |
+ threading.Thread( target=self.thread_change,args=(change,)).start() |
|
| 107 |
+ |
|
| 108 |
+ def thread_change(self,change): |
|
| 109 |
+ logging.info('Send to jeedom : %s' % (str(change),))
|
|
| 110 |
+ i=0 |
|
| 111 |
+ while i < self.retry: |
|
| 112 |
+ try: |
|
| 113 |
+ r = requests.post(self.url + '?apikey=' + self.apikey, json=change, timeout=(0.5, 120), verify=False) |
|
| 114 |
+ if r.status_code == requests.codes.ok: |
|
| 115 |
+ break |
|
| 116 |
+ except Exception as error: |
|
| 117 |
+ logging.error('Error on send request to jeedom ' + str(error)+' retry : '+str(i)+'/'+str(self.retry))
|
|
| 118 |
+ i = i + 1 |
|
| 119 |
+ |
|
| 120 |
+ def set_change(self,changes): |
|
| 121 |
+ self.changes = changes |
|
| 122 |
+ |
|
| 123 |
+ def get_change(self): |
|
| 124 |
+ return self.changes |
|
| 125 |
+ |
|
| 126 |
+ def merge_dict(self,d1, d2): |
|
| 127 |
+ for k,v2 in d2.items(): |
|
| 128 |
+ v1 = d1.get(k) # returns None if v1 has no value for this key |
|
| 129 |
+ if isinstance(v1, Mapping) and isinstance(v2, Mapping): |
|
| 130 |
+ self.merge_dict(v1, v2) |
|
| 131 |
+ else: |
|
| 132 |
+ d1[k] = v2 |
|
| 133 |
+ |
|
| 134 |
+ def test(self): |
|
| 135 |
+ try: |
|
| 136 |
+ response = requests.get(self.url + '?apikey=' + self.apikey, verify=False) |
|
| 137 |
+ if response.status_code != requests.codes.ok: |
|
| 138 |
+ logging.error('Callback error: %s %s. Please check your network configuration page'% (response.status.code, response.status.message,))
|
|
| 139 |
+ return False |
|
| 140 |
+ except Exception as e: |
|
| 141 |
+ logging.error('Callback result as a unknown error: %s. Please check your network configuration page'% (e.message,))
|
|
| 142 |
+ return False |
|
| 143 |
+ return True |
|
| 144 |
+ |
|
| 145 |
+# ------------------------------------------------------------------------------ |
|
| 146 |
+ |
|
| 147 |
+class jeedom_utils(): |
|
| 148 |
+ |
|
| 149 |
+ @staticmethod |
|
| 150 |
+ def convert_log_level(level = 'error'): |
|
| 151 |
+ LEVELS = {'debug': logging.DEBUG,
|
|
| 152 |
+ 'info': logging.INFO, |
|
| 153 |
+ 'notice': logging.WARNING, |
|
| 154 |
+ 'warning': logging.WARNING, |
|
| 155 |
+ 'error': logging.ERROR, |
|
| 156 |
+ 'critical': logging.CRITICAL, |
|
| 157 |
+ 'none': logging.CRITICAL} |
|
| 158 |
+ return LEVELS.get(level, logging.CRITICAL) |
|
| 159 |
+ |
|
| 160 |
+ @staticmethod |
|
| 161 |
+ def set_log_level(level = 'error'): |
|
| 162 |
+ FORMAT = '[%(asctime)-15s][%(levelname)s] : %(message)s' |
|
| 163 |
+ logging.basicConfig(level=jeedom_utils.convert_log_level(level),format=FORMAT, datefmt="%Y-%m-%d %H:%M:%S") |
|
| 164 |
+ |
|
| 165 |
+ @staticmethod |
|
| 166 |
+ def find_tty_usb(idVendor, idProduct, product = None): |
|
| 167 |
+ context = pyudev.Context() |
|
| 168 |
+ for device in context.list_devices(subsystem='tty'): |
|
| 169 |
+ if 'ID_VENDOR' not in device: |
|
| 170 |
+ continue |
|
| 171 |
+ if device['ID_VENDOR_ID'] != idVendor: |
|
| 172 |
+ continue |
|
| 173 |
+ if device['ID_MODEL_ID'] != idProduct: |
|
| 174 |
+ continue |
|
| 175 |
+ if product is not None: |
|
| 176 |
+ if 'ID_VENDOR' not in device or device['ID_VENDOR'].lower().find(product.lower()) == -1 : |
|
| 177 |
+ continue |
|
| 178 |
+ return str(device.device_node) |
|
| 179 |
+ return None |
|
| 180 |
+ |
|
| 181 |
+ @staticmethod |
|
| 182 |
+ def stripped(str): |
|
| 183 |
+ return "".join([i for i in str if i in range(32, 127)]) |
|
| 184 |
+ |
|
| 185 |
+ @staticmethod |
|
| 186 |
+ def ByteToHex( byteStr ): |
|
| 187 |
+ return byteStr.hex() |
|
| 188 |
+ |
|
| 189 |
+ @staticmethod |
|
| 190 |
+ def dec2bin(x, width=8): |
|
| 191 |
+ return ''.join(str((x>>i)&1) for i in xrange(width-1,-1,-1)) |
|
| 192 |
+ |
|
| 193 |
+ @staticmethod |
|
| 194 |
+ def dec2hex(dec): |
|
| 195 |
+ if dec is None: |
|
| 196 |
+ return '0x00' |
|
| 197 |
+ return "0x{:02X}".format(dec)
|
|
| 198 |
+ |
|
| 199 |
+ @staticmethod |
|
| 200 |
+ def testBit(int_type, offset): |
|
| 201 |
+ mask = 1 << offset |
|
| 202 |
+ return(int_type & mask) |
|
| 203 |
+ |
|
| 204 |
+ @staticmethod |
|
| 205 |
+ def clearBit(int_type, offset): |
|
| 206 |
+ mask = ~(1 << offset) |
|
| 207 |
+ return(int_type & mask) |
|
| 208 |
+ |
|
| 209 |
+ @staticmethod |
|
| 210 |
+ def split_len(seq, length): |
|
| 211 |
+ return [seq[i:i+length] for i in range(0, len(seq), length)] |
|
| 212 |
+ |
|
| 213 |
+ @staticmethod |
|
| 214 |
+ def write_pid(path): |
|
| 215 |
+ pid = str(os.getpid()) |
|
| 216 |
+ logging.info("Writing PID " + pid + " to " + str(path))
|
|
| 217 |
+ open(path, 'w').write("%s\n" % pid)
|
|
| 218 |
+ |
|
| 219 |
+ @staticmethod |
|
| 220 |
+ def remove_accents(input_str): |
|
| 221 |
+ nkfd_form = unicodedata.normalize('NFKD', unicode(input_str))
|
|
| 222 |
+ return u"".join([c for c in nkfd_form if not unicodedata.combining(c)]) |
|
| 223 |
+ |
|
| 224 |
+ @staticmethod |
|
| 225 |
+ def printHex(hex): |
|
| 226 |
+ return ' '.join([hex[i:i + 2] for i in range(0, len(hex), 2)]) |
|
| 227 |
+ |
|
| 228 |
+# ------------------------------------------------------------------------------ |
|
| 229 |
+ |
|
| 230 |
+class jeedom_serial(): |
|
| 231 |
+ |
|
| 232 |
+ def __init__(self,device = '',rate = '',timeout = 9,rtscts = True,xonxoff=False): |
|
| 233 |
+ self.device = device |
|
| 234 |
+ self.rate = rate |
|
| 235 |
+ self.timeout = timeout |
|
| 236 |
+ self.port = None |
|
| 237 |
+ self.rtscts = rtscts |
|
| 238 |
+ self.xonxoff = xonxoff |
|
| 239 |
+ logging.info('Init serial module v%s' % (str(serial.VERSION),))
|
|
| 240 |
+ |
|
| 241 |
+ def open(self): |
|
| 242 |
+ if self.device: |
|
| 243 |
+ logging.info("Open serial port on device: " + str(self.device)+', rate '+str(self.rate)+', timeout : '+str(self.timeout))
|
|
| 244 |
+ else: |
|
| 245 |
+ logging.error("Device name missing.")
|
|
| 246 |
+ return False |
|
| 247 |
+ logging.info("Open Serialport")
|
|
| 248 |
+ try: |
|
| 249 |
+ self.port = serial.Serial( |
|
| 250 |
+ self.device, |
|
| 251 |
+ self.rate, |
|
| 252 |
+ timeout=self.timeout, |
|
| 253 |
+ rtscts=self.rtscts, |
|
| 254 |
+ xonxoff=self.xonxoff, |
|
| 255 |
+ parity=serial.PARITY_NONE, |
|
| 256 |
+ stopbits=serial.STOPBITS_ONE |
|
| 257 |
+ ) |
|
| 258 |
+ except serial.SerialException as e: |
|
| 259 |
+ logging.error("Error: Failed to connect on device " + self.device + " Details : " + str(e))
|
|
| 260 |
+ return False |
|
| 261 |
+ if not self.port.isOpen(): |
|
| 262 |
+ self.port.open() |
|
| 263 |
+ self.flushOutput() |
|
| 264 |
+ self.flushInput() |
|
| 265 |
+ return True |
|
| 266 |
+ |
|
| 267 |
+ def close(self): |
|
| 268 |
+ logging.info("Close serial port")
|
|
| 269 |
+ try: |
|
| 270 |
+ self.port.close() |
|
| 271 |
+ logging.info("Serial port closed")
|
|
| 272 |
+ return True |
|
| 273 |
+ except: |
|
| 274 |
+ logging.error("Failed to close the serial port (" + self.device + ")")
|
|
| 275 |
+ return False |
|
| 276 |
+ |
|
| 277 |
+ def write(self,data): |
|
| 278 |
+ logging.info("Write data to serial port : "+str(jeedom_utils.ByteToHex(data)))
|
|
| 279 |
+ self.port.write(data) |
|
| 280 |
+ |
|
| 281 |
+ def flushOutput(self,): |
|
| 282 |
+ logging.info("flushOutput serial port ")
|
|
| 283 |
+ self.port.flushOutput() |
|
| 284 |
+ |
|
| 285 |
+ def flushInput(self): |
|
| 286 |
+ logging.info("flushInput serial port ")
|
|
| 287 |
+ self.port.flushInput() |
|
| 288 |
+ |
|
| 289 |
+ def read(self): |
|
| 290 |
+ if self.port.inWaiting() != 0: |
|
| 291 |
+ return self.port.read() |
|
| 292 |
+ return None |
|
| 293 |
+ |
|
| 294 |
+ def readbytes(self,number): |
|
| 295 |
+ buf = b'' |
|
| 296 |
+ for i in range(number): |
|
| 297 |
+ try: |
|
| 298 |
+ byte = self.port.read() |
|
| 299 |
+ except IOError as e: |
|
| 300 |
+ logging.error("Error: " + str(e))
|
|
| 301 |
+ except OSError as e: |
|
| 302 |
+ logging.error("Error: " + str(e))
|
|
| 303 |
+ buf += byte |
|
| 304 |
+ return buf |
|
| 305 |
+ |
|
| 306 |
+# ------------------------------------------------------------------------------ |
|
| 307 |
+ |
|
| 308 |
+JEEDOM_SOCKET_MESSAGE = Queue() |
|
| 309 |
+ |
|
| 310 |
+class jeedom_socket_handler(StreamRequestHandler): |
|
| 311 |
+ def handle(self): |
|
| 312 |
+ global JEEDOM_SOCKET_MESSAGE |
|
| 313 |
+ logging.info("Client connected to [%s:%d]" % self.client_address)
|
|
| 314 |
+ lg = self.rfile.readline() |
|
| 315 |
+ JEEDOM_SOCKET_MESSAGE.put(lg) |
|
| 316 |
+ logging.info("Message read from socket: " + str(lg.strip()))
|
|
| 317 |
+ self.netAdapterClientConnected = False |
|
| 318 |
+ logging.info("Client disconnected from [%s:%d]" % self.client_address)
|
|
| 319 |
+ |
|
| 320 |
+class jeedom_socket(): |
|
| 321 |
+ |
|
| 322 |
+ def __init__(self,address='localhost', port=55000): |
|
| 323 |
+ self.address = address |
|
| 324 |
+ self.port = port |
|
| 325 |
+ socketserver.TCPServer.allow_reuse_address = True |
|
| 326 |
+ |
|
| 327 |
+ def open(self): |
|
| 328 |
+ self.netAdapter = TCPServer((self.address, self.port), jeedom_socket_handler) |
|
| 329 |
+ if self.netAdapter: |
|
| 330 |
+ logging.info("Socket interface started")
|
|
| 331 |
+ threading.Thread(target=self.loopNetServer, args=()).start() |
|
| 332 |
+ else: |
|
| 333 |
+ logging.info("Cannot start socket interface")
|
|
| 334 |
+ |
|
| 335 |
+ def loopNetServer(self): |
|
| 336 |
+ logging.info("LoopNetServer Thread started")
|
|
| 337 |
+ logging.info("Listening on: [%s:%d]" % (self.address, self.port))
|
|
| 338 |
+ self.netAdapter.serve_forever() |
|
| 339 |
+ logging.info("LoopNetServer Thread stopped")
|
|
| 340 |
+ |
|
| 341 |
+ def close(self): |
|
| 342 |
+ self.netAdapter.shutdown() |
|
| 343 |
+ |
|
| 344 |
+ def getMessage(self): |
|
| 345 |
+ return self.message |
|
| 346 |
+ |
|
| 347 |
+# ------------------------------------------------------------------------------ |
|
| 348 |
+# END |
|
| 349 |
+# ------------------------------------------------------------------------------ |