decode-config.py 136 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. VER = '2.1.0015'
  4. """
  5. decode-config.py - Backup/Restore Sonoff-Tasmota configuration data
  6. Copyright (C) 2018 Norbert Richter <nr@prsolution.eu>
  7. This program is free software: you can redistribute it and/or modify
  8. it under the terms of the GNU General Public License as published by
  9. the Free Software Foundation, either version 3 of the License, or
  10. (at your option) any later version.
  11. This program is distributed in the hope that it will be useful,
  12. but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. GNU General Public License for more details.
  15. You should have received a copy of the GNU General Public License
  16. along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. Requirements:
  18. - Python
  19. - pip install json pycurl urllib2 configargparse
  20. Instructions:
  21. Execute command with option -d to retrieve config data from a host
  22. or use -f to read a configuration file saved using Tasmota Web-UI
  23. For further information read 'decode-config.md'
  24. For help execute command with argument -h (or -H for advanced help)
  25. Usage: decode-config.py [-f <filename>] [-d <host>] [-P <port>]
  26. [-u <username>] [-p <password>] [-i <filename>]
  27. [-o <filename>] [-t json|bin|dmp] [-E] [-e] [-F]
  28. [--json-indent <indent>] [--json-compact]
  29. [--json-hide-pw] [--json-show-pw]
  30. [--cmnd-indent <indent>] [--cmnd-groups]
  31. [--cmnd-nogroups] [--cmnd-sort] [--cmnd-unsort]
  32. [-c <filename>] [-S] [-T json|cmnd|command]
  33. [-g {Display,Domoticz,Internal,KNX,Led,Logging,MCP230xx,MQTT,Main,Management,Pow,Sensor,Serial,SetOption,SonoffRF,System,Timers,Wifi} [{Display,Domoticz,Internal,KNX,Led,Logging,MCP230xx,MQTT,Main,Management,Pow,Sensor,Serial,SetOption,SonoffRF,System,Timers,Wifi} ...]]
  34. [--ignore-warnings] [-h] [-H] [-v] [-V]
  35. Backup/Restore Sonoff-Tasmota configuration data. Args that start with '--'
  36. (eg. -f) can also be set in a config file (specified via -c). Config file
  37. syntax allows: key=value, flag=true, stuff=[a,b,c] (for details, see syntax at
  38. https://goo.gl/R74nmi). If an arg is specified in more than one place, then
  39. commandline values override config file values which override defaults.
  40. Source:
  41. Read/Write Tasmota configuration from/to
  42. -f, --file, --tasmota-file <filename>
  43. file to retrieve/write Tasmota configuration from/to
  44. (default: None)'
  45. -d, --device, --host <host>
  46. hostname or IP address to retrieve/send Tasmota
  47. configuration from/to (default: None)
  48. -P, --port <port> TCP/IP port number to use for the host connection
  49. (default: 80)
  50. -u, --username <username>
  51. host HTTP access username (default: admin)
  52. -p, --password <password>
  53. host HTTP access password (default: None)
  54. Backup/Restore:
  55. Backup & restore specification
  56. -i, --restore-file <filename>
  57. file to restore configuration from (default: None).
  58. Replacements: @v=firmware version from config,
  59. @f=device friendly name from config, @h=device
  60. hostname from config, @H=device hostname from device
  61. (-d arg only)
  62. -o, --backup-file <filename>
  63. file to backup configuration to (default: None).
  64. Replacements: @v=firmware version from config,
  65. @f=device friendly name from config, @h=device
  66. hostname from config, @H=device hostname from device
  67. (-d arg only)
  68. -t, --backup-type json|bin|dmp
  69. backup filetype (default: 'json')
  70. -E, --extension append filetype extension for -i and -o filename
  71. (default)
  72. -e, --no-extension do not append filetype extension, use -i and -o
  73. filename as passed
  74. -F, --force-restore force restore even configuration is identical
  75. JSON output:
  76. JSON format specification
  77. --json-indent <indent>
  78. pretty-printed JSON output using indent level
  79. (default: 'None'). -1 disables indent.
  80. --json-compact compact JSON output by eliminate whitespace
  81. --json-hide-pw hide passwords
  82. --json-show-pw, --json-unhide-pw
  83. unhide passwords (default)
  84. Tasmota command output:
  85. Tasmota command output format specification
  86. --cmnd-indent <indent>
  87. Tasmota command grouping indent level (default: '2').
  88. 0 disables indent
  89. --cmnd-groups group Tasmota commands (default)
  90. --cmnd-nogroups leave Tasmota commands ungrouped
  91. --cmnd-sort sort Tasmota commands (default)
  92. --cmnd-unsort leave Tasmota commands unsorted
  93. Common:
  94. Optional arguments
  95. -c, --config <filename>
  96. program config file - can be used to set default
  97. command args (default: None)
  98. -S, --output display output regardsless of backup/restore usage
  99. (default do not output on backup or restore usage)
  100. -T, --output-format json|cmnd|command
  101. display output format (default: 'json')
  102. -g, --group {Display,Domoticz,Internal,KNX,Led,Logging,MCP230xx,MQTT,Main,Management,Pow,Sensor,Serial,SetOption,SonoffRF,System,Timers,Wifi}
  103. limit data processing to command groups (default no
  104. filter)
  105. --ignore-warnings do not exit on warnings. Not recommended, used by your
  106. own responsibility!
  107. Info:
  108. Extra information
  109. -h, --help show usage help message and exit
  110. -H, --full-help show full help message and exit
  111. -v, --verbose produce more output about what the program does
  112. -V, --version show program's version number and exit
  113. Either argument -d <host> or -f <filename> must be given.
  114. Returns:
  115. 0: successful
  116. 1: restore skipped
  117. 2: program argument error
  118. 3: file not found
  119. 4: data size mismatch
  120. 5: data CRC error
  121. 6: unsupported configuration version
  122. 7: configuration file read error
  123. 8: JSON file decoding error
  124. 9: Restore file data error
  125. 10: Device data download error
  126. 11: Device data upload error
  127. 20: python module missing
  128. 21: Internal error
  129. >21: python library exit code
  130. 4xx, 5xx: HTTP errors
  131. """
  132. class ExitCode:
  133. OK = 0
  134. RESTORE_SKIPPED = 1
  135. ARGUMENT_ERROR = 2
  136. FILE_NOT_FOUND = 3
  137. DATA_SIZE_MISMATCH = 4
  138. DATA_CRC_ERROR = 5
  139. UNSUPPORTED_VERSION = 6
  140. FILE_READ_ERROR = 7
  141. JSON_READ_ERROR = 8
  142. RESTORE_DATA_ERROR = 9
  143. DOWNLOAD_CONFIG_ERROR = 10
  144. UPLOAD_CONFIG_ERROR = 11
  145. MODULE_NOT_FOUND = 20
  146. INTERNAL_ERROR = 21
  147. # ======================================================================
  148. # imports
  149. # ======================================================================
  150. import os.path
  151. import io
  152. import sys, platform
  153. def ModuleImportError(module):
  154. er = str(module)
  155. print >> sys.stderr, "{}. Try 'pip install {}' to install it".format(er,er.split(' ')[len(er.split(' '))-1])
  156. sys.exit(ExitCode.MODULE_NOT_FOUND)
  157. try:
  158. from datetime import datetime
  159. import time
  160. import copy
  161. import struct
  162. import socket
  163. import re
  164. import math
  165. import inspect
  166. import json
  167. import configargparse
  168. import pycurl
  169. import urllib2
  170. except ImportError, e:
  171. ModuleImportError(e)
  172. # ======================================================================
  173. # globals
  174. # ======================================================================
  175. PROG='{} v{} by Norbert Richter <nr@prsolution.eu>'.format(os.path.basename(sys.argv[0]),VER)
  176. CONFIG_FILE_XOR = 0x5A
  177. BINARYFILE_MAGIC = 0x63576223
  178. STR_ENCODING = 'utf8'
  179. HIDDEN_PASSWORD = '********'
  180. INTERNAL = 'Internal'
  181. DEFAULTS = {
  182. 'source':
  183. {
  184. 'device': None,
  185. 'port': 80,
  186. 'username': 'admin',
  187. 'password': None,
  188. 'tasmotafile': None,
  189. },
  190. 'backup':
  191. {
  192. 'restorefile': None,
  193. 'backupfile': None,
  194. 'backupfileformat': 'json',
  195. 'extension': True,
  196. 'forcerestore': False,
  197. },
  198. 'jsonformat':
  199. {
  200. 'jsonindent': None,
  201. 'jsoncompact': False,
  202. 'jsonsort': True,
  203. 'jsonhidepw': False,
  204. },
  205. 'cmndformat':
  206. {
  207. 'cmndindent': 2,
  208. 'cmndgroup': True,
  209. 'cmndsort': True,
  210. },
  211. 'common':
  212. {
  213. 'output': False,
  214. 'outputformat': 'json',
  215. 'configfile': None,
  216. 'ignorewarning':False,
  217. 'filter': None,
  218. },
  219. }
  220. args = {}
  221. exitcode = 0
  222. # ======================================================================
  223. # Settings mapping
  224. # ======================================================================
  225. """
  226. Settings dictionary describes the config file fields definition:
  227. <setting> = { <name> : <def> }
  228. <name>: "string"
  229. a python valid dictionary key (string)
  230. <def>: ( <format>, <addrdef>, <datadef> [,<converter>] )
  231. a tuple containing the following items:
  232. <format>: <formatstring> | <setting>
  233. data type & format definition
  234. <formatstring>: <string>
  235. defines the use of data at <addrdef>
  236. format is defined in 'struct module format string'
  237. see
  238. https://docs.python.org/2.7/library/struct.html#format-strings
  239. <setting>: <setting>
  240. A dictionary describes a (sub)setting dictonary
  241. and can recursively define another <setting>
  242. <addrdef>: <baseaddr> | (<baseaddr>, <bits>, <bitshift>)
  243. address definition
  244. <baseaddr>: <uint>
  245. The address (starting from 0) within binary config data.
  246. <bits>: <uint>
  247. number of bits used (positive integer)
  248. <bitshift>: <int>
  249. bit shift <bitshift>:
  250. <bitshift> >= 0: shift the result right
  251. <bitshift> < 0: shift the result left
  252. <datadef>: <arraydef> | (<arraydef>, <validate> [,cmd])
  253. data definition
  254. <arraydef>: None | <dim> | [<dim>] | [<dim> ,<dim>...]
  255. None:
  256. Single value, not an array
  257. <dim>: <int>
  258. [<dim>]
  259. Defines a one-dimensional array of size <n>
  260. [<dim> ,<dim>...]
  261. Defines a one- or multi-dimensional array
  262. <validate>: <function>
  263. value validation function
  264. <cmd>: (<group>, <tasmotacmnd>)
  265. Tasmota command definition
  266. <group>: <string>
  267. command group string
  268. <tasmotacmnd>: <function>
  269. convert data into Tasmota command function
  270. <converter>: <readconverter> | (<readconverter>, <writeconverter>)
  271. read/write converter
  272. <readconverter>: None | <function>
  273. Will be used in Bin2Mapping to convert values read
  274. from the binary data object into mapping dictionary
  275. None
  276. None indicates not read conversion
  277. <function>
  278. to convert value from binary object to JSON.
  279. <writeconverter>: None | False | <function>
  280. Will be used in Mapping2Bin to convert values read
  281. from mapping dictionary before write to binary
  282. data object
  283. None
  284. None indicates not write conversion
  285. False
  286. False indicates the value is readonly and will
  287. not be written into the binary object.
  288. <function>
  289. to convert value from JSON back to binary object
  290. Common definitions
  291. <function>: <functionname> | <string> | None
  292. function to be called or string to evaluate:
  293. <functionname>:
  294. A function name will be called with one or two parameter:
  295. The value to be processed
  296. (optional) the current array index (1,n)
  297. <string>
  298. A string will be evaluate as is. The following
  299. placeholder can be used to replace it by runtime values:
  300. '$':
  301. will be replaced by the mapping name value
  302. '#':
  303. will be replace by array index (if any)
  304. '@':
  305. can be used as reference to other mapping values
  306. see definition below for examples
  307. <string>: 'string' | "string"
  308. characters enclosed in ' or "
  309. <int>: integer
  310. numbers in the range -2147483648 through 2147483647
  311. <uint>: unsigned integer
  312. numbers in the range 0 through 4294967295
  313. """
  314. # ----------------------------------------------------------------------
  315. # Settings helper
  316. # ----------------------------------------------------------------------
  317. def passwordread(value):
  318. return HIDDEN_PASSWORD if args.jsonhidepw else value
  319. def passwordwrite(value):
  320. return None if value == HIDDEN_PASSWORD else value
  321. def bitsRead(x, n=0, c=1):
  322. """
  323. Reads bit(s) of a number
  324. @param x:
  325. the number from which to read
  326. @param n:
  327. which bit position to read
  328. @param c:
  329. how many bits to read (1 if omitted)
  330. @return:
  331. the bit value(s)
  332. """
  333. if isinstance(x,str):
  334. x = int(x, 0)
  335. if isinstance(x,str):
  336. n = int(n, 0)
  337. if n >= 0:
  338. x >>= n
  339. else:
  340. x <<= abs(n)
  341. if c>0:
  342. x &= (1<<c)-1
  343. return x
  344. def MqttFingerprint(value, idx=None):
  345. fingerprint = ""
  346. for i in value:
  347. fingerprint += "{:02x} ".format(ord(i))
  348. return "MqttFingerprint{} {}".format('' if idx is None else idx, fingerprint.strip())
  349. # ----------------------------------------------------------------------
  350. # Tasmota configuration data definition
  351. # ----------------------------------------------------------------------
  352. Groups = ('Main','Sensor','Timers','Management','Wifi','MQTT','Serial','SetOption','Logging','Pow','Led','KNX','Domoticz','Display','MCP230xx')
  353. Setting_5_10_0 = {
  354. # <format>, <addrdef>, <datadef> [,<converter>]
  355. 'cfg_holder': ('<L', 0x000, (None, None, (INTERNAL, None)), '"0x{:08x}".format($)' ),
  356. 'save_flag': ('<L', 0x004, (None, None, ('System', None)), (None, False) ),
  357. 'version': ('<L', 0x008, (None, None, (INTERNAL, None)), ('hex($)', False) ),
  358. 'bootcount': ('<L', 0x00C, (None, None, ('System', None)), (None, False) ),
  359. 'flag': ({
  360. 'save_state': ('<L', (0x010,1, 0), (None, None, ('Management', '"SetOption0 {}".format($)')) ),
  361. 'button_restrict': ('<L', (0x010,1, 1), (None, None, ('Management', '"SetOption1 {}".format($)')) ),
  362. 'value_units': ('<L', (0x010,1, 2), (None, None, ('MQTT', '"SetOption2 {}".format($)')) ),
  363. 'mqtt_enabled': ('<L', (0x010,1, 3), (None, None, ('MQTT', '"SetOption3 {}".format($)')) ),
  364. 'mqtt_response': ('<L', (0x010,1, 4), (None, None, ('MQTT', '"SetOption4 {}".format($)')) ),
  365. 'mqtt_power_retain': ('<L', (0x010,1, 5), (None, None, ('Main', '"PowerRetain {}".format($)')) ),
  366. 'mqtt_button_retain': ('<L', (0x010,1, 6), (None, None, ('MQTT', '"ButtonRetain {}".format($)')) ),
  367. 'mqtt_switch_retain': ('<L', (0x010,1, 7), (None, None, ('MQTT', '"SwitchRetain {}".format($)')) ),
  368. 'temperature_conversion': ('<L', (0x010,1, 8), (None, None, ('Sensor', '"SetOption8 {}".format($)')) ),
  369. 'mqtt_sensor_retain': ('<L', (0x010,1, 9), (None, None, ('MQTT', '"SensorRetain {}".format($)')) ),
  370. 'mqtt_offline': ('<L', (0x010,1,10), (None, None, ('MQTT', '"SetOption10 {}".format($)')) ),
  371. 'button_swap': ('<L', (0x010,1,11), (None, None, ('Main', '"SetOption11 {}".format($)')) ),
  372. 'stop_flash_rotate': ('<L', (0x010,1,12), (None, None, ('Management', '"SetOption12 {}".format($)')) ),
  373. 'button_single': ('<L', (0x010,1,13), (None, None, ('Main', '"SetOption13 {}".format($)')) ),
  374. 'interlock': ('<L', (0x010,1,14), (None, None, ('Main', '"SetOption14 {}".format($)')) ),
  375. 'pwm_control': ('<L', (0x010,1,15), (None, None, ('Main', '"SetOption15 {}".format($)')) ),
  376. 'ws_clock_reverse': ('<L', (0x010,1,16), (None, None, ('SetOption', '"SetOption16 {}".format($)')) ),
  377. 'decimal_text': ('<L', (0x010,1,17), (None, None, ('SetOption', '"SetOption17 {}".format($)')) ),
  378. }, 0x010, (None, None, ('*', None)), (None, False) ),
  379. 'save_data': ('<h', 0x014, (None, '0 <= $ <= 3600', ('Management', '"SaveData {}".format($)')) ),
  380. 'timezone': ('b', 0x016, (None, '-13 <= $ <= 13 or $==99', ('Management', '"Timezone {}".format($)')) ),
  381. 'ota_url': ('101s',0x017, (None, None, ('Main', '"OtaUrl {}".format($)')) ),
  382. 'mqtt_prefix': ('11s', 0x07C, ([3], None, ('MQTT', '"Prefix{} {}".format(#,$)')) ),
  383. 'seriallog_level': ('B', 0x09E, (None, '0 <= $ <= 5', ('Logging', '"SerialLog {}".format($)')) ),
  384. 'sta_config': ('B', 0x09F, (None, '0 <= $ <= 5', ('Wifi', '"WifiConfig {}".format($)')) ),
  385. 'sta_active': ('B', 0x0A0, (None, '0 <= $ <= 1', ('Wifi', '"AP {}".format($)')) ),
  386. 'sta_ssid': ('33s', 0x0A1, ([2], None, ('Wifi', '"SSId{} {}".format(#,$)')) ),
  387. 'sta_pwd': ('65s', 0x0E3, ([2], None, ('Wifi', '"Password{} {}".format(#,$)')), (passwordread,passwordwrite) ),
  388. 'hostname': ('33s', 0x165, (None, None, ('Wifi', '"Hostname {}".format($)')) ),
  389. 'syslog_host': ('33s', 0x186, (None, None, ('Logging', '"LogHost {}".format($)')) ),
  390. 'syslog_port': ('<H', 0x1A8, (None, '1 <= $ <= 32766', ('Logging', '"LogPort {}".format($)')) ),
  391. 'syslog_level': ('B', 0x1AA, (None, '0 <= $ <= 4', ('Logging', '"SysLog {}".format($)')) ),
  392. 'webserver': ('B', 0x1AB, (None, '0 <= $ <= 2', ('Wifi', '"WebServer {}".format($)')) ),
  393. 'weblog_level': ('B', 0x1AC, (None, '0 <= $ <= 4', ('Logging', '"WebLog {}".format($)')) ),
  394. 'mqtt_fingerprint': ('60s', 0x1AD, (None, None, ('MQTT', None)) ),
  395. 'mqtt_host': ('33s', 0x1E9, (None, None, ('MQTT', '"MqttHost {}".format($)')) ),
  396. 'mqtt_port': ('<H', 0x20A, (None, None, ('MQTT', '"MqttPort {}".format($)')) ),
  397. 'mqtt_client': ('33s', 0x20C, (None, None, ('MQTT', '"MqttClient {}".format($)')) ),
  398. 'mqtt_user': ('33s', 0x22D, (None, None, ('MQTT', '"MqttUser {}".format($)')) ),
  399. 'mqtt_pwd': ('33s', 0x24E, (None, None, ('MQTT', '"MqttPassword {}".format($)')), (passwordread,passwordwrite) ),
  400. 'mqtt_topic': ('33s', 0x26F, (None, None, ('MQTT', '"FullTopic {}".format($)')) ),
  401. 'button_topic': ('33s', 0x290, (None, None, ('MQTT', '"ButtonTopic {}".format($)')) ),
  402. 'mqtt_grptopic': ('33s', 0x2B1, (None, None, ('MQTT', '"GroupTopic {}".format($)')) ),
  403. 'mqtt_fingerprinth': ('B', 0x2D2, ([20], None, ('MQTT', None)) ),
  404. 'pwm_frequency': ('<H', 0x2E6, (None, '$==1 or 100 <= $ <= 4000', ('Management', '"PwmFrequency {}".format($)')) ),
  405. 'power': ({
  406. 'power1': ('<L', (0x2E8,1,0), (None, None, ('Main', '"Power1 {}".format($)')) ),
  407. 'power2': ('<L', (0x2E8,1,1), (None, None, ('Main', '"Power2 {}".format($)')) ),
  408. 'power3': ('<L', (0x2E8,1,2), (None, None, ('Main', '"Power3 {}".format($)')) ),
  409. 'power4': ('<L', (0x2E8,1,3), (None, None, ('Main', '"Power4 {}".format($)')) ),
  410. 'power5': ('<L', (0x2E8,1,4), (None, None, ('Main', '"Power5 {}".format($)')) ),
  411. 'power6': ('<L', (0x2E8,1,5), (None, None, ('Main', '"Power6 {}".format($)')) ),
  412. 'power7': ('<L', (0x2E8,1,6), (None, None, ('Main', '"Power7 {}".format($)')) ),
  413. 'power8': ('<L', (0x2E8,1,7), (None, None, ('Main', '"Power8 {}".format($)')) ),
  414. }, 0x2E8, (None, None, ('Main', None)), (None, False) ),
  415. 'pwm_value': ('<H', 0x2EC, ([5], '0 <= $ <= 1023', ('Management', '"Pwm{} {}".format(#,$)')) ),
  416. 'altitude': ('<h', 0x2F6, (None, '-30000 <= $ <= 30000', ('Sensor', '"Altitude {}".format($)')) ),
  417. 'tele_period': ('<H', 0x2F8, (None, '0 <= $ <= 1 or 10 <= $ <= 3600',('MQTT', '"TelePeriod {}".format($)')) ),
  418. 'ledstate': ('B', 0x2FB, (None, '0 <= ($ & 0x7) <= 7', ('Main', '"LedState {}".format($)')) ),
  419. 'param': ('B', 0x2FC, ([23], None, ('SetOption', '"SetOption{} {}".format(#+31,$)')) ),
  420. 'state_text': ('11s', 0x313, ([4], None, ('MQTT', '"StateText{} {}".format(#,$)')) ),
  421. 'domoticz_update_timer': ('<H', 0x340, (None, '0 <= $ <= 3600', ('Domoticz', '"DomoticzUpdateTimer {}".format($)')) ),
  422. 'pwm_range': ('<H', 0x342, (None, '$==1 or 255 <= $ <= 1023', ('Management', '"PwmRange {}".format($)')) ),
  423. 'domoticz_relay_idx': ('<L', 0x344, ([4], None, ('Domoticz', '"DomoticzIdx{} {}".format(#,$)')) ),
  424. 'domoticz_key_idx': ('<L', 0x354, ([4], None, ('Domoticz', '"DomoticzKeyIdx{} {}".format(#,$)')) ),
  425. 'energy_power_calibration': ('<L', 0x364, (None, None, ('Pow', '"PowerSet {}".format($)')) ),
  426. 'energy_voltage_calibration': ('<L', 0x368, (None, None, ('Pow', '"VoltageSet {}".format($)')) ),
  427. 'energy_current_calibration': ('<L', 0x36C, (None, None, ('Pow', '"CurrentSet {}".format($)')) ),
  428. 'energy_kWhtoday': ('<L', 0x370, (None, '0 <= $ <= 42500', ('Pow', '"EnergyReset1 {}".format($)')) ),
  429. 'energy_kWhyesterday': ('<L', 0x374, (None, '0 <= $ <= 42500', ('Pow', '"EnergyReset2 {}".format($)')) ),
  430. 'energy_kWhdoy': ('<H', 0x378, (None, None, ('Pow', None)) ),
  431. 'energy_min_power': ('<H', 0x37A, (None, None, ('Pow', '"PowerLow {}".format($)')) ),
  432. 'energy_max_power': ('<H', 0x37C, (None, None, ('Pow', '"PowerHigh {}".format($)')) ),
  433. 'energy_min_voltage': ('<H', 0x37E, (None, None, ('Pow', '"VoltageLow {}".format($)')) ),
  434. 'energy_max_voltage': ('<H', 0x380, (None, None, ('Pow', '"VoltageHigh {}".format($)')) ),
  435. 'energy_min_current': ('<H', 0x382, (None, None, ('Pow', '"CurrentLow {}".format($)')) ),
  436. 'energy_max_current': ('<H', 0x384, (None, None, ('Pow', '"CurrentHigh {}".format($)')) ),
  437. 'energy_max_power_limit': ('<H', 0x386, (None, None, ('Pow', '"MaxPower {}".format($)')) ),
  438. 'energy_max_power_limit_hold': ('<H', 0x388, (None, None, ('Pow', '"MaxPowerHold {}".format($)')) ),
  439. 'energy_max_power_limit_window':('<H', 0x38A, (None, None, ('Pow', '"MaxPowerWindow {}".format($)')) ),
  440. 'energy_max_power_safe_limit': ('<H', 0x38C, (None, None, ('Pow', '"SavePower {}".format($)')) ),
  441. 'energy_max_power_safe_limit_hold':
  442. ('<H', 0x38E, (None, None, ('Pow', '"SavePowerHold {}".format($)')) ),
  443. 'energy_max_power_safe_limit_window':
  444. ('<H', 0x390, (None, None, ('Pow', '"SavePowerWindow {}".format($)')) ),
  445. 'energy_max_energy': ('<H', 0x392, (None, None, ('Pow', '"MaxEnergy {}".format($)')) ),
  446. 'energy_max_energy_start': ('<H', 0x394, (None, None, ('Pow', '"MaxEnergyStart {}".format($)')) ),
  447. 'mqtt_retry': ('<H', 0x396, (None, '10 <= $ <= 32000', ('MQTT', '"MqttRetry {}".format($)')) ),
  448. 'poweronstate': ('B', 0x398, (None, '0 <= $ <= 5', ('Main', '"PowerOnState {}".format($)')) ),
  449. 'last_module': ('B', 0x399, (None, None, ('System', None)) ),
  450. 'blinktime': ('<H', 0x39A, (None, '2 <= $ <= 3600', ('Main', '"BlinkTime {}".format($)')) ),
  451. 'blinkcount': ('<H', 0x39C, (None, '0 <= $ <= 32000', ('Main', '"BlinkCount {}".format($)')) ),
  452. 'friendlyname': ('33s', 0x3AC, ([4], None, ('Management', '"FriendlyName{} {}".format(#,$)')) ),
  453. 'switch_topic': ('33s', 0x430, (None, None, ('MQTT', '"SwitchTopic {}".format($)')) ),
  454. 'sleep': ('B', 0x453, (None, '0 <= $ <= 250', ('Management', '"Sleep {}".format($)')) ),
  455. 'domoticz_switch_idx': ('<H', 0x454, ([4], None, ('Domoticz', '"DomoticzSwitchIdx{} {}".format(#,$)')) ),
  456. 'domoticz_sensor_idx': ('<H', 0x45C, ([12], None, ('Domoticz', '"DomoticzSensorIdx{} {}".format(#,$)')) ),
  457. 'module': ('B', 0x474, (None, None, ('Management', '"Module {}".format($)')) ),
  458. 'ws_color': ('B', 0x475, ([4,3],None, ('Led', None)) ),
  459. 'ws_width': ('B', 0x481, ([3], None, ('Led', None)) ),
  460. 'my_gp': ('B', 0x484, ([18], None, ('Management', '"Gpio{} {}".format(#,$)')) ),
  461. 'light_pixels': ('<H', 0x496, (None, '1 <= $ <= 512', ('Led', '"Pxels {}".format($)')) ),
  462. 'light_color': ('B', 0x498, ([5], None, ('Led', None)) ),
  463. 'light_correction': ('B', 0x49D , (None, '0 <= $ <= 1', ('Led', '"LedTable {}".format($)')) ),
  464. 'light_dimmer': ('B', 0x49E, (None, '0 <= $ <= 100', ('Led', '"Wakeup {}".format($)')) ),
  465. 'light_fade': ('B', 0x4A1, (None, '0 <= $ <= 1', ('Led', '"Fade {}".format($)')) ),
  466. 'light_speed': ('B', 0x4A2, (None, '1 <= $ <= 20', ('Led', '"Speed {}".format($)')) ),
  467. 'light_scheme': ('B', 0x4A3, (None, None, ('Led', '"Scheme {}".format($)')) ),
  468. 'light_width': ('B', 0x4A4, (None, '0 <= $ <= 4', ('Led', '"Width {}".format($)')) ),
  469. 'light_wakeup': ('<H', 0x4A6, (None, '0 <= $ <= 3100', ('Led', '"WakeUpDuration {}".format($)')) ),
  470. 'web_password': ('33s', 0x4A9, (None, None, ('Wifi', '"WebPassword {}".format($)')), (passwordread,passwordwrite) ),
  471. 'switchmode': ('B', 0x4CA, ([4], '0 <= $ <= 7', ('Main', '"SwitchMode{} {}".format(#,$)')) ),
  472. 'ntp_server': ('33s', 0x4CE, ([3], None, ('Wifi', '"NtpServer{} {}".format(#,$)')) ),
  473. 'ina219_mode': ('B', 0x531, (None, '0 <= $ <= 7', ('Sensor', '"Sensor13 {}".format($)')) ),
  474. 'pulse_timer': ('<H', 0x532, ([8], '0 <= $ <= 64900', ('Main', '"PulseTime{} {}".format(#,$)')), ("float($)/10 if 1 <= $ <= 111 else $-100 if $ != 0 else 0", "int($*10) if 0.1 <= $ < 12 else $+100 if $ != 0 else 0") ),
  475. 'ip_address': ('<L', 0x544, ([4], None, ('Wifi', '"IPAddress{} {}".format(#,$)')), ("socket.inet_ntoa(struct.pack('<L', $))", "struct.unpack('<L', socket.inet_aton($))[0]")),
  476. 'energy_kWhtotal': ('<L', 0x554, (None, None, ('Pow', None)) ),
  477. 'mqtt_fulltopic': ('100s',0x558, (None, None, ('MQTT', '"FullTopic {}".format($)')) ),
  478. 'flag2': ({
  479. 'current_resolution': ('<L', (0x5BC,2,15), (None, '0 <= $ <= 3', ('Pow', '"AmpRes {}".format($)')) ),
  480. 'voltage_resolution': ('<L', (0x5BC,2,17), (None, '0 <= $ <= 3', ('Pow', '"VoltRes {}".format($)')) ),
  481. 'wattage_resolution': ('<L', (0x5BC,2,19), (None, '0 <= $ <= 3', ('Pow', '"WattRes {}".format($)')) ),
  482. 'emulation': ('<L', (0x5BC,2,21), (None, '0 <= $ <= 2', ('Management', '"Emulation {}".format($)')) ),
  483. 'energy_resolution': ('<L', (0x5BC,3,23), (None, '0 <= $ <= 5', ('Pow', '"EnergyRes {}".format($)')) ),
  484. 'pressure_resolution': ('<L', (0x5BC,2,26), (None, '0 <= $ <= 3', ('Sensor', '"PressRes {}".format($)')) ),
  485. 'humidity_resolution': ('<L', (0x5BC,2,28), (None, '0 <= $ <= 3', ('Sensor', '"HumRes {}".format($)')) ),
  486. 'temperature_resolution': ('<L', (0x5BC,2,30), (None, '0 <= $ <= 3', ('Sensor', '"TempRes {}".format($)')) ),
  487. }, 0x5BC, (None, None, ('*', None)), (None, False) ),
  488. 'pulse_counter': ('<L', 0x5C0, ([4], None, ('Sensor', '"Counter{} {}".format(#,$)')) ),
  489. 'pulse_counter_type': ('<H', 0x5D0, (None, None, ('Sensor', '"CounterType {}".format($)')) ),
  490. 'pulse_counter_type': ({
  491. 'pulse_counter_type1': ('<H', (0x5D0,1,0), (None, None, ('Sensor', '"CounterType1 {}".format($)')) ),
  492. 'pulse_counter_type2': ('<H', (0x5D0,1,1), (None, None, ('Sensor', '"CounterType2 {}".format($)')) ),
  493. 'pulse_counter_type3': ('<H', (0x5D0,1,2), (None, None, ('Sensor', '"CounterType3 {}".format($)')) ),
  494. 'pulse_counter_type4': ('<H', (0x5D0,1,3), (None, None, ('Sensor', '"CounterType4 {}".format($)')) ),
  495. }, 0x5D0, (None, None, ('Sensor', None)), (None, False) ),
  496. 'pulse_counter_debounce': ('<H', 0x5D2, (None, '0 <= $ <= 3200', ('Sensor', '"CounterDebounce {}".format($)')) ),
  497. 'rf_code': ('B', 0x5D4, ([17,9],None, ('SonoffRF', None)), '"0x{:02x}".format($)'),
  498. }
  499. # ======================================================================
  500. Setting_5_11_0 = copy.deepcopy(Setting_5_10_0)
  501. Setting_5_11_0.update ({
  502. 'display_model': ('B', 0x2D2, (None, '0 <= $ <= 16', ('Display', '"Model {}".format($)')) ),
  503. 'display_mode': ('B', 0x2D3, (None, '0 <= $ <= 5', ('Display', '"Mode {}".format($)')) ),
  504. 'display_refresh': ('B', 0x2D4, (None, '1 <= $ <= 7', ('Display', '"Refresh {}".format($)')) ),
  505. 'display_rows': ('B', 0x2D5, (None, '1 <= $ <= 32', ('Display', '"Rows {}".format($)')) ),
  506. 'display_cols': ('B', 0x2D6, ([2], '1 <= $ <= 40', ('Display', '"Cols{} {}".format(#,$)')) ),
  507. 'display_address': ('B', 0x2D8, ([8], None, ('Display', '"Address{} {}".format(#,$)')) ),
  508. 'display_dimmer': ('B', 0x2E0, (None, '0 <= $ <= 100', ('Display', '"Dimmer {}".format($)')) ),
  509. 'display_size': ('B', 0x2E1, (None, '1 <= $ <= 4', ('Display', '"Size {}".format($)')) ),
  510. })
  511. Setting_5_11_0['flag'][0].update ({
  512. 'light_signal': ('<L', (0x010,1,18), (None, None, ('Sensor', '"SetOption18 {}".format($)')) ),
  513. })
  514. Setting_5_11_0.pop('mqtt_fingerprinth',None)
  515. # ======================================================================
  516. Setting_5_12_0 = copy.deepcopy(Setting_5_11_0)
  517. Setting_5_12_0['flag'][0].update ({
  518. 'hass_discovery': ('<L', (0x010,1,19), (None, None, ('SetOption', '"SetOption19 {}".format($)')) ),
  519. 'not_power_linked': ('<L', (0x010,1,20), (None, None, ('Led', '"SetOption20 {}".format($)')) ),
  520. 'no_power_on_check': ('<L', (0x010,1,21), (None, None, ('Pow', '"SetOption21 {}".format($)')) ),
  521. })
  522. # ======================================================================
  523. Setting_5_13_1 = copy.deepcopy(Setting_5_12_0)
  524. Setting_5_13_1['flag'][0].update ({
  525. 'mqtt_serial': ('<L', (0x010,1,22), (None, None, ('SetOption', '"SetOption22 {}".format($)')) ),
  526. 'rules_enabled': ('<L', (0x010,1,23), (None, None, ('SetOption', '"SetOption23 {}".format($)')) ),
  527. 'rules_once': ('<L', (0x010,1,24), (None, None, ('SetOption', '"SetOption24 {}".format($)')) ),
  528. 'knx_enabled': ('<L', (0x010,1,25), (None, None, ('KNX', '"KNX_ENABLED {}".format($)')) ),
  529. })
  530. Setting_5_13_1.update ({
  531. 'baudrate': ('B', 0x09D, (None, None, ('Serial', '"Baudrate {}".format($)')), ('$ * 1200','$ / 1200') ),
  532. 'mqtt_fingerprint': ('20s', 0x1AD, ([2], None, ('MQTT', MqttFingerprint)) ),
  533. 'energy_power_delta': ('B', 0x33F, (None, None, ('Pow', '"PowerDelta {}".format($)')) ),
  534. 'light_rotation': ('<H', 0x39E, (None, None, ('Led', '"Rotation {}".format($)')) ),
  535. 'serial_delimiter': ('B', 0x451, (None, None, ('Serial', '"SerialDelimiter {}".format($)')) ),
  536. 'sbaudrate': ('B', 0x452, (None, None, ('Serial', '"SBaudrate {}".format($)')), ('$ * 1200','$ / 1200') ),
  537. 'knx_GA_registered': ('B', 0x4A5, (None, None, ('KNX', None)) ),
  538. 'knx_CB_registered': ('B', 0x4A8, (None, None, ('KNX', None)) ),
  539. 'timer': ({
  540. 'value': ('<L', 0x670, (None, None, ('Timers', '"Timer{} {{\\\"Arm\\\":{arm},\\\"Mode\\\":{mode},\\\"Time\\\":\\\"{tsign}{time}\\\",\\\"Window\\\":{window},\\\"Days\\\":\\\"{days}\\\",\\\"Repeat\\\":{repeat},\\\"Output\\\":{device},\\\"Action\\\":{power}}}".format(#, arm=bitsRead($,31),mode=bitsRead($,29,2),tsign="-" if bitsRead($,29,2)>0 and bitsRead($,0,11)>(12*60) else "",time=time.strftime("%H:%M",time.gmtime((bitsRead($,0,11) if bitsRead($,29,2)==0 else bitsRead($,0,11) if bitsRead($,0,11)<=(12*60) else bitsRead($,0,11)-(12*60))*60)),window=bitsRead($,11,4),repeat=bitsRead($,15),days="{:07b}".format(bitsRead($,16,7))[::-1],device=bitsRead($,23,4)+1,power=bitsRead($,27,2) )')), ('"0x{:08x}".format($)', False) ),
  541. 'time': ('<L', (0x670,11, 0),(None, '0 <= $ < 1440', ('Timers', None)) ),
  542. 'window': ('<L', (0x670, 4,11),(None, None, ('Timers', None)) ),
  543. 'repeat': ('<L', (0x670, 1,15),(None, None, ('Timers', None)) ),
  544. 'days': ('<L', (0x670, 7,16),(None, None, ('Timers', None)), '"0b{:07b}".format($)' ),
  545. 'device': ('<L', (0x670, 4,23),(None, None, ('Timers', None)) ),
  546. 'power': ('<L', (0x670, 2,27),(None, None, ('Timers', None)) ),
  547. 'mode': ('<L', (0x670, 2,29),(None, '0 <= $ <= 3', ('Timers', None)) ),
  548. 'arm': ('<L', (0x670, 1,31),(None, None, ('Timers', None)) ),
  549. }, 0x670, ([16], None, ('Timers', None)) ),
  550. 'latitude': ('i', 0x6B0, (None, None, ('Timers', '"Latitude {}".format($)')), ('float($) / 1000000', 'int($ * 1000000)')),
  551. 'longitude': ('i', 0x6B4, (None, None, ('Timers', '"Longitude {}".format($)')), ('float($) / 1000000', 'int($ * 1000000)')),
  552. 'knx_physsical_addr': ('<H', 0x6B8, (None, None, ('KNX', None)) ),
  553. 'knx_GA_addr': ('<H', 0x6BA, ([10], None, ('KNX', None)) ),
  554. 'knx_CB_addr': ('<H', 0x6CE, ([10], None, ('KNX', None)) ),
  555. 'knx_GA_param': ('B', 0x6E2, ([10], None, ('KNX', None)) ),
  556. 'knx_CB_param': ('B', 0x6EC, ([10], None, ('KNX', None)) ),
  557. 'rules': ('512s',0x800, (None, None, ('Management', '"Rule {}".format("\\"" if len($)==0 else $)')) ),
  558. })
  559. # ======================================================================
  560. Setting_5_14_0 = copy.deepcopy(Setting_5_13_1)
  561. Setting_5_14_0['flag'][0].update ({
  562. 'device_index_enable': ('<L', (0x010,1,26), (None, None, ('Main', '"SetOption26 {}".format($)')) ),
  563. })
  564. Setting_5_14_0['flag'][0].pop('rules_once',None)
  565. Setting_5_14_0.update ({
  566. 'tflag': ({
  567. 'hemis': ('<H', (0x2E2,1, 0), (None, None, ('Management', None)) ),
  568. 'week': ('<H', (0x2E2,3, 1), (None, '0 <= $ <= 4', ('Management', None)) ),
  569. 'month': ('<H', (0x2E2,4, 4), (None, '1 <= $ <= 12', ('Management', None)) ),
  570. 'dow': ('<H', (0x2E2,3, 8), (None, '1 <= $ <= 7', ('Management', None)) ),
  571. 'hour': ('<H', (0x2E2,5,11), (None, '0 <= $ <= 23', ('Management', None)) ),
  572. }, 0x2E2, ([2], None, ('Management', None)), (None, False) ),
  573. 'param': ('B', 0x2FC, ([18], None, ('SetOption', '"SetOption{} {}".format(#+31,$)')) ),
  574. 'toffset': ('<h', 0x30E, ([2], None, ('Management', '"{cmnd} {hemis},{week},{month},{dow},{hour},{toffset}".format(cmnd="TimeSTD" if idx==1 else "TimeDST", hemis=@["tflag"][#-1]["hemis"], week=@["tflag"][#-1]["week"], month=@["tflag"][#-1]["month"], dow=@["tflag"][#-1]["dow"], hour=@["tflag"][#-1]["hour"], toffset=value)')) ),
  575. })
  576. # ======================================================================
  577. Setting_6_0_0 = copy.deepcopy(Setting_5_14_0)
  578. Setting_6_0_0.update({
  579. 'cfg_holder': ('<H', 0x000, (None, None, ('System', None)), ),
  580. 'cfg_size': ('<H', 0x002, (None, None, (INTERNAL, None)), (None, False)),
  581. 'bootcount': ('<H', 0x00C, (None, None, ('System', None)), (None, False)),
  582. 'cfg_crc': ('<H', 0x00E, (None, None, (INTERNAL, None)), '"0x{:04x}".format($)'),
  583. 'rule_enabled': ({
  584. 'rule1': ('B', (0x49F,1,0), (None, None, ('Management', '"Rule1 {}".format($)')) ),
  585. 'rule2': ('B', (0x49F,1,1), (None, None, ('Management', '"Rule2 {}".format($)')) ),
  586. 'rule3': ('B', (0x49F,1,2), (None, None, ('Management', '"Rule3 {}".format($)')) ),
  587. }, 0x49F, (None, None, ('Management', None)), (None, False) ),
  588. 'rule_once': ({
  589. 'rule1': ('B', (0x4A0,1,0), (None, None, ('Management', '"Rule1 {}".format($+4)')) ),
  590. 'rule2': ('B', (0x4A0,1,1), (None, None, ('Management', '"Rule2 {}".format($+4)')) ),
  591. 'rule3': ('B', (0x4A0,1,2), (None, None, ('Management', '"Rule3 {}".format($+4)')) ),
  592. }, 0x4A0, (None, None, ('Management', None)), (None, False) ),
  593. 'mems': ('10s', 0x7CE, ([5], None, ('Management', '"Mem{} {}".format(#,"\\"" if len($)==0 else $)')) ),
  594. 'rules': ('512s',0x800, ([3], None, ('Management', '"Rule{} {}".format(#,"\\"" if len($)==0 else $)')) ),
  595. })
  596. Setting_6_0_0['flag'][0].update ({
  597. 'knx_enable_enhancement': ('<L', (0x010,1,27), (None, None, ('KNX', '"KNX_ENHANCED {}".format($)')) ),
  598. })
  599. # ======================================================================
  600. Setting_6_1_1 = copy.deepcopy(Setting_6_0_0)
  601. Setting_6_1_1.update ({
  602. 'flag3': ('<L', 0x3A0, (None, None, ('System', None)), '"0x{:08x}".format($)' ),
  603. 'switchmode': ('B', 0x3A4, ([8], '0 <= $ <= 7', ('Main', '"SwitchMode{} {}".format(#,$)')) ),
  604. 'mcp230xx_config': ({
  605. 'value': ('<L', 0x6F6, (None, None, ('MCP230xx', '"Sensor29 {pin},{pinmode},{pullup},{intmode}".format(pin=#-1, pinmode=@["mcp230xx_config"][#-1]["pinmode"], pullup=@["mcp230xx_config"][#-1]["pullup"], intmode=@["mcp230xx_config"][#-1]["int_report_mode"])')), ('"0x{:08x}".format($)', False) ),
  606. 'pinmode': ('<L', (0x6F6,3, 0), (None, '0 <= $ <= 5', ('MCP230xx', None)) ),
  607. 'pullup': ('<L', (0x6F6,1, 3), (None, None, ('MCP230xx', None)) ),
  608. 'saved_state': ('<L', (0x6F6,1, 4), (None, None, ('MCP230xx', None)) ),
  609. 'int_report_mode': ('<L', (0x6F6,2, 5), (None, None, ('MCP230xx', None)) ),
  610. 'int_report_defer': ('<L', (0x6F6,4, 7), (None, None, ('MCP230xx', None)) ),
  611. 'int_count_en': ('<L', (0x6F6,1,11), (None, None, ('MCP230xx', None)) ),
  612. }, 0x6F6, ([16], None, ('MCP230xx', None)), (None, False) ),
  613. })
  614. Setting_6_1_1['flag'][0].update ({
  615. 'rf_receive_decimal': ('<L', (0x010,1,28), (None, None, ('SetOption' , '"SetOption28 {}".format($)')) ),
  616. 'ir_receive_decimal': ('<L', (0x010,1,29), (None, None, ('SetOption', '"SetOption29 {}".format($)')) ),
  617. 'hass_light': ('<L', (0x010,1,30), (None, None, ('SetOption', '"SetOption30 {}".format($)')) ),
  618. })
  619. # ======================================================================
  620. Setting_6_2_1 = copy.deepcopy(Setting_6_1_1)
  621. Setting_6_2_1.update ({
  622. 'rule_stop': ({
  623. 'rule1': ('B', (0x1A7,1,0), (None, None, ('Management', '"Rule1 {}".format($+8)')) ),
  624. 'rule2': ('B', (0x1A7,1,1), (None, None, ('Management', '"Rule2 {}".format($+8)')) ),
  625. 'rule3': ('B', (0x1A7,1,2), (None, None, ('Management', '"Rule3 {}".format($+8)')) ),
  626. }, 0x1A7, None),
  627. 'display_rotate': ('B', 0x2FA, (None, '0 <= $ <= 3', ('Display', '"Rotate {}".format($)')) ),
  628. 'display_font': ('B', 0x312, (None, '1 <= $ <= 4', ('Display', '"Font {}".format($)')) ),
  629. 'flag3': ({
  630. 'timers_enable': ('<L', (0x3A0,1, 0), (None, None, ('Timers', '"Timers {}".format($)')) ),
  631. 'user_esp8285_enable': ('<L', (0x3A0,1,31), (None, None, ('System', None)) ),
  632. }, 0x3A0, (None, None, ('*', None)), (None, False) ),
  633. 'button_debounce': ('<H', 0x542, (None, '40 <= $ <= 1000', ('Main', '"ButtonDebounce {}".format($)')) ),
  634. 'switch_debounce': ('<H', 0x66E, (None, '40 <= $ <= 1000', ('Main', '"SwitchDebounce {}".format($)')) ),
  635. 'mcp230xx_int_prio': ('B', 0x716, (None, None, ('MCP230xx', None)) ),
  636. 'mcp230xx_int_timer': ('<H', 0x718, (None, None, ('MCP230xx', None)) ),
  637. })
  638. Setting_6_2_1['flag'][0].pop('rules_enabled',None)
  639. Setting_6_2_1['flag'][0].update ({
  640. 'mqtt_serial_raw': ('<L', (0x010,1,23), (None, None, ('SetOption', '"SetOption23 {}".format($)')) ),
  641. 'global_state': ('<L', (0x010,1,31), (None, None, ('SetOption', '"SetOption31 {}".format($)')) ),
  642. })
  643. Setting_6_2_1['flag2'][0].update ({
  644. # currently unsupported Tasmota command, should be Sensor32, still needs to implement
  645. 'axis_resolution': ('<L', (0x5BC,2,13), (None, None, ('System', None)) ),
  646. })
  647. # ======================================================================
  648. Setting_6_2_1_2 = copy.deepcopy(Setting_6_2_1)
  649. Setting_6_2_1_2['flag3'][0].update ({
  650. 'user_esp8285_enable': ('<L', (0x3A0,1, 1), (None, None, ('SetOption', '"SetOption51 {}".format($)')) ),
  651. })
  652. # ======================================================================
  653. Setting_6_2_1_3 = copy.deepcopy(Setting_6_2_1_2)
  654. Setting_6_2_1_3['flag2'][0].update ({
  655. 'frequency_resolution': ('<L', (0x5BC,2,11), (None, '0 <= $ <= 3', ('Pow', '"FreqRes {}".format($)')) ),
  656. })
  657. Setting_6_2_1_3['flag3'][0].update ({
  658. 'time_append_timezone': ('<L', (0x3A0,1, 2), (None, None, ('SetOption', '"SetOption52 {}".format($)')) ),
  659. })
  660. # ======================================================================
  661. Setting_6_2_1_6 = copy.deepcopy(Setting_6_2_1_3)
  662. Setting_6_2_1_6.update({
  663. 'energy_frequency_calibration': ('<L', 0x7C8, (None, '45000 < $ < 65000', ('Pow', '"FrequencySet {}".format($)')) ),
  664. })
  665. # ======================================================================
  666. Setting_6_2_1_10 = copy.deepcopy(Setting_6_2_1_6)
  667. Setting_6_2_1_10.update({
  668. 'rgbwwTable': ('B', 0x71A, ([5], None, ('System', None)) ), # RGBWWTable 255,135,70,255,255
  669. })
  670. # ======================================================================
  671. Setting_6_2_1_14 = copy.deepcopy(Setting_6_2_1_10)
  672. Setting_6_2_1_14.update({
  673. 'weight_item': ('<H', 0x7BC, (None, None, ('Management', '"Sensor34 6 {}".format($)')), ('int($ * 10)', 'float($) / 10') ), # undocumented
  674. 'weight_max': ('<H', 0x7BE, (None, None, ('Management', '"Sensor34 5 {}".format($)')), ('float($) / 1000', 'int($ * 1000)') ), # undocumented
  675. 'weight_reference': ('<L', 0x7C0, (None, None, ('Management', '"Sensor34 3 {}".format($)')) ), # undocumented
  676. 'weight_calibration': ('<L', 0x7C4, (None, None, ('Management', '"Sensor34 4 {}".format($)')) ), # undocumented
  677. 'web_refresh': ('<H', 0x7CC, (None, '1000 <= $ <= 10000', ('Management', '"WebRefresh {}".format($)')) ), # undocumented
  678. })
  679. Setting_6_2_1_14['flag2'][0].update ({
  680. 'weight_resolution': ('<L', (0x5BC,2, 9), (None, '0 <= $ <= 3', ('Management', '"WeightRes {}".format($)')) ), # undocumented
  681. })
  682. # ======================================================================
  683. Setting_6_2_1_19 = copy.deepcopy(Setting_6_2_1_14)
  684. Setting_6_2_1_19.update({
  685. 'weight_item': ('<L', 0x7B8, (None, None, ('Management', '"Sensor34 6 {}".format($)')), ('int($ * 10)', 'float($) / 10') ), # undocumented
  686. })
  687. Setting_6_2_1_20 = Setting_6_2_1_19
  688. Setting_6_2_1_20['flag3'][0].update ({
  689. 'gui_hostname_ip': ('<L', (0x3A0,1,3), (None, None, ('SetOption', '"SetOption53 {}".format($)')) ),
  690. })
  691. # ======================================================================
  692. Setting_6_3_0 = copy.deepcopy(Setting_6_2_1_20)
  693. Setting_6_3_0.update({
  694. 'energy_kWhtotal_time': ('<L', 0x7B4, (None, None, ('System', None)) ),
  695. })
  696. # ======================================================================
  697. Setting_6_3_0_2 = copy.deepcopy(Setting_6_3_0)
  698. Setting_6_3_0_2.update({
  699. 'timezone_minutes': ('B', 0x66D, (None, None, ('System', None)) ),
  700. })
  701. Setting_6_3_0_2['flag'][0].pop('rules_once',None)
  702. Setting_6_3_0_2['flag'][0].update ({
  703. 'pressure_conversion': ('<L', (0x010,1,24), (None, None, ('SetOption', '"SetOption24 {}".format($)')) ),
  704. })
  705. # ======================================================================
  706. Setting_6_3_0_4 = copy.deepcopy(Setting_6_3_0_2)
  707. Setting_6_3_0_4.update({
  708. 'drivers': ('<L', 0x794, ([3], None, ('System', None)), '"0x{:08x}".format($)' ),
  709. 'monitors': ('<L', 0x7A0, (None, None, ('System', None)), '"0x{:08x}".format($)' ),
  710. 'sensors': ('<L', 0x7A4, ([3], None, ('System', None)), '"0x{:08x}".format($)' ),
  711. 'displays': ('<L', 0x7B0, (None, None, ('System', None)), '"0x{:08x}".format($)' ),
  712. })
  713. Setting_6_3_0_4['flag3'][0].update ({
  714. 'tuya_apply_o20': ('<L', (0x3A0,1, 4), (None, None, ('SetOption', '"SetOption54 {}".format($)')) ),
  715. })
  716. # ======================================================================
  717. Setting_6_3_0_8 = copy.deepcopy(Setting_6_3_0_4)
  718. Setting_6_3_0_8['flag3'][0].update ({
  719. 'hass_short_discovery_msg': ('<L', (0x3A0,1, 5), (None, None, ('SetOption', '"SetOption55 {}".format($)')) ),
  720. })
  721. # ======================================================================
  722. Setting_6_3_0_10 = copy.deepcopy(Setting_6_3_0_8)
  723. Setting_6_3_0_10['flag3'][0].update ({
  724. 'use_wifi_scan': ('<L', (0x3A0,1, 6), (None, None, ('SetOption', '"SetOption56 {}".format($)')) ),
  725. 'use_wifi_rescan': ('<L', (0x3A0,1, 7), (None, None, ('SetOption', '"SetOption57 {}".format($)')) ),
  726. })
  727. # ======================================================================
  728. Setting_6_3_0_11 = copy.deepcopy(Setting_6_3_0_10)
  729. Setting_6_3_0_11['flag3'][0].update ({
  730. 'receive_raw': ('<L', (0x3A0,1, 8), (None, None, ('SetOption', '"SetOption58 {}".format($)')) ),
  731. })
  732. # ======================================================================
  733. Setting_6_3_0_13 = copy.deepcopy(Setting_6_3_0_11)
  734. Setting_6_3_0_13['flag3'][0].update ({
  735. 'hass_tele_on_power': ('<L', (0x3A0,1, 9), (None, None, ('SetOption', '"SetOption59 {}".format($)')) ),
  736. })
  737. # ======================================================================
  738. Setting_6_3_0_14 = copy.deepcopy(Setting_6_3_0_13)
  739. Setting_6_3_0_14['flag2'][0].update ({
  740. 'calc_resolution': ('<L', (0x5BC,3, 6), (None, '0 <= $ <= 7', ('Management', '"CalcRes {}".format($)')) ),
  741. })
  742. # ======================================================================
  743. Setting_6_3_0_15 = copy.deepcopy(Setting_6_3_0_14)
  744. Setting_6_3_0_15['flag3'][0].update ({
  745. 'sleep_normal': ('<L', (0x3A0,1,10), (None, None, ('SetOption', '"SetOption60 {}".format($)')) ),
  746. })
  747. # ======================================================================
  748. Setting_6_3_0_16 = copy.deepcopy(Setting_6_3_0_15)
  749. Setting_6_3_0_16['mcp230xx_config'][0].update ({
  750. 'int_retain_flag': ('<L', (0x6F6,1,12), (None, None, ('MCP230xx', None)) ),
  751. })
  752. Setting_6_3_0_16['flag3'][0].update ({
  753. 'button_switch_force_local':('<L', (0x3A0,1,11), (None, None, ('SetOption', '"SetOption61 {}".format($)')) ),
  754. })
  755. # ======================================================================
  756. Setting_6_4_0_2 = copy.deepcopy(Setting_6_3_0_16)
  757. Setting_6_4_0_2['flag3'][0].pop('hass_short_discovery_msg',None)
  758. # ======================================================================
  759. Settings = [
  760. (0x6040002, 0xe00, Setting_6_4_0_2),
  761. (0x6030010, 0xe00, Setting_6_3_0_16),
  762. (0x603000F, 0xe00, Setting_6_3_0_15),
  763. (0x603000E, 0xe00, Setting_6_3_0_14),
  764. (0x603000D, 0xe00, Setting_6_3_0_13),
  765. (0x603000B, 0xe00, Setting_6_3_0_11),
  766. (0x603000A, 0xe00, Setting_6_3_0_10),
  767. (0x6030008, 0xe00, Setting_6_3_0_8),
  768. (0x6030004, 0xe00, Setting_6_3_0_4),
  769. (0x6030002, 0xe00, Setting_6_3_0_2),
  770. (0x6030000, 0xe00, Setting_6_3_0),
  771. (0x6020114, 0xe00, Setting_6_2_1_20),
  772. (0x6020113, 0xe00, Setting_6_2_1_19),
  773. (0x602010E, 0xe00, Setting_6_2_1_14),
  774. (0x602010A, 0xe00, Setting_6_2_1_10),
  775. (0x6020106, 0xe00, Setting_6_2_1_6),
  776. (0x6020103, 0xe00, Setting_6_2_1_3),
  777. (0x6020102, 0xe00, Setting_6_2_1_2),
  778. (0x6020100, 0xe00, Setting_6_2_1),
  779. (0x6010100, 0xe00, Setting_6_1_1),
  780. (0x6000000, 0xe00, Setting_6_0_0),
  781. (0x50e0000, 0xa00, Setting_5_14_0),
  782. (0x50d0100, 0xa00, Setting_5_13_1),
  783. (0x50c0000, 0x670, Setting_5_12_0),
  784. (0x50b0000, 0x670, Setting_5_11_0),
  785. (0x50a0000, 0x670, Setting_5_10_0),
  786. ]
  787. # ======================================================================
  788. # Common helper
  789. # ======================================================================
  790. class LogType:
  791. INFO = 'INFO'
  792. WARNING = 'WARNING'
  793. ERROR = 'ERROR'
  794. def message(msg, typ=None, status=None, line=None):
  795. """
  796. Writes a message to stdout
  797. @param msg:
  798. message to output
  799. @param typ:
  800. INFO, WARNING or ERROR
  801. @param status:
  802. status number
  803. """
  804. print >> sys.stderr, '{styp}{sdelimiter}{sstatus}{slineno}{scolon}{smgs}'.format(\
  805. styp=typ if typ is not None else '',
  806. sdelimiter=' ' if status is not None and status > 0 and typ is not None else '',
  807. sstatus=status if status is not None and status > 0 else '',
  808. scolon=': ' if typ is not None or line is not None else '',
  809. smgs=msg,
  810. slineno=' (@{:04d})'.format(line) if line is not None else '')
  811. def exit(status=0, msg="end", typ=LogType.ERROR, src=None, doexit=True, line=None):
  812. """
  813. Called when the program should be exit
  814. @param status:
  815. the exit status program returns to callert
  816. @param msg:
  817. the msg logged before exit
  818. @param typ:
  819. msg type: 'INFO', 'WARNING' or 'ERROR'
  820. @param doexit:
  821. True to exit program, otherwise return
  822. """
  823. if src is not None:
  824. msg = '{} ({})'.format(src, msg)
  825. message(msg, typ=typ if status!=ExitCode.OK else LogType.INFO, status=status, line=line)
  826. exitcode = status
  827. if doexit:
  828. sys.exit(exitcode)
  829. def ShortHelp(doexit=True):
  830. """
  831. Show short help (usage) only - ued by own -h handling
  832. @param doexit:
  833. sys.exit with OK if True
  834. """
  835. print parser.description
  836. print
  837. parser.print_usage()
  838. print
  839. print "For advanced help use '{prog} -H' or '{prog} --full-help'".format(prog=os.path.basename(sys.argv[0]))
  840. if doexit:
  841. sys.exit(ExitCode.OK)
  842. class HTTPHeader:
  843. """
  844. pycurl helper class retrieving the request header
  845. """
  846. def __init__(self):
  847. self.contents = ''
  848. def clear(self):
  849. self.contents = ''
  850. def store(self, _buffer):
  851. self.contents = "{}{}".format(self.contents, _buffer)
  852. def response(self):
  853. header = str(self.contents).split('\n')
  854. if len(header) > 0:
  855. return header[0].rstrip()
  856. return ''
  857. def contenttype(self):
  858. for item in str(self.contents).split('\n'):
  859. ditem = item.split(":")
  860. if ditem[0].strip().lower() == 'content-type' and len(ditem) > 1:
  861. return ditem[1].strip()
  862. return ''
  863. def __str__(self):
  864. return self.contents
  865. class CustomHelpFormatter(configargparse.HelpFormatter):
  866. """
  867. Class for customizing the help output
  868. """
  869. def _format_action_invocation(self, action):
  870. """
  871. Reformat multiple metavar output
  872. -d <host>, --device <host>, --host <host>
  873. to single output
  874. -d, --device, --host <host>
  875. """
  876. orgstr = configargparse.HelpFormatter._format_action_invocation(self, action)
  877. if orgstr and orgstr[0] != '-': # only optional arguments
  878. return orgstr
  879. res = getattr(action, '_formatted_action_invocation', None)
  880. if res:
  881. return res
  882. options = orgstr.split(', ')
  883. if len(options) <= 1:
  884. action._formatted_action_invocation = orgstr
  885. return orgstr
  886. return_list = []
  887. for option in options:
  888. meta = ""
  889. arg = option.split(' ')
  890. if len(arg) > 1:
  891. meta = arg[1]
  892. return_list.append(arg[0])
  893. if len(meta) > 0 and len(return_list) > 0:
  894. return_list[len(return_list)-1] += " "+meta
  895. action._formatted_action_invocation = ', '.join(return_list)
  896. return action._formatted_action_invocation
  897. # ======================================================================
  898. # Tasmota config data handling
  899. # ======================================================================
  900. def GetTemplateSizes():
  901. """
  902. Get all possible template sizes as list
  903. @param version:
  904. <int> version number from read binary data to search for
  905. @return:
  906. template sizes as list []
  907. """
  908. sizes = []
  909. for cfg in Settings:
  910. sizes.append(cfg[1])
  911. # return unique sizes only (remove duplicates)
  912. return list(set(sizes))
  913. def GetTemplateSetting(decode_cfg):
  914. """
  915. Search for version, size and settings to be used depending on given binary config data
  916. @param decode_cfg:
  917. binary config data (decrypted)
  918. @return:
  919. version, size, settings to use; None if version is invalid
  920. """
  921. version = 0x0
  922. size = setting = None
  923. version = GetField(decode_cfg, 'version', Setting_6_2_1['version'], raw=True)
  924. # search setting definition top-down
  925. for cfg in sorted(Settings, key=lambda s: s[0], reverse=True):
  926. if version >= cfg[0]:
  927. size = cfg[1]
  928. setting = cfg[2]
  929. break
  930. return version, size, setting
  931. def GetGroupList(setting):
  932. """
  933. Get all avilable group definition from setting
  934. @return:
  935. configargparse.parse_args() result
  936. """
  937. groups = set()
  938. for name in setting:
  939. dev = setting[name]
  940. format, group = GetFieldDef(dev, fields="format, group")
  941. if group is not None and len(group) > 0:
  942. groups.add(group)
  943. if isinstance(format, dict):
  944. subgroups = GetGroupList(format)
  945. if subgroups is not None and len(subgroups) > 0:
  946. for group in subgroups:
  947. groups.add(group)
  948. groups=list(groups)
  949. groups.sort()
  950. return groups
  951. class FileType:
  952. FILE_NOT_FOUND = None
  953. DMP = 'dmp'
  954. JSON = 'json'
  955. BIN = 'bin'
  956. UNKNOWN = 'unknown'
  957. INCOMPLETE_JSON = 'incomplete json'
  958. INVALID_JSON = 'invalid json'
  959. INVALID_BIN = 'invalid bin'
  960. def GetFileType(filename):
  961. """
  962. Get the FileType class member of a given filename
  963. @param filename:
  964. filename of the file to analyse
  965. @return:
  966. FileType class member
  967. """
  968. filetype = FileType.UNKNOWN
  969. # try filename
  970. try:
  971. isfile = os.path.isfile(filename)
  972. try:
  973. f = open(filename, "r")
  974. try:
  975. # try reading as json
  976. inputjson = json.load(f)
  977. if 'header' in inputjson:
  978. filetype = FileType.JSON
  979. else:
  980. filetype = FileType.INCOMPLETE_JSON
  981. except ValueError:
  982. filetype = FileType.INVALID_JSON
  983. # not a valid json, get filesize and compare it with all possible sizes
  984. try:
  985. size = os.path.getsize(filename)
  986. except:
  987. filetype = FileType.UNKNOWN
  988. sizes = GetTemplateSizes()
  989. # size is one of a dmp file size
  990. if size in sizes:
  991. filetype = FileType.DMP
  992. elif (size - ((len(hex(BINARYFILE_MAGIC))-2)/2)) in sizes:
  993. # check if the binary file has the magic header
  994. try:
  995. inputfile = open(filename, "rb")
  996. inputbin = inputfile.read()
  997. inputfile.close()
  998. if struct.unpack_from('<L', inputbin, 0)[0] == BINARYFILE_MAGIC:
  999. filetype = FileType.BIN
  1000. else:
  1001. filetype = FileType.INVALID_BIN
  1002. except:
  1003. pass
  1004. # ~ else:
  1005. # ~ filetype = FileType.UNKNOWN
  1006. finally:
  1007. f.close()
  1008. except:
  1009. filetype = FileType.FILE_NOT_FOUND
  1010. except:
  1011. filetype = FileType.FILE_NOT_FOUND
  1012. return filetype
  1013. def GetVersionStr(version):
  1014. """
  1015. Create human readable version string
  1016. @param version:
  1017. version integer
  1018. @return:
  1019. version string
  1020. """
  1021. if isinstance(version, (unicode,str)):
  1022. version = int(version, 0)
  1023. major = ((version>>24) & 0xff)
  1024. minor = ((version>>16) & 0xff)
  1025. release = ((version>> 8) & 0xff)
  1026. subrelease = (version & 0xff)
  1027. if major >= 6:
  1028. if subrelease > 0:
  1029. subreleasestr = str(subrelease)
  1030. else:
  1031. subreleasestr = ''
  1032. else:
  1033. if subrelease > 0:
  1034. subreleasestr = str(chr(subrelease+ord('a')-1))
  1035. else:
  1036. subreleasestr = ''
  1037. return "{:d}.{:d}.{:d}{}{}".format( major, minor, release, '.' if (major >= 6 and subreleasestr != '') else '', subreleasestr)
  1038. def MakeFilename(filename, filetype, configmapping):
  1039. """
  1040. Replace variables within a filename
  1041. @param filename:
  1042. original filename possible containing replacements:
  1043. @v:
  1044. Tasmota version from config data
  1045. @f:
  1046. friendlyname from config data
  1047. @h:
  1048. hostname from config data
  1049. @H:
  1050. hostname from device (-d arg only)
  1051. @param filetype:
  1052. FileType.x object - creates extension if not None
  1053. @param configmapping:
  1054. binary config data (decrypted)
  1055. @return:
  1056. New filename with replacements
  1057. """
  1058. config_version = config_friendlyname = config_hostname = device_hostname = ''
  1059. if 'version' in configmapping:
  1060. config_version = GetVersionStr( int(str(configmapping['version']), 0) )
  1061. if 'friendlyname' in configmapping:
  1062. config_friendlyname = configmapping['friendlyname'][0]
  1063. if 'hostname' in configmapping:
  1064. if configmapping['hostname'].find('%') < 0:
  1065. config_hostname = configmapping['hostname']
  1066. if filename.find('@H') >= 0 and args.device is not None:
  1067. device_hostname = GetTasmotaHostname(args.device, args.port, username=args.username, password=args.password)
  1068. if device_hostname is None:
  1069. device_hostname = ''
  1070. dirname = basename = ext = ''
  1071. # split file parts
  1072. dirname = os.path.normpath(os.path.dirname(filename))
  1073. basename = os.path.basename(filename)
  1074. name, ext = os.path.splitext(basename)
  1075. # make a valid filename
  1076. try:
  1077. name = name.decode('unicode-escape').translate(dict((ord(char), None) for char in '\/*?:"<>|'))
  1078. except:
  1079. pass
  1080. name = str(name.replace(' ','_'))
  1081. # append extension based on filetype if not given
  1082. if len(ext) and ext[0]=='.':
  1083. ext = ext[1:]
  1084. if filetype is not None and args.extension and (len(ext)<2 or all(c.isdigit() for c in ext)):
  1085. ext = filetype.lower()
  1086. # join filename + extension
  1087. if len(ext):
  1088. name_ext = name+'.'+ext
  1089. else:
  1090. name_ext = name
  1091. # join path and filename
  1092. try:
  1093. filename = os.path.join(dirname, name_ext)
  1094. except:
  1095. pass
  1096. filename = filename.replace('@v', config_version)
  1097. filename = filename.replace('@f', config_friendlyname )
  1098. filename = filename.replace('@h', config_hostname )
  1099. filename = filename.replace('@H', device_hostname )
  1100. return filename
  1101. def MakeUrl(host, port=80, location=''):
  1102. """
  1103. Create a Tasmota host url
  1104. @param host:
  1105. hostname or IP of Tasmota host
  1106. @param port:
  1107. port number to use for http connection
  1108. @param location:
  1109. http url location
  1110. @return:
  1111. Tasmota http url
  1112. """
  1113. return "http://{shost}{sdelimiter}{sport}/{slocation}".format(\
  1114. shost=host,
  1115. sdelimiter=':' if port != 80 else '',
  1116. sport=port if port != 80 else '',
  1117. slocation=location )
  1118. def LoadTasmotaConfig(filename):
  1119. """
  1120. Load config from Tasmota file
  1121. @param filename:
  1122. filename to load
  1123. @return:
  1124. binary config data (encrypted) or None on error
  1125. """
  1126. encode_cfg = None
  1127. # read config from a file
  1128. if not os.path.isfile(filename): # check file exists
  1129. exit(ExitCode.FILE_NOT_FOUND, "File '{}' not found".format(filename),line=inspect.getlineno(inspect.currentframe()))
  1130. try:
  1131. tasmotafile = open(filename, "rb")
  1132. encode_cfg = tasmotafile.read()
  1133. tasmotafile.close()
  1134. except Exception, e:
  1135. exit(e[0], "'{}' {}".format(filename, e[1]),line=inspect.getlineno(inspect.currentframe()))
  1136. return encode_cfg
  1137. def TasmotaGet(cmnd, host, port, username=DEFAULTS['source']['username'], password=None, contenttype = None):
  1138. """
  1139. Tasmota http request
  1140. @param host:
  1141. hostname or IP of Tasmota device
  1142. @param port:
  1143. http port of Tasmota device
  1144. @param username:
  1145. optional username for Tasmota web login
  1146. @param password
  1147. optional password for Tasmota web login
  1148. @return:
  1149. binary config data (encrypted) or None on error
  1150. """
  1151. body = None
  1152. # read config direct from device via http
  1153. c = pycurl.Curl()
  1154. buffer = io.BytesIO()
  1155. c.setopt(c.WRITEDATA, buffer)
  1156. header = HTTPHeader()
  1157. c.setopt(c.HEADERFUNCTION, header.store)
  1158. c.setopt(c.FOLLOWLOCATION, True)
  1159. c.setopt(c.URL, MakeUrl(host, port, cmnd))
  1160. if username is not None and password is not None:
  1161. c.setopt(c.HTTPAUTH, c.HTTPAUTH_BASIC)
  1162. c.setopt(c.USERPWD, username + ':' + password)
  1163. c.setopt(c.HTTPGET, True)
  1164. c.setopt(c.VERBOSE, False)
  1165. responsecode = 200
  1166. try:
  1167. c.perform()
  1168. responsecode = c.getinfo(c.RESPONSE_CODE)
  1169. response = header.response()
  1170. except Exception, e:
  1171. exit(e[0], e[1],line=inspect.getlineno(inspect.currentframe()))
  1172. finally:
  1173. c.close()
  1174. if responsecode >= 400:
  1175. exit(responsecode, 'HTTP result: {}'.format(header.response()),line=inspect.getlineno(inspect.currentframe()))
  1176. elif contenttype is not None and header.contenttype()!=contenttype:
  1177. exit(ExitCode.DOWNLOAD_CONFIG_ERROR, "Device did not response properly, may be Tasmota webserver admin mode is disabled (WebServer 2)",line=inspect.getlineno(inspect.currentframe()))
  1178. try:
  1179. body = buffer.getvalue()
  1180. except:
  1181. pass
  1182. return responsecode, body
  1183. def GetTasmotaHostname(host, port, username=DEFAULTS['source']['username'], password=None):
  1184. """
  1185. Get Tasmota hostname from device
  1186. @param host:
  1187. hostname or IP of Tasmota device
  1188. @param port:
  1189. http port of Tasmota device
  1190. @param username:
  1191. optional username for Tasmota web login
  1192. @param password
  1193. optional password for Tasmota web login
  1194. @return:
  1195. Tasmota real hostname or None on error
  1196. """
  1197. hostname = None
  1198. loginstr = ""
  1199. if password is not None:
  1200. loginstr = "user={}&password={}&".format(urllib2.quote(username), urllib2.quote(password))
  1201. # get hostname
  1202. responsecode, body = TasmotaGet("cm?{}cmnd=status%205".format(loginstr), host, port, username=username, password=password)
  1203. if body is not None:
  1204. jsonbody = json.loads(body)
  1205. if "StatusNET" in jsonbody and "Hostname" in jsonbody["StatusNET"]:
  1206. hostname = jsonbody["StatusNET"]["Hostname"]
  1207. if args.verbose:
  1208. message("Hostname for '{}' retrieved: '{}'".format(host, hostname), typ=LogType.INFO)
  1209. return hostname
  1210. def PullTasmotaConfig(host, port, username=DEFAULTS['source']['username'], password=None):
  1211. """
  1212. Pull config from Tasmota device
  1213. @param host:
  1214. hostname or IP of Tasmota device
  1215. @param port:
  1216. http port of Tasmota device
  1217. @param username:
  1218. optional username for Tasmota web login
  1219. @param password
  1220. optional password for Tasmota web login
  1221. @return:
  1222. binary config data (encrypted) or None on error
  1223. """
  1224. responsecode, body = TasmotaGet('dl', host, port, username, password, contenttype='application/octet-stream')
  1225. return body
  1226. def PushTasmotaConfig(encode_cfg, host, port, username=DEFAULTS['source']['username'], password=None):
  1227. """
  1228. Upload binary data to a Tasmota host using http
  1229. @param encode_cfg:
  1230. encrypted binary data or filename containing Tasmota encrypted binary config
  1231. @param host:
  1232. hostname or IP of Tasmota device
  1233. @param port:
  1234. http port of Tasmota device
  1235. @param username:
  1236. optional username for Tasmota web login
  1237. @param password
  1238. optional password for Tasmota web login
  1239. @return
  1240. errorcode, errorstring
  1241. errorcode=0 if success, otherwise http response or exception code
  1242. """
  1243. if isinstance(encode_cfg, bytearray):
  1244. encode_cfg = str(encode_cfg)
  1245. # get restore config page first to set internal Tasmota vars
  1246. responsecode, body = TasmotaGet('rs?', host, port, username, password, contenttype='text/html')
  1247. if body is None:
  1248. return responsecode, "ERROR"
  1249. # post data
  1250. c = pycurl.Curl()
  1251. header = HTTPHeader()
  1252. c.setopt(c.HEADERFUNCTION, header.store)
  1253. c.setopt(c.WRITEFUNCTION, lambda x: None)
  1254. c.setopt(c.POST, 1)
  1255. c.setopt(c.URL, MakeUrl(host, port, 'u2'))
  1256. if username is not None and password is not None:
  1257. c.setopt(c.HTTPAUTH, c.HTTPAUTH_BASIC)
  1258. c.setopt(c.USERPWD, username + ':' + password)
  1259. try:
  1260. isfile = os.path.isfile(encode_cfg)
  1261. except:
  1262. isfile = False
  1263. if isfile:
  1264. c.setopt(c.HTTPPOST, [("file", (c.FORM_FILE, encode_cfg))])
  1265. else:
  1266. # use as binary data
  1267. c.setopt(c.HTTPPOST, [
  1268. ('fileupload', (
  1269. c.FORM_BUFFER, '{sprog}_v{sver}.dmp'.format(sprog=os.path.basename(sys.argv[0]), sver=VER),
  1270. c.FORM_BUFFERPTR, encode_cfg
  1271. )),
  1272. ])
  1273. responsecode = 200
  1274. try:
  1275. c.perform()
  1276. responsecode = c.getinfo(c.RESPONSE_CODE)
  1277. except Exception, e:
  1278. return e[0], e[1]
  1279. c.close()
  1280. if responsecode >= 400:
  1281. return responsecode, header.response()
  1282. elif header.contenttype() != 'text/html':
  1283. return ExitCode.UPLOAD_CONFIG_ERROR, "Device did not response properly, may be Tasmota webserver admin mode is disabled (WebServer 2)"
  1284. return 0, 'OK'
  1285. def DecryptEncrypt(obj):
  1286. """
  1287. Decrpt/Encrypt binary config data
  1288. @param obj:
  1289. binary config data
  1290. @return:
  1291. decrypted configuration (if obj contains encrypted data)
  1292. """
  1293. if isinstance(obj, bytearray):
  1294. obj = str(obj)
  1295. dobj = obj[0:2]
  1296. for i in range(2, len(obj)):
  1297. dobj += chr( (ord(obj[i]) ^ (CONFIG_FILE_XOR +i)) & 0xff )
  1298. return dobj
  1299. def GetSettingsCrc(dobj):
  1300. """
  1301. Return binary config data calclulated crc
  1302. @param dobj:
  1303. decrypted binary config data
  1304. @return:
  1305. 2 byte unsigned integer crc value
  1306. """
  1307. if isinstance(dobj, bytearray):
  1308. dobj = str(dobj)
  1309. crc = 0
  1310. for i in range(0, len(dobj)):
  1311. if not i in [14,15]: # Skip crc
  1312. byte = ord(dobj[i])
  1313. crc += byte * (i+1)
  1314. return crc & 0xffff
  1315. def GetFieldDef(fielddef, fields="format, addrdef, baseaddr, bits, bitshift, datadef, arraydef, validate, cmd, group, tasmotacmnd, converter, readconverter, writeconverter"):
  1316. """
  1317. Get field definition items
  1318. @param fielddef:
  1319. field format - see "Settings dictionary" above
  1320. @param fields:
  1321. comma separated string list of values to be returned
  1322. possible values see fields default
  1323. @return:
  1324. set of values defined in <fields>
  1325. """
  1326. format = addrdef = baseaddr = datadef = arraydef = validate = cmd = group = tasmotacmnd = converter = readconverter = writeconverter = None
  1327. bits = bitshift = 0
  1328. # calling with nothing is wrong
  1329. if fielddef is None:
  1330. print >> sys.stderr, '<fielddef> is None'
  1331. raise SyntaxError('<fielddef> error')
  1332. # get top level items
  1333. if len(fielddef) == 3:
  1334. # converter not present
  1335. format, addrdef, datadef = fielddef
  1336. elif len(fielddef) == 4:
  1337. # converter present
  1338. format, addrdef, datadef, converter = fielddef
  1339. else:
  1340. print >> sys.stderr, 'wrong <fielddef> {} length ({}) in setting'.format(fielddef, len(fielddef))
  1341. raise SyntaxError('<fielddef> error')
  1342. # ignore calls with 'root' setting
  1343. if isinstance(format, dict) and baseaddr is None and datadef is None:
  1344. return eval(fields)
  1345. if not isinstance(format, (unicode,str,dict)):
  1346. print >> sys.stderr, 'wrong <format> {} type {} in <fielddef> {}'.format(format, type(format), fielddef)
  1347. raise SyntaxError('<fielddef> error')
  1348. # extract addrdef items
  1349. baseaddr = addrdef
  1350. if isinstance(baseaddr, (list,tuple)):
  1351. if len(baseaddr) == 3:
  1352. # baseaddr bit definition
  1353. baseaddr, bits, bitshift = baseaddr
  1354. if not isinstance(bits, int):
  1355. print >> sys.stderr, '<bits> must be a integer in <fielddef> {}'.format(bits, fielddef)
  1356. raise SyntaxError('<fielddef> error')
  1357. if not isinstance(bitshift, int):
  1358. print >> sys.stderr, '<bitshift> must be a integer in <fielddef> {}'.format(bitshift, fielddef)
  1359. raise SyntaxError('<fielddef> error')
  1360. else:
  1361. print >> sys.stderr, 'wrong <addrdef> {} length ({}) in <fielddef> {}'.format(addrdef, len(addrdef), fielddef)
  1362. raise SyntaxError('<fielddef> error')
  1363. if not isinstance(baseaddr, int):
  1364. print >> sys.stderr, '<baseaddr> must be a integer in <fielddef> {}'.format(baseaddr, fielddef)
  1365. raise SyntaxError('<fielddef> error')
  1366. # extract datadef items
  1367. arraydef = datadef
  1368. if isinstance(datadef, (tuple)):
  1369. if len(datadef) == 2:
  1370. # datadef has a validator
  1371. arraydef, validate = datadef
  1372. elif len(datadef) == 3:
  1373. # datadef has a validator and cmd set
  1374. arraydef, validate, cmd = datadef
  1375. # cmd must be a tuple with 2 objects
  1376. if isinstance(cmd, (tuple)) and len(cmd) == 2:
  1377. group, tasmotacmnd = cmd
  1378. if group is not None and not isinstance(group, (str, unicode)):
  1379. print >> sys.stderr, 'wrong <group> {} in <fielddef> {}'.format(group, fielddef)
  1380. raise SyntaxError('<fielddef> error')
  1381. if tasmotacmnd is not None and not callable(tasmotacmnd) and not isinstance(tasmotacmnd, (str, unicode)):
  1382. print >> sys.stderr, 'wrong <tasmotacmnd> {} in <fielddef> {}'.format(tasmotacmnd, fielddef)
  1383. raise SyntaxError('<fielddef> error')
  1384. else:
  1385. print >> sys.stderr, 'wrong <cmd> {} length ({}) in <fielddef> {}'.format(cmd, len(cmd), fielddef)
  1386. raise SyntaxError('<fielddef> error')
  1387. else:
  1388. print >> sys.stderr, 'wrong <datadef> {} length ({}) in <fielddef> {}'.format(datadef, len(datadef), fielddef)
  1389. raise SyntaxError('<fielddef> error')
  1390. if validate is not None and (not isinstance(validate, (unicode,str)) and not callable(validate)):
  1391. print >> sys.stderr, 'wrong <validate> {} type {} in <fielddef> {}'.format(validate, type(validate), fielddef)
  1392. raise SyntaxError('<fielddef> error')
  1393. # convert single int into one-dimensional list
  1394. if isinstance(arraydef, int):
  1395. arraydef = [arraydef]
  1396. if arraydef is not None and not isinstance(arraydef, (list)):
  1397. print >> sys.stderr, 'wrong <arraydef> {} type {} in <fielddef> {}'.format(arraydef, type(arraydef), fielddef)
  1398. raise SyntaxError('<fielddef> error')
  1399. # get read/write converter items
  1400. readconverter = converter
  1401. if isinstance(converter, (tuple)):
  1402. if len(converter) == 2:
  1403. # converter has read/write converter
  1404. readconverter, writeconverter = converter
  1405. if readconverter is not None and not isinstance(readconverter, (str,unicode)) and not callable(readconverter):
  1406. print >> sys.stderr, 'wrong <readconverter> {} type {} in <fielddef> {}'.format(readconverter, type(readconverter), fielddef)
  1407. raise SyntaxError('<fielddef> error')
  1408. if writeconverter is not None and (not isinstance(writeconverter, (bool,str,unicode)) and not callable(writeconverter)):
  1409. print >> sys.stderr, 'wrong <writeconverter> {} type {} in <fielddef> {}'.format(writeconverter, type(writeconverter), fielddef)
  1410. raise SyntaxError('<fielddef> error')
  1411. else:
  1412. print >> sys.stderr, 'wrong <converter> {} length ({}) in <fielddef> {}'.format(converter, len(converter), fielddef)
  1413. raise SyntaxError('<fielddef> error')
  1414. return eval(fields)
  1415. def ReadWriteConverter(value, fielddef, read=True, raw=False):
  1416. """
  1417. Convert field value based on field desc
  1418. @param value:
  1419. original value
  1420. @param fielddef
  1421. field definition - see "Settings dictionary" above
  1422. @param read
  1423. use read conversion if True, otherwise use write conversion
  1424. @param raw
  1425. return raw values (True) or converted values (False)
  1426. @return:
  1427. (un)converted value
  1428. """
  1429. converter, readconverter, writeconverter = GetFieldDef(fielddef, fields='converter, readconverter, writeconverter')
  1430. # call password functions even if raw value should be processed
  1431. if read and callable(readconverter) and readconverter == passwordread:
  1432. raw = False
  1433. if not read and callable(writeconverter) and writeconverter == passwordwrite:
  1434. raw = False
  1435. if not raw and converter is not None:
  1436. conv = readconverter if read else writeconverter
  1437. try:
  1438. if isinstance(conv, str): # evaluate strings
  1439. return eval(conv.replace('$','value'))
  1440. elif callable(conv): # use as format function
  1441. return conv(value)
  1442. except Exception, e:
  1443. exit(e[0], e[1], typ=LogType.WARNING, line=inspect.getlineno(inspect.currentframe()))
  1444. return value
  1445. def CmndConverter(valuemapping, value, idx, fielddef):
  1446. """
  1447. Convert field value into Tasmota command if available
  1448. @param valuemapping:
  1449. data mapping
  1450. @param value:
  1451. original value
  1452. @param fielddef
  1453. field definition - see "Settings dictionary" above
  1454. @return:
  1455. converted value or None if unable to convert
  1456. """
  1457. converter, readconverter, writeconverter, group, tasmotacmnd = GetFieldDef(fielddef, fields='converter, readconverter, writeconverter, group, tasmotacmnd')
  1458. result = None
  1459. if (callable(readconverter) and readconverter == passwordread) or (callable(writeconverter) and writeconverter == passwordwrite):
  1460. if value == HIDDEN_PASSWORD:
  1461. return None
  1462. else:
  1463. result = value
  1464. if tasmotacmnd is not None and (callable(tasmotacmnd) or len(tasmotacmnd) > 0):
  1465. if idx is not None:
  1466. idx += 1
  1467. if isinstance(tasmotacmnd, str): # evaluate strings
  1468. if idx is not None:
  1469. evalstr = tasmotacmnd.replace('$','value').replace('#','idx').replace('@','valuemapping')
  1470. else:
  1471. evalstr = tasmotacmnd.replace('$','value').replace('@','valuemapping')
  1472. # ~ try:
  1473. result = eval(evalstr)
  1474. # ~ except:
  1475. # ~ print evalstr
  1476. # ~ print value
  1477. elif callable(tasmotacmnd): # use as format function
  1478. if idx is not None:
  1479. result = tasmotacmnd(value, idx)
  1480. else:
  1481. result = tasmotacmnd(value)
  1482. return result
  1483. def ValidateValue(value, fielddef):
  1484. """
  1485. Validate a value if validator is defined in fielddef
  1486. @param value:
  1487. original value
  1488. @param fielddef
  1489. field definition - see "Settings dictionary" above
  1490. @return:
  1491. True if value is valid, False if invalid
  1492. """
  1493. validate = GetFieldDef(fielddef, fields='validate')
  1494. if value == 0:
  1495. # can not complete all validate condition
  1496. # some Tasmota values are not allowed to be 0 on input
  1497. # even though these values are set to 0 on Tasmota initial.
  1498. # so we can't validate 0 values
  1499. return True;
  1500. valid = True
  1501. try:
  1502. if isinstance(validate, str): # evaluate strings
  1503. valid = eval(validate.replace('$','value'))
  1504. elif callable(validate): # use as format function
  1505. valid = validate(value)
  1506. except:
  1507. valid = False
  1508. return valid
  1509. def GetFieldMinMax(fielddef):
  1510. """
  1511. Get minimum, maximum of field based on field format definition
  1512. @param fielddef:
  1513. field format - see "Settings dictionary" above
  1514. @return:
  1515. min, max
  1516. """
  1517. minmax = {'c': (0, 1),
  1518. '?': (0, 1),
  1519. 'b': (~0x7f, 0x7f),
  1520. 'B': (0, 0xff),
  1521. 'h': (~0x7fff, 0x7fff),
  1522. 'H': (0, 0xffff),
  1523. 'i': (~0x7fffffff, 0x7fffffff),
  1524. 'I': (0, 0xffffffff),
  1525. 'l': (~0x7fffffff, 0x7fffffff),
  1526. 'L': (0, 0xffffffff),
  1527. 'q': (~0x7fffffffffffffff, 0x7fffffffffffffff),
  1528. 'Q': (0, 0x7fffffffffffffff),
  1529. 'f': (sys.float_info.min, sys.float_info.max),
  1530. 'd': (sys.float_info.min, sys.float_info.max),
  1531. }
  1532. format = GetFieldDef(fielddef, fields='format')
  1533. _min = 0
  1534. _max = 0
  1535. if format[-1:] in minmax:
  1536. _min, _max = minmax[format[-1:]]
  1537. elif format[-1:] in ['s','p']:
  1538. # s and p may have a prefix as length
  1539. match = re.search("\s*(\d+)", format)
  1540. if match:
  1541. _max=int(match.group(0))
  1542. return _min,_max
  1543. def GetFieldLength(fielddef):
  1544. """
  1545. Get length of a field in bytes based on field format definition
  1546. @param fielddef:
  1547. field format - see "Settings dictionary" above
  1548. @return:
  1549. length of field in bytes
  1550. """
  1551. length=0
  1552. format, addrdef, arraydef = GetFieldDef(fielddef, fields='format, addrdef, arraydef')
  1553. # <arraydef> contains a integer list
  1554. if isinstance(arraydef, list) and len(arraydef) > 0:
  1555. # arraydef contains a list
  1556. # calc size recursive by sum of all elements
  1557. for i in range(0, arraydef[0]):
  1558. subfielddef = GetSubfieldDef(fielddef)
  1559. if len(arraydef) > 1:
  1560. length += GetFieldLength( (format, addrdef, subfielddef) )
  1561. # single array
  1562. else:
  1563. length += GetFieldLength( (format, addrdef, None) )
  1564. elif isinstance(format, dict):
  1565. # -> iterate through format
  1566. addr = None
  1567. setting = format
  1568. for name in setting:
  1569. baseaddr, bits, bitshift = GetFieldDef(setting[name], fields='baseaddr, bits, bitshift')
  1570. _len = GetFieldLength(setting[name])
  1571. if addr != baseaddr:
  1572. addr = baseaddr
  1573. length += _len
  1574. # a simple value
  1575. elif isinstance(format, str):
  1576. if format[-1:] in ['b','B','c','?']:
  1577. length=1
  1578. elif format[-1:] in ['h','H']:
  1579. length=2
  1580. elif format[-1:] in ['i','I','l','L','f']:
  1581. length=4
  1582. elif format[-1:] in ['q','Q','d']:
  1583. length=8
  1584. elif format[-1:] in ['s','p']:
  1585. # s and p may have a prefix as length
  1586. match = re.search("\s*(\d+)", format)
  1587. if match:
  1588. length=int(match.group(0))
  1589. return length
  1590. def GetSubfieldDef(fielddef):
  1591. """
  1592. Get subfield definition from a given field definition
  1593. @param fielddef:
  1594. see Settings desc above
  1595. @return:
  1596. subfield definition
  1597. """
  1598. format, addrdef, datadef, arraydef, validate, cmd, converter = GetFieldDef(fielddef, fields='format, addrdef, datadef, arraydef, validate, cmd, converter')
  1599. # create new arraydef
  1600. if len(arraydef) > 1:
  1601. arraydef = arraydef[1:]
  1602. else:
  1603. arraydef = None
  1604. # create new datadef
  1605. if isinstance(datadef, tuple):
  1606. if cmd is not None:
  1607. datadef = (arraydef, validate, cmd)
  1608. else:
  1609. datadef = (arraydef, validate)
  1610. else:
  1611. datadef = arraydef
  1612. # set new field def
  1613. subfielddef = None
  1614. if converter is not None:
  1615. subfielddef = (format, addrdef, datadef, converter)
  1616. else:
  1617. subfielddef = (format, addrdef, datadef)
  1618. return subfielddef
  1619. def IsFilterGroup(group):
  1620. """
  1621. Check if group is valid on filter
  1622. @param grooup:
  1623. group name to check
  1624. @return:
  1625. True if group is in filter, otherwise False
  1626. """
  1627. if args.filter is not None:
  1628. if group is None:
  1629. return False
  1630. if group != INTERNAL and group != '*' and group not in args.filter:
  1631. return False
  1632. return True
  1633. def GetField(dobj, fieldname, fielddef, raw=False, addroffset=0):
  1634. """
  1635. Get field value from definition
  1636. @param dobj:
  1637. decrypted binary config data
  1638. @param fieldname:
  1639. name of the field
  1640. @param fielddef:
  1641. see Settings desc above
  1642. @param raw
  1643. return raw values (True) or converted values (False)
  1644. @param addroffset
  1645. use offset for baseaddr (used for recursive calls)
  1646. @return:
  1647. field mapping
  1648. """
  1649. if isinstance(dobj, bytearray):
  1650. dobj = str(dobj)
  1651. valuemapping = None
  1652. # get field definition
  1653. format, baseaddr, bits, bitshift, arraydef, group, tasmotacmnd = GetFieldDef(fielddef, fields='format, baseaddr, bits, bitshift, arraydef, group, tasmotacmnd')
  1654. # filter groups
  1655. if not IsFilterGroup(group):
  1656. return valuemapping
  1657. # <arraydef> contains a integer list
  1658. if isinstance(arraydef, list) and len(arraydef) > 0:
  1659. valuemapping = []
  1660. offset = 0
  1661. for i in range(0, arraydef[0]):
  1662. subfielddef = GetSubfieldDef(fielddef)
  1663. length = GetFieldLength(subfielddef)
  1664. if length != 0:
  1665. value = GetField(dobj, fieldname, subfielddef, raw=raw, addroffset=addroffset+offset)
  1666. valuemapping.append(value)
  1667. offset += length
  1668. # <format> contains a dict
  1669. elif isinstance(format, dict):
  1670. mapping_value = {}
  1671. # -> iterate through format
  1672. for name in format:
  1673. value = None
  1674. value = GetField(dobj, name, format[name], raw=raw, addroffset=addroffset)
  1675. if value is not None:
  1676. mapping_value[name] = value
  1677. # copy complete returned mapping
  1678. valuemapping = copy.deepcopy(mapping_value)
  1679. # a simple value
  1680. elif isinstance(format, (str, bool, int, float, long)):
  1681. if GetFieldLength(fielddef) != 0:
  1682. valuemapping = struct.unpack_from(format, dobj, baseaddr+addroffset)[0]
  1683. if not format[-1:].lower() in ['s','p']:
  1684. valuemapping = bitsRead(valuemapping, bitshift, bits)
  1685. # additional processing for strings
  1686. if format[-1:].lower() in ['s','p']:
  1687. # use left string until \0
  1688. s = str(valuemapping).split('\0')[0]
  1689. # remove character > 127
  1690. valuemapping = unicode(s, errors='ignore')
  1691. valuemapping = ReadWriteConverter(valuemapping, fielddef, read=True, raw=raw)
  1692. else:
  1693. exit(ExitCode.INTERNAL_ERROR, "Wrong mapping format definition: '{}'".format(format), typ=LogType.WARNING, doexit=not args.ignorewarning, line=inspect.getlineno(inspect.currentframe()))
  1694. return valuemapping
  1695. def SetField(dobj, fieldname, fielddef, restore, addroffset=0, filename=""):
  1696. """
  1697. Get field value from definition
  1698. @param dobj:
  1699. decrypted binary config data
  1700. @param fieldname:
  1701. name of the field
  1702. @param fielddef:
  1703. see Settings desc above
  1704. @param restore
  1705. restore mapping with the new value(s)
  1706. @param addroffset
  1707. use offset for baseaddr (used for recursive calls)
  1708. @param filename
  1709. related filename (for messages only)
  1710. @return:
  1711. new decrypted binary config data
  1712. """
  1713. format, baseaddr, bits, bitshift, arraydef, group, writeconverter = GetFieldDef(fielddef, fields='format, baseaddr, bits, bitshift, arraydef, group, writeconverter')
  1714. # cast unicode
  1715. fieldname = str(fieldname)
  1716. # filter groups
  1717. if not IsFilterGroup(group):
  1718. return dobj
  1719. # do not write readonly values
  1720. if writeconverter is False:
  1721. if args.debug:
  1722. print >> sys.stderr, "SetField(): Readonly '{}' using '{}'/{}{} @{} skipped".format(fieldname, format, arraydef, bits, hex(baseaddr+addroffset))
  1723. return dobj
  1724. # <arraydef> contains a list
  1725. if isinstance(arraydef, list) and len(arraydef) > 0:
  1726. offset = 0
  1727. if len(restore) > arraydef[0]:
  1728. exit(ExitCode.RESTORE_DATA_ERROR, "file '{sfile}', array '{sname}[{selem}]' exceeds max number of elements [{smax}]".format(sfile=filename, sname=fieldname, selem=len(restore), smax=arraydef[0]), typ=LogType.WARNING, doexit=not args.ignorewarning, line=inspect.getlineno(inspect.currentframe()))
  1729. for i in range(0, arraydef[0]):
  1730. subfielddef = GetSubfieldDef(fielddef)
  1731. length = GetFieldLength(subfielddef)
  1732. if length != 0:
  1733. if i >= len(restore): # restore data list may be shorter than definition
  1734. break
  1735. subrestore = restore[i]
  1736. dobj = SetField(dobj, fieldname, subfielddef, subrestore, addroffset=addroffset+offset, filename=filename)
  1737. offset += length
  1738. # <format> contains a dict
  1739. elif isinstance(format, dict):
  1740. for name in format: # -> iterate through format
  1741. if name in restore:
  1742. dobj = SetField(dobj, name, format[name], restore[name], addroffset=addroffset, filename=filename)
  1743. # a simple value
  1744. elif isinstance(format, (str, bool, int, float, long)):
  1745. valid = True
  1746. err = ""
  1747. errformat = ""
  1748. _min, _max = GetFieldMinMax(fielddef)
  1749. value = _value = None
  1750. skip = False
  1751. # simple char value
  1752. if format[-1:] in ['c']:
  1753. try:
  1754. value = ReadWriteConverter(restore.encode(STR_ENCODING)[0], fielddef, read=False)
  1755. except Exception, e:
  1756. exit(e[0], e[1], typ=LogType.WARNING, line=inspect.getlineno(inspect.currentframe()))
  1757. valid = False
  1758. # bool
  1759. elif format[-1:] in ['?']:
  1760. try:
  1761. value = ReadWriteConverter(bool(restore), fielddef, read=False)
  1762. except Exception, e:
  1763. exit(e[0], e[1], typ=LogType.WARNING, line=inspect.getlineno(inspect.currentframe()))
  1764. valid = False
  1765. # integer
  1766. elif format[-1:] in ['b','B','h','H','i','I','l','L','q','Q','P']:
  1767. value = ReadWriteConverter(restore, fielddef, read=False)
  1768. if isinstance(value, (str, unicode)):
  1769. value = int(value, 0)
  1770. else:
  1771. value = int(value)
  1772. # bits
  1773. if bits != 0:
  1774. bitvalue = value
  1775. value = struct.unpack_from(format, dobj, baseaddr+addroffset)[0]
  1776. # validate restore value
  1777. valid = ValidateValue(bitvalue, fielddef)
  1778. if not valid:
  1779. err = "valid bit range exceeding"
  1780. else:
  1781. mask = (1<<bits)-1
  1782. if bitvalue > mask:
  1783. _min = 0
  1784. _max = mask
  1785. _value = bitvalue
  1786. valid = False
  1787. else:
  1788. if bitshift >= 0:
  1789. bitvalue <<= bitshift
  1790. mask <<= bitshift
  1791. else:
  1792. bitvalue >>= abs(bitshift)
  1793. mask >>= abs(bitshift)
  1794. v=value
  1795. value &= (0xffffffff ^ mask)
  1796. value |= bitvalue
  1797. # full size values
  1798. else:
  1799. # validate restore function
  1800. valid = ValidateValue(value, fielddef)
  1801. if not valid:
  1802. err = "valid range exceeding"
  1803. _value = value
  1804. # float
  1805. elif format[-1:] in ['f','d']:
  1806. try:
  1807. value = ReadWriteConverter(float(restore), fielddef, read=False)
  1808. except:
  1809. valid = False
  1810. # string
  1811. elif format[-1:] in ['s','p']:
  1812. value = ReadWriteConverter(restore.encode(STR_ENCODING), fielddef, read=False)
  1813. err = "string length exceeding"
  1814. if value is not None:
  1815. # be aware 0 byte at end of string (str must be < max, not <= max)
  1816. _max -= 1
  1817. valid = _min <= len(value) < _max
  1818. else:
  1819. skip = True
  1820. valid = True
  1821. if value is None and not skip:
  1822. # None is an invalid value
  1823. valid = False
  1824. if valid is None and not skip:
  1825. # validate against object type size
  1826. valid = _min <= value <= _max
  1827. if not valid:
  1828. err = "type range exceeding"
  1829. errformat = " [{smin},{smax}]"
  1830. if _value is None:
  1831. # copy value before possible change below
  1832. _value = value
  1833. if isinstance(_value, (str, unicode)):
  1834. _value = "'{}'".format(_value)
  1835. if valid:
  1836. if not skip:
  1837. if args.debug:
  1838. if bits:
  1839. sbits=" {} bits shift {}".format(bits, bitshift)
  1840. else:
  1841. sbits = ""
  1842. print >> sys.stderr, "SetField(): Set '{}' using '{}'/{}{} @{} to {}".format(fieldname, format, arraydef, sbits, hex(baseaddr+addroffset), _value)
  1843. if fieldname != 'cfg_crc':
  1844. prevvalue = struct.unpack_from(format, dobj, baseaddr+addroffset)[0]
  1845. struct.pack_into(format, dobj, baseaddr+addroffset, value)
  1846. curvalue = struct.unpack_from(format, dobj, baseaddr+addroffset)[0]
  1847. if prevvalue != curvalue and args.verbose:
  1848. message("Value for '{}' changed from {} to {}".format(fieldname, prevvalue, curvalue), typ=LogType.INFO)
  1849. else:
  1850. sformat = "file '{sfile}' - {{'{sname}': {svalue}}} ({serror})"+errformat
  1851. exit(ExitCode.RESTORE_DATA_ERROR, sformat.format(sfile=filename, sname=fieldname, serror=err, svalue=_value, smin=_min, smax=_max), typ=LogType.WARNING, doexit=not args.ignorewarning)
  1852. return dobj
  1853. def SetCmnd(cmnds, fieldname, fielddef, valuemapping, mappedvalue, addroffset=0, idx=None):
  1854. """
  1855. Get field value from definition
  1856. @param cmnds:
  1857. Tasmota command mapping: { 'group': ['cmnd' <,'cmnd'...>] ... }
  1858. @param fieldname:
  1859. name of the field
  1860. @param fielddef:
  1861. see Settings desc above
  1862. @param valuemapping:
  1863. data mapping
  1864. @param mappedvalue
  1865. mappedvalue mapping with the new value(s)
  1866. @param addroffset
  1867. use offset for baseaddr (used for recursive calls)
  1868. @param idx
  1869. optional array index
  1870. @return:
  1871. new Tasmota command mapping
  1872. """
  1873. format, baseaddr, bits, bitshift, arraydef, group, tasmotacmnd, writeconverter = GetFieldDef(fielddef, fields='format, baseaddr, bits, bitshift, arraydef, group, tasmotacmnd, writeconverter')
  1874. # cast unicode
  1875. fieldname = str(fieldname)
  1876. # filter groups
  1877. if not IsFilterGroup(group):
  1878. return cmnds
  1879. # <arraydef> contains a list
  1880. if isinstance(arraydef, list) and len(arraydef) > 0:
  1881. offset = 0
  1882. if len(mappedvalue) > arraydef[0]:
  1883. exit(ExitCode.RESTORE_DATA_ERROR, "array '{sname}[{selem}]' exceeds max number of elements [{smax}]".format(sname=fieldname, selem=len(mappedvalue), smax=arraydef[0]), typ=LogType.WARNING, doexit=not args.ignorewarning, line=inspect.getlineno(inspect.currentframe()))
  1884. for i in range(0, arraydef[0]):
  1885. subfielddef = GetSubfieldDef(fielddef)
  1886. length = GetFieldLength(subfielddef)
  1887. if length != 0:
  1888. if i >= len(mappedvalue): # mappedvalue data list may be shorter than definition
  1889. break
  1890. subrestore = mappedvalue[i]
  1891. cmnds = SetCmnd(cmnds, fieldname, subfielddef, valuemapping, subrestore, addroffset=addroffset+offset, idx=i)
  1892. offset += length
  1893. # <format> contains a dict
  1894. elif isinstance(format, dict):
  1895. for name in format: # -> iterate through format
  1896. if name in mappedvalue:
  1897. cmnds = SetCmnd(cmnds, name, format[name], valuemapping, mappedvalue[name], addroffset=addroffset, idx=idx)
  1898. # a simple value
  1899. elif isinstance(format, (str, bool, int, float, long)):
  1900. cmnd = CmndConverter(valuemapping, mappedvalue, idx, fielddef)
  1901. if group is not None and cmnd is not None:
  1902. if group not in cmnds:
  1903. cmnds[group] = []
  1904. cmnds[group].append(cmnd)
  1905. return cmnds
  1906. def Bin2Mapping(decode_cfg):
  1907. """
  1908. Decodes binary data stream into pyhton mappings dict
  1909. @param decode_cfg:
  1910. binary config data (decrypted)
  1911. @return:
  1912. valuemapping data as mapping dictionary
  1913. """
  1914. if isinstance(decode_cfg, bytearray):
  1915. decode_cfg = str(decode_cfg)
  1916. # get binary header and template to use
  1917. version, size, setting = GetTemplateSetting(decode_cfg)
  1918. # if we did not found a mathching setting
  1919. if setting is None:
  1920. exit(ExitCode.UNSUPPORTED_VERSION, "Tasmota configuration version 0x{:x} not supported".format(version),line=inspect.getlineno(inspect.currentframe()))
  1921. if 'version' in setting:
  1922. cfg_version = GetField(decode_cfg, 'version', setting['version'], raw=True)
  1923. # check size if exists
  1924. if 'cfg_size' in setting:
  1925. cfg_size = GetField(decode_cfg, 'cfg_size', setting['cfg_size'], raw=True)
  1926. # read size should be same as definied in setting
  1927. if cfg_size > size:
  1928. # may be processed
  1929. exit(ExitCode.DATA_SIZE_MISMATCH, "Number of bytes read does ot match - read {}, expected {} byte".format(cfg_size, size), typ=LogType.ERROR,line=inspect.getlineno(inspect.currentframe()))
  1930. elif cfg_size < size:
  1931. # less number of bytes can not be processed
  1932. exit(ExitCode.DATA_SIZE_MISMATCH, "Number of bytes read to small to process - read {}, expected {} byte".format(cfg_size, size), typ=LogType.ERROR,line=inspect.getlineno(inspect.currentframe()))
  1933. # check crc if exists
  1934. if 'cfg_crc' in setting:
  1935. cfg_crc = GetField(decode_cfg, 'cfg_crc', setting['cfg_crc'], raw=True)
  1936. else:
  1937. cfg_crc = GetSettingsCrc(decode_cfg)
  1938. if cfg_crc != GetSettingsCrc(decode_cfg):
  1939. exit(ExitCode.DATA_CRC_ERROR, 'Data CRC error, read 0x{:x} should be 0x{:x}'.format(cfg_crc, GetSettingsCrc(decode_cfg)), typ=LogType.WARNING, doexit=not args.ignorewarning,line=inspect.getlineno(inspect.currentframe()))
  1940. # get valuemapping
  1941. valuemapping = GetField(decode_cfg, None, (setting,0,(None, None, (INTERNAL, None))))
  1942. # add header info
  1943. timestamp = datetime.now()
  1944. valuemapping['header'] = { 'timestamp':timestamp.strftime("%Y-%m-%d %H:%M:%S"),
  1945. 'format': {
  1946. 'jsonindent': args.jsonindent,
  1947. 'jsoncompact': args.jsoncompact,
  1948. 'jsonsort': args.jsonsort,
  1949. 'jsonhidepw': args.jsonhidepw,
  1950. },
  1951. 'template': {
  1952. 'version': hex(version),
  1953. 'crc': hex(cfg_crc),
  1954. },
  1955. 'data': {
  1956. 'crc': hex(GetSettingsCrc(decode_cfg)),
  1957. 'size': len(decode_cfg),
  1958. },
  1959. 'script': {
  1960. 'name': os.path.basename(__file__),
  1961. 'version': VER,
  1962. },
  1963. 'os': (platform.machine(), platform.system(), platform.release(), platform.version(), platform.platform()),
  1964. 'python': platform.python_version(),
  1965. }
  1966. if 'cfg_crc' in setting:
  1967. valuemapping['header']['template'].update({'size': cfg_size})
  1968. if 'version' in setting:
  1969. valuemapping['header']['data'].update({'version': hex(cfg_version)})
  1970. return valuemapping
  1971. def Mapping2Bin(decode_cfg, jsonconfig, filename=""):
  1972. """
  1973. Encodes into binary data stream
  1974. @param decode_cfg:
  1975. binary config data (decrypted)
  1976. @param jsonconfig:
  1977. restore data mapping
  1978. @param filename:
  1979. name of the restore file (for error output only)
  1980. @return:
  1981. changed binary config data (decrypted) or None on error
  1982. """
  1983. if isinstance(decode_cfg, str):
  1984. decode_cfg = bytearray(decode_cfg)
  1985. # get binary header data to use the correct version template from device
  1986. version, size, setting = GetTemplateSetting(decode_cfg)
  1987. # make empty binarray array
  1988. _buffer = bytearray()
  1989. # add data
  1990. _buffer.extend(decode_cfg)
  1991. if setting is not None:
  1992. # iterate through restore data mapping
  1993. for name in jsonconfig:
  1994. # key must exist in both dict
  1995. if name in setting:
  1996. SetField(_buffer, name, setting[name], jsonconfig[name], addroffset=0, filename=filename)
  1997. else:
  1998. if name != 'header':
  1999. exit(ExitCode.RESTORE_DATA_ERROR, "Restore file '{}' contains obsolete name '{}', skipped".format(filename, name), typ=LogType.WARNING, doexit=not args.ignorewarning)
  2000. if 'cfg_crc' in setting:
  2001. crc = GetSettingsCrc(_buffer)
  2002. struct.pack_into(setting['cfg_crc'][0], _buffer, setting['cfg_crc'][1], crc)
  2003. return _buffer
  2004. else:
  2005. exit(ExitCode.UNSUPPORTED_VERSION,"File '{}', Tasmota configuration version 0x{:x} not supported".format(filename, version), typ=LogType.WARNING, doexit=not args.ignorewarning)
  2006. return None
  2007. def Mapping2Cmnd(decode_cfg, valuemapping, filename=""):
  2008. """
  2009. Encodes mapping data into Tasmota command mapping
  2010. @param decode_cfg:
  2011. binary config data (decrypted)
  2012. @param valuemapping:
  2013. data mapping
  2014. @param filename:
  2015. name of the restore file (for error output only)
  2016. @return:
  2017. Tasmota command mapping {group: [cmnd <,cmnd <,...>>]}
  2018. """
  2019. if isinstance(decode_cfg, str):
  2020. decode_cfg = bytearray(decode_cfg)
  2021. # get binary header data to use the correct version template from device
  2022. version, size, setting = GetTemplateSetting(decode_cfg)
  2023. cmnds = {}
  2024. if setting is not None:
  2025. # iterate through restore data mapping
  2026. for name in valuemapping:
  2027. # key must exist in both dict
  2028. if name in setting:
  2029. cmnds = SetCmnd(cmnds, name, setting[name], valuemapping, valuemapping[name], addroffset=0)
  2030. else:
  2031. if name != 'header':
  2032. exit(ExitCode.RESTORE_DATA_ERROR, "Restore file '{}' contains obsolete name '{}', skipped".format(filename, name), typ=LogType.WARNING, doexit=not args.ignorewarning)
  2033. return cmnds
  2034. else:
  2035. exit(ExitCode.UNSUPPORTED_VERSION,"File '{}', Tasmota configuration version 0x{:x} not supported".format(filename, version), typ=LogType.WARNING, doexit=not args.ignorewarning)
  2036. return None
  2037. def Backup(backupfile, backupfileformat, encode_cfg, decode_cfg, configmapping):
  2038. """
  2039. Create backup file
  2040. @param backupfile:
  2041. Raw backup filename from program args
  2042. @param backupfileformat:
  2043. Backup file format
  2044. @param encode_cfg:
  2045. binary config data (encrypted)
  2046. @param decode_cfg:
  2047. binary config data (decrypted)
  2048. @param configmapping:
  2049. config data mapppings
  2050. """
  2051. name, ext = os.path.splitext(backupfile)
  2052. if ext.lower() == '.'+FileType.BIN.lower():
  2053. backupfileformat = FileType.BIN
  2054. elif ext.lower() == '.'+FileType.DMP.lower():
  2055. backupfileformat = FileType.DMP
  2056. elif ext.lower() == '.'+FileType.JSON.lower():
  2057. backupfileformat = FileType.JSON
  2058. fileformat = ""
  2059. # Tasmota format
  2060. if backupfileformat.lower() == FileType.DMP.lower():
  2061. fileformat = "Tasmota"
  2062. backup_filename = MakeFilename(backupfile, FileType.DMP, configmapping)
  2063. if args.verbose:
  2064. message("Writing backup file '{}' ({} format)".format(backup_filename, fileformat), typ=LogType.INFO)
  2065. try:
  2066. backupfp = open(backup_filename, "wb")
  2067. backupfp.write(encode_cfg)
  2068. except Exception, e:
  2069. exit(e[0], "'{}' {}".format(backup_filename, e[1]),line=inspect.getlineno(inspect.currentframe()))
  2070. finally:
  2071. backupfp.close()
  2072. # binary format
  2073. elif backupfileformat.lower() == FileType.BIN.lower():
  2074. fileformat = "binary"
  2075. backup_filename = MakeFilename(backupfile, FileType.BIN, configmapping)
  2076. if args.verbose:
  2077. message("Writing backup file '{}' ({} format)".format(backup_filename, fileformat), typ=LogType.INFO)
  2078. try:
  2079. backupfp = open(backup_filename, "wb")
  2080. magic = BINARYFILE_MAGIC
  2081. backupfp.write(struct.pack('<L',magic))
  2082. backupfp.write(decode_cfg)
  2083. except Exception, e:
  2084. exit(e[0], "'{}' {}".format(backup_filename, e[1]),line=inspect.getlineno(inspect.currentframe()))
  2085. finally:
  2086. backupfp.close()
  2087. # JSON format
  2088. elif backupfileformat.lower() == FileType.JSON.lower():
  2089. fileformat = "JSON"
  2090. backup_filename = MakeFilename(backupfile, FileType.JSON, configmapping)
  2091. if args.verbose:
  2092. message("Writing backup file '{}' ({} format)".format(backup_filename, fileformat), typ=LogType.INFO)
  2093. try:
  2094. backupfp = open(backup_filename, "w")
  2095. json.dump(configmapping, backupfp, sort_keys=args.jsonsort, indent=None if args.jsonindent < 0 else args.jsonindent, separators=(',', ':') if args.jsoncompact else (', ', ': ') )
  2096. except Exception, e:
  2097. exit(e[0], "'{}' {}".format(backup_filename, e[1]),line=inspect.getlineno(inspect.currentframe()))
  2098. finally:
  2099. backupfp.close()
  2100. if args.verbose:
  2101. srctype = 'device'
  2102. src = args.device
  2103. if args.tasmotafile is not None:
  2104. srctype = 'file'
  2105. src = args.tasmotafile
  2106. message("Backup successful from {} '{}' to file '{}' ({} format)".format(srctype, src, backup_filename, fileformat), typ=LogType.INFO)
  2107. def Restore(restorefile, backupfileformat, encode_cfg, decode_cfg, configmapping):
  2108. """
  2109. Restore from file
  2110. @param encode_cfg:
  2111. binary config data (encrypted)
  2112. @param backupfileformat:
  2113. Backup file format
  2114. @param decode_cfg:
  2115. binary config data (decrypted)
  2116. @param configmapping:
  2117. config data mapppings
  2118. """
  2119. new_encode_cfg = None
  2120. restorefileformat = None
  2121. if backupfileformat.lower() == 'bin':
  2122. restorefileformat = FileType.BIN
  2123. elif backupfileformat.lower() == 'dmp':
  2124. restorefileformat = FileType.DMP
  2125. elif backupfileformat.lower() == 'json':
  2126. restorefileformat = FileType.JSON
  2127. restorefilename = MakeFilename(restorefile, restorefileformat, configmapping)
  2128. filetype = GetFileType(restorefilename)
  2129. if filetype == FileType.DMP:
  2130. if args.verbose:
  2131. message("Reading restore file '{}' (Tasmota format)".format(restorefilename), typ=LogType.INFO)
  2132. try:
  2133. restorefp = open(restorefilename, "rb")
  2134. new_encode_cfg = restorefp.read()
  2135. restorefp.close()
  2136. except Exception, e:
  2137. exit(e[0], "'{}' {}".format(restorefilename, e[1]),line=inspect.getlineno(inspect.currentframe()))
  2138. elif filetype == FileType.BIN:
  2139. if args.verbose:
  2140. message("Reading restore file '{}' (binary format)".format(restorefilename), typ=LogType.INFO)
  2141. try:
  2142. restorefp = open(restorefilename, "rb")
  2143. restorebin = restorefp.read()
  2144. restorefp.close()
  2145. except Exception, e:
  2146. exit(e[0], "'{}' {}".format(restorefilename, e[1]),line=inspect.getlineno(inspect.currentframe()))
  2147. header = struct.unpack_from('<L', restorebin, 0)[0]
  2148. if header == BINARYFILE_MAGIC:
  2149. decode_cfg = restorebin[4:] # remove header from encrypted config file
  2150. new_encode_cfg = DecryptEncrypt(decode_cfg) # process binary to binary config
  2151. elif filetype == FileType.JSON or filetype == FileType.INVALID_JSON:
  2152. if args.verbose:
  2153. message("Reading restore file '{}' (JSON format)".format(restorefilename), typ=LogType.INFO)
  2154. try:
  2155. restorefp = open(restorefilename, "r")
  2156. jsonconfig = json.load(restorefp)
  2157. except ValueError as e:
  2158. exit(ExitCode.JSON_READ_ERROR, "File '{}' invalid JSON: {}".format(restorefilename, e), line=inspect.getlineno(inspect.currentframe()))
  2159. finally:
  2160. restorefp.close()
  2161. # process json config to binary config
  2162. new_decode_cfg = Mapping2Bin(decode_cfg, jsonconfig, restorefilename)
  2163. new_encode_cfg = DecryptEncrypt(new_decode_cfg)
  2164. elif filetype == FileType.FILE_NOT_FOUND:
  2165. exit(ExitCode.FILE_NOT_FOUND, "File '{}' not found".format(restorefilename),line=inspect.getlineno(inspect.currentframe()))
  2166. elif filetype == FileType.INCOMPLETE_JSON:
  2167. exit(ExitCode.JSON_READ_ERROR, "File '{}' incomplete JSON, missing name 'header'".format(restorefilename),line=inspect.getlineno(inspect.currentframe()))
  2168. elif filetype == FileType.INVALID_BIN:
  2169. exit(ExitCode.FILE_READ_ERROR, "File '{}' invalid BIN format".format(restorefilename),line=inspect.getlineno(inspect.currentframe()))
  2170. else:
  2171. exit(ExitCode.FILE_READ_ERROR, "File '{}' unknown error".format(restorefilename),line=inspect.getlineno(inspect.currentframe()))
  2172. if new_encode_cfg is not None:
  2173. if args.forcerestore or new_encode_cfg != encode_cfg:
  2174. # write config direct to device via http
  2175. if args.device is not None:
  2176. if args.verbose:
  2177. message("Push new data to '{}' using restore file '{}'".format(args.device, restorefilename), typ=LogType.INFO)
  2178. error_code, error_str = PushTasmotaConfig(new_encode_cfg, args.device, args.port, args.username, args.password)
  2179. if error_code:
  2180. exit(ExitCode.UPLOAD_CONFIG_ERROR, "Config data upload failed - {}".format(error_str),line=inspect.getlineno(inspect.currentframe()))
  2181. else:
  2182. if args.verbose:
  2183. message("Restore successful to device '{}' using restore file '{}'".format(args.device, restorefilename), typ=LogType.INFO)
  2184. # write config from a file
  2185. elif args.tasmotafile is not None:
  2186. if args.verbose:
  2187. message("Write new data to file '{}' using restore file '{}'".format(args.tasmotafile, restorefilename), typ=LogType.INFO)
  2188. try:
  2189. outputfile = open(args.tasmotafile, "wb")
  2190. outputfile.write(new_encode_cfg)
  2191. except Exception, e:
  2192. exit(e[0], "'{}' {}".format(args.tasmotafile, e[1]),line=inspect.getlineno(inspect.currentframe()))
  2193. finally:
  2194. outputfile.close()
  2195. if args.verbose:
  2196. message("Restore successful to file '{}' using restore file '{}'".format(args.tasmotafile, restorefilename), typ=LogType.INFO)
  2197. else:
  2198. global exitcode
  2199. exitcode = ExitCode.RESTORE_SKIPPED
  2200. if args.verbose:
  2201. message("Configuration data leaving unchanged", typ=LogType.INFO)
  2202. def OutputTasmotaCmnds(tasmotacmnds):
  2203. """
  2204. Print Tasmota command mapping
  2205. @param tasmotacmnds:
  2206. Tasmota command mapping {group: [cmnd <,cmnd <,...>>]}
  2207. """
  2208. def OutputTasmotaSubCmnds(cmnds):
  2209. if args.cmndsort:
  2210. for cmnd in sorted(cmnds, key = lambda cmnd:[int(c) if c.isdigit() else c for c in re.split('(\d+)', cmnd)]):
  2211. print "{}{}".format(" "*args.cmndindent, cmnd)
  2212. else:
  2213. for cmnd in cmnds:
  2214. print "{}{}".format(" "*args.cmndindent, cmnd)
  2215. if args.cmndgroup:
  2216. for group in Groups:
  2217. if group in tasmotacmnds:
  2218. cmnds = tasmotacmnds[group]
  2219. print
  2220. print "# {}:".format(group)
  2221. OutputTasmotaSubCmnds(cmnds)
  2222. else:
  2223. cmnds = []
  2224. for group in Groups:
  2225. if group in tasmotacmnds:
  2226. cmnds.extend(tasmotacmnds[group])
  2227. OutputTasmotaSubCmnds(cmnds)
  2228. def ParseArgs():
  2229. """
  2230. Program argument parser
  2231. @return:
  2232. configargparse.parse_args() result
  2233. """
  2234. global parser
  2235. parser = configargparse.ArgumentParser(description='Backup/Restore Sonoff-Tasmota configuration data.',
  2236. epilog='Either argument -d <host> or -f <filename> must be given.',
  2237. add_help=False,
  2238. formatter_class=lambda prog: CustomHelpFormatter(prog))
  2239. source = parser.add_argument_group('Source', 'Read/Write Tasmota configuration from/to')
  2240. source.add_argument('-f', '--file', '--tasmota-file',
  2241. metavar='<filename>',
  2242. dest='tasmotafile',
  2243. default=DEFAULTS['source']['tasmotafile'],
  2244. help="file to retrieve/write Tasmota configuration from/to (default: {})'".format(DEFAULTS['source']['tasmotafile']))
  2245. source.add_argument('-d', '--device', '--host',
  2246. metavar='<host>',
  2247. dest='device',
  2248. default=DEFAULTS['source']['device'],
  2249. help="hostname or IP address to retrieve/send Tasmota configuration from/to (default: {})".format(DEFAULTS['source']['device']) )
  2250. source.add_argument('-P', '--port',
  2251. metavar='<port>',
  2252. dest='port',
  2253. default=DEFAULTS['source']['port'],
  2254. help="TCP/IP port number to use for the host connection (default: {})".format(DEFAULTS['source']['port']) )
  2255. source.add_argument('-u', '--username',
  2256. metavar='<username>',
  2257. dest='username',
  2258. default=DEFAULTS['source']['username'],
  2259. help="host HTTP access username (default: {})".format(DEFAULTS['source']['username']))
  2260. source.add_argument('-p', '--password',
  2261. metavar='<password>',
  2262. dest='password',
  2263. default=DEFAULTS['source']['password'],
  2264. help="host HTTP access password (default: {})".format(DEFAULTS['source']['password']))
  2265. backup = parser.add_argument_group('Backup/Restore', 'Backup & restore specification')
  2266. backup.add_argument('-i', '--restore-file',
  2267. metavar='<filename>',
  2268. dest='restorefile',
  2269. default=DEFAULTS['backup']['backupfile'],
  2270. help="file to restore configuration from (default: {}). Replacements: @v=firmware version from config, @f=device friendly name from config, @h=device hostname from config, @H=device hostname from device (-d arg only)".format(DEFAULTS['backup']['restorefile']))
  2271. backup.add_argument('-o', '--backup-file',
  2272. metavar='<filename>',
  2273. dest='backupfile',
  2274. default=DEFAULTS['backup']['backupfile'],
  2275. help="file to backup configuration to (default: {}). Replacements: @v=firmware version from config, @f=device friendly name from config, @h=device hostname from config, @H=device hostname from device (-d arg only)".format(DEFAULTS['backup']['backupfile']))
  2276. backup_file_formats = ['json', 'bin', 'dmp']
  2277. backup.add_argument('-t', '--backup-type',
  2278. metavar='|'.join(backup_file_formats),
  2279. dest='backupfileformat',
  2280. choices=backup_file_formats,
  2281. default=DEFAULTS['backup']['backupfileformat'],
  2282. help="backup filetype (default: '{}')".format(DEFAULTS['backup']['backupfileformat']) )
  2283. backup.add_argument('-E', '--extension',
  2284. dest='extension',
  2285. action='store_true',
  2286. default=DEFAULTS['backup']['extension'],
  2287. help="append filetype extension for -i and -o filename{}".format(' (default)' if DEFAULTS['backup']['extension'] else '') )
  2288. backup.add_argument('-e', '--no-extension',
  2289. dest='extension',
  2290. action='store_false',
  2291. default=DEFAULTS['backup']['extension'],
  2292. help="do not append filetype extension, use -i and -o filename as passed{}".format(' (default)' if not DEFAULTS['backup']['extension'] else '') )
  2293. backup.add_argument('-F', '--force-restore',
  2294. dest='forcerestore',
  2295. action='store_true',
  2296. default=DEFAULTS['backup']['forcerestore'],
  2297. help="force restore even configuration is identical{}".format(' (default)' if DEFAULTS['backup']['forcerestore'] else '') )
  2298. jsonformat = parser.add_argument_group('JSON output', 'JSON format specification')
  2299. jsonformat.add_argument('--json-indent',
  2300. metavar='<indent>',
  2301. dest='jsonindent',
  2302. type=int,
  2303. default=DEFAULTS['jsonformat']['jsonindent'],
  2304. help="pretty-printed JSON output using indent level (default: '{}'). -1 disables indent.".format(DEFAULTS['jsonformat']['jsonindent']) )
  2305. jsonformat.add_argument('--json-compact',
  2306. dest='jsoncompact',
  2307. action='store_true',
  2308. default=DEFAULTS['jsonformat']['jsoncompact'],
  2309. help="compact JSON output by eliminate whitespace{}".format(' (default)' if DEFAULTS['jsonformat']['jsoncompact'] else '') )
  2310. jsonformat.add_argument('--json-sort',
  2311. dest='jsonsort',
  2312. action='store_true',
  2313. default=DEFAULTS['jsonformat']['jsonsort'],
  2314. help=configargparse.SUPPRESS) #"sort json keywords{}".format(' (default)' if DEFAULTS['jsonformat']['jsonsort'] else '') )
  2315. jsonformat.add_argument('--json-unsort',
  2316. dest='jsonsort',
  2317. action='store_false',
  2318. default=DEFAULTS['jsonformat']['jsonsort'],
  2319. help=configargparse.SUPPRESS) #"do not sort json keywords{}".format(' (default)' if not DEFAULTS['jsonformat']['jsonsort'] else '') )
  2320. jsonformat.add_argument('--json-hide-pw',
  2321. dest='jsonhidepw',
  2322. action='store_true',
  2323. default=DEFAULTS['jsonformat']['jsonhidepw'],
  2324. help="hide passwords{}".format(' (default)' if DEFAULTS['jsonformat']['jsonhidepw'] else '') )
  2325. jsonformat.add_argument('--json-show-pw', '--json-unhide-pw',
  2326. dest='jsonhidepw',
  2327. action='store_false',
  2328. default=DEFAULTS['jsonformat']['jsonhidepw'],
  2329. help="unhide passwords{}".format(' (default)' if not DEFAULTS['jsonformat']['jsonhidepw'] else '') )
  2330. cmndformat = parser.add_argument_group('Tasmota command output', 'Tasmota command output format specification')
  2331. cmndformat.add_argument('--cmnd-indent',
  2332. metavar='<indent>',
  2333. dest='cmndindent',
  2334. type=int,
  2335. default=DEFAULTS['cmndformat']['cmndindent'],
  2336. help="Tasmota command grouping indent level (default: '{}'). 0 disables indent".format(DEFAULTS['cmndformat']['cmndindent']) )
  2337. cmndformat.add_argument('--cmnd-groups',
  2338. dest='cmndgroup',
  2339. action='store_true',
  2340. default=DEFAULTS['cmndformat']['cmndgroup'],
  2341. help="group Tasmota commands{}".format(' (default)' if DEFAULTS['cmndformat']['cmndgroup'] else '') )
  2342. cmndformat.add_argument('--cmnd-nogroups',
  2343. dest='cmndgroup',
  2344. action='store_false',
  2345. default=DEFAULTS['cmndformat']['cmndgroup'],
  2346. help="leave Tasmota commands ungrouped{}".format(' (default)' if not DEFAULTS['cmndformat']['cmndgroup'] else '') )
  2347. cmndformat.add_argument('--cmnd-sort',
  2348. dest='cmndsort',
  2349. action='store_true',
  2350. default=DEFAULTS['cmndformat']['cmndsort'],
  2351. help="sort Tasmota commands{}".format(' (default)' if DEFAULTS['cmndformat']['cmndsort'] else '') )
  2352. cmndformat.add_argument('--cmnd-unsort',
  2353. dest='cmndsort',
  2354. action='store_false',
  2355. default=DEFAULTS['cmndformat']['cmndsort'],
  2356. help="leave Tasmota commands unsorted{}".format(' (default)' if not DEFAULTS['cmndformat']['cmndsort'] else '') )
  2357. common = parser.add_argument_group('Common', 'Optional arguments')
  2358. common.add_argument('-c', '--config',
  2359. metavar='<filename>',
  2360. dest='configfile',
  2361. default=DEFAULTS['common']['configfile'],
  2362. is_config_file=True,
  2363. help="program config file - can be used to set default command args (default: {})".format(DEFAULTS['common']['configfile']) )
  2364. common.add_argument('-S', '--output',
  2365. dest='output',
  2366. action='store_true',
  2367. default=DEFAULTS['common']['output'],
  2368. help="display output regardsless of backup/restore usage{}".format(" (default)" if DEFAULTS['common']['output'] else " (default do not output on backup or restore usage)") )
  2369. output_formats = ['json', 'cmnd','command']
  2370. common.add_argument('-T', '--output-format',
  2371. metavar='|'.join(output_formats),
  2372. dest='outputformat',
  2373. choices=output_formats,
  2374. default=DEFAULTS['common']['outputformat'],
  2375. help="display output format (default: '{}')".format(DEFAULTS['common']['outputformat']) )
  2376. groups = GetGroupList(Settings[0][2])
  2377. if '*' in groups:
  2378. groups.remove('*')
  2379. common.add_argument('-g', '--group',
  2380. dest='filter',
  2381. choices=groups,
  2382. nargs='+',
  2383. default=DEFAULTS['common']['filter'],
  2384. help="limit data processing to command groups (default {})".format("no filter" if DEFAULTS['common']['filter'] == None else DEFAULTS['common']['filter']) )
  2385. common.add_argument('--ignore-warnings',
  2386. dest='ignorewarning',
  2387. action='store_true',
  2388. default=DEFAULTS['common']['ignorewarning'],
  2389. help="do not exit on warnings{}. Not recommended, used by your own responsibility!".format(' (default)' if DEFAULTS['common']['ignorewarning'] else '') )
  2390. info = parser.add_argument_group('Info','Extra information')
  2391. info.add_argument('-D', '--debug',
  2392. dest='debug',
  2393. action='store_true',
  2394. help=configargparse.SUPPRESS)
  2395. info.add_argument('-h', '--help',
  2396. dest='shorthelp',
  2397. action='store_true',
  2398. help='show usage help message and exit')
  2399. info.add_argument("-H", "--full-help",
  2400. action="help",
  2401. help="show full help message and exit")
  2402. info.add_argument('-v', '--verbose',
  2403. dest='verbose',
  2404. action='store_true',
  2405. help='produce more output about what the program does')
  2406. info.add_argument('-V', '--version',
  2407. action='version',
  2408. version=PROG)
  2409. args = parser.parse_args()
  2410. if args.debug:
  2411. print >> sys.stderr, parser.format_values()
  2412. print >> sys.stderr, "Settings:"
  2413. for k in args.__dict__:
  2414. print >> sys.stderr, " "+str(k), "= ",eval('args.{}'.format(k))
  2415. return args
  2416. if __name__ == "__main__":
  2417. args = ParseArgs()
  2418. if args.shorthelp:
  2419. ShortHelp()
  2420. # check source args
  2421. if args.device is not None and args.tasmotafile is not None:
  2422. exit(ExitCode.ARGUMENT_ERROR, "Unable to select source, do not use -d and -f together",line=inspect.getlineno(inspect.currentframe()))
  2423. # default no configuration available
  2424. encode_cfg = None
  2425. # pull config from Tasmota device
  2426. if args.tasmotafile is not None:
  2427. if args.verbose:
  2428. message("Load data from file '{}'".format(args.tasmotafile), typ=LogType.INFO)
  2429. encode_cfg = LoadTasmotaConfig(args.tasmotafile)
  2430. # load config from Tasmota file
  2431. if args.device is not None:
  2432. if args.verbose:
  2433. message("Load data from device '{}'".format(args.device), typ=LogType.INFO)
  2434. encode_cfg = PullTasmotaConfig(args.device, args.port, username=args.username, password=args.password)
  2435. if encode_cfg is None:
  2436. # no config source given
  2437. ShortHelp(False)
  2438. print
  2439. print parser.epilog
  2440. sys.exit(ExitCode.OK)
  2441. if len(encode_cfg) == 0:
  2442. exit(ExitCode.FILE_READ_ERROR, "Unable to read configuration data from {} '{}'".format('device' if args.device is not None else 'file', \
  2443. args.device if args.device is not None else args.tasmotafile) \
  2444. ,line=inspect.getlineno(inspect.currentframe()) )
  2445. # decrypt Tasmota config
  2446. decode_cfg = DecryptEncrypt(encode_cfg)
  2447. # decode into mappings dictionary
  2448. configmapping = Bin2Mapping(decode_cfg)
  2449. if args.verbose and 'version' in configmapping:
  2450. message("{} '{}' is using version {}".format('File' if args.tasmotafile is not None else 'Device',
  2451. args.tasmotafile if args.tasmotafile is not None else args.device,
  2452. GetVersionStr(configmapping['version'])),
  2453. typ=LogType.INFO)
  2454. # backup to file
  2455. if args.backupfile is not None:
  2456. Backup(args.backupfile, args.backupfileformat, encode_cfg, decode_cfg, configmapping)
  2457. # restore from file
  2458. if args.restorefile is not None:
  2459. Restore(args.restorefile, args.backupfileformat, encode_cfg, decode_cfg, configmapping)
  2460. # json screen output
  2461. if (args.backupfile is None and args.restorefile is None) or args.output:
  2462. if args.outputformat == 'json':
  2463. print json.dumps(configmapping, sort_keys=args.jsonsort, indent=None if args.jsonindent<0 else args.jsonindent, separators=(',', ':') if args.jsoncompact else (', ', ': ') )
  2464. if args.outputformat == 'cmnd' or args.outputformat == 'command':
  2465. tasmotacmnds = Mapping2Cmnd(decode_cfg, configmapping)
  2466. OutputTasmotaCmnds(tasmotacmnds)
  2467. sys.exit(exitcode)