s0meters.py 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985
  1. #!/usr/bin/python3 -u
  2. #
  3. import serial
  4. #from time import sleep
  5. import time
  6. import os
  7. import sys
  8. import paho.mqtt.client as mqtt
  9. import json
  10. import yaml
  11. from yaml.constructor import ConstructorError
  12. from threading import Thread
  13. from influxdb import InfluxDBClient
  14. import configparser
  15. #from datetime import datetime
  16. import datetime
  17. # Change working dir to the same dir as this script
  18. os.chdir(sys.path[0])
  19. config = configparser.ConfigParser()
  20. config.read('s0meters.ini')
  21. quiet = False
  22. verbose = False
  23. debug = False
  24. if len(sys.argv) >= 2:
  25. if sys.argv[1] == "-q":
  26. verbose = False
  27. quiet = True
  28. debug = False
  29. elif sys.argv[1] == "-v":
  30. verbose = True
  31. quiet = False
  32. debug = False
  33. print("VERBOSE=ON")
  34. elif sys.argv[1] == "-d":
  35. verbose = True
  36. quiet = False
  37. debug = True
  38. print("DEBUG=ON")
  39. consoleAddTimestamp = config['main'].get('consoleAddTimestamp')
  40. conf_storage_path = config['filelog'].get('storage_path')
  41. if conf_storage_path[-1:] != '/':
  42. conf_storage_path += '/'
  43. ser = serial.Serial(port=config['hardware'].get('serialPort'),
  44. baudrate = config['hardware'].get('serialBaud'),
  45. parity=serial.PARITY_NONE,
  46. stopbits=serial.STOPBITS_ONE,
  47. bytesize=serial.EIGHTBITS,
  48. timeout=config['hardware'].getint('serialTout'))
  49. if not quiet:
  50. print("S0MetersLog by Flo Kra")
  51. print("=======================================================================")
  52. meters_yaml = yaml.load(open(config['main'].get('meters_config_yml')), Loader=yaml.SafeLoader)
  53. if not quiet:
  54. print("Meters configuration:")
  55. print(json.dumps(meters_yaml, indent=2))
  56. print("loading InfluxDB configuration...")
  57. influxdb_yaml = yaml.load(open(config['main'].get('influx_config_yml')), Loader=yaml.SafeLoader)
  58. if not quiet:
  59. print("InfluxDB Instances:")
  60. print(json.dumps(influxdb_yaml, indent=4))
  61. influxclient = dict()
  62. for instance in influxdb_yaml:
  63. i_host = influxdb_yaml[instance].get('host', None)
  64. i_port = int(influxdb_yaml[instance].get('port', 8086))
  65. i_username = influxdb_yaml[instance].get('username', None)
  66. i_password = influxdb_yaml[instance].get('password', None)
  67. i_database = influxdb_yaml[instance].get('database', None)
  68. if i_host != None and i_database != None:
  69. if i_username != None and i_password != None:
  70. influxclient[instance] = InfluxDBClient(i_host, i_port, i_username, i_password, i_database)
  71. else:
  72. influxclient[instance] = InfluxDBClient(i_host, i_port, i_database)
  73. data_energy = dict()
  74. data_momentary = dict()
  75. saved_today_date = dict()
  76. influxdb_energy_lastWriteTime = dict()
  77. energy_history_lastPublished = dict()
  78. saved_energy_today_min = dict()
  79. saved_energy_yesterday_total = dict()
  80. for meter in meters_yaml:
  81. data_energy[meter] = dict()
  82. data_momentary[meter] = dict()
  83. MQTTenabled = config['mqtt'].getboolean('enable', False)
  84. if MQTTenabled:
  85. def on_connect(client, userdata, flags, rc):
  86. if not quiet:
  87. print("MQTT: connected with result code " + str(rc))
  88. client.subscribe(config['mqtt'].get('topic_cmd'))
  89. if not quiet:
  90. print("MQTT: subscribed topic:", config['mqtt'].get('topic_cmd'))
  91. def on_message(client, userdata, msg):
  92. if not quiet:
  93. print("MQTT incoming on topic", msg.topic)
  94. if msg.topic == config['mqtt'].get('topic_cmd'):
  95. if not quiet:
  96. print("MQTT CMD:", msg.payload)
  97. cmd = msg.payload.decode('ascii') + '\n'
  98. ser.write(cmd.encode('ascii'))
  99. mqttc = mqtt.Client()
  100. mqttc.on_connect = on_connect
  101. #mqttc.on_disconnect = on_disconnect
  102. mqttc.on_message = on_message
  103. if config['mqtt'].get('user') != "" and config['mqtt'].get('password') != "":
  104. mqttc.username_pw_set(config['mqtt'].get('user'), config['mqtt'].get('password'))
  105. mqttc.connect(config['mqtt'].get('server'), config['mqtt'].getint('port'), config['mqtt'].getint('keepalive'))
  106. mqttc.loop_start()
  107. def processMeterData(data):
  108. try:
  109. cJson = json.loads(data)
  110. except:
  111. cJson = False
  112. #print(json.dumps(cJson, indent=1))
  113. #print(cJson['C'])
  114. #print(json.dumps(meters_yaml[cJson['C']]))
  115. #print(json.dumps(cJson))
  116. # parse JSON object received from DualS0ImpCounter hardware
  117. if cJson:
  118. # extract variables from JSON
  119. cNum = cJson.get('C')
  120. #print(cNum)
  121. cReading = float(cJson.get('reading', None))
  122. dTime = cJson.get('dTime', None)
  123. # declare variables for program sequence control
  124. write_energy_to_influxdb = False
  125. # get config data for this meter number
  126. cName = meters_yaml[cNum].get('name', None)
  127. statTopic = meters_yaml[cNum].get('statTopic', None)
  128. unit = meters_yaml[cNum].get('unit', None)
  129. publishJSON = meters_yaml[cNum].get('MQTTPublishJSON', False)
  130. extendJSON = False
  131. if publishJSON or verbose: extendJSON = True
  132. conv_unit = meters_yaml[cNum].get('conv_unit', None)
  133. conv_factor = meters_yaml[cNum].get('conv_factor', None)
  134. conv_digits = meters_yaml[cNum].get('conv_digits', None)
  135. if conv_digits is None: conv_digits = 2
  136. cost_unit = meters_yaml[cNum].get('cost_unit', None)
  137. cost_per_unit = meters_yaml[cNum].get('cost_per_unit', None)
  138. cost_from_conv = meters_yaml[cNum].get('cost_from_conv', False)
  139. momUnit = meters_yaml[cNum].get('momUnit', None)
  140. momType = meters_yaml[cNum].get('momType', None)
  141. readingType = meters_yaml[cNum].get('readingType', None)
  142. momUnit_conv1 = meters_yaml[cNum].get('momUnit_conv1', None)
  143. momType_conv1 = meters_yaml[cNum].get('momType_conv1', None)
  144. momUnit_conv2 = meters_yaml[cNum].get('momUnit_conv2', None)
  145. momType_conv2 = meters_yaml[cNum].get('momType_conv2', None)
  146. impPerUnit = meters_yaml[cNum].get('impPerUnit', None)
  147. momFactor = meters_yaml[cNum].get('momFactor', None)
  148. if momFactor is None:
  149. momFactor = 1
  150. momDigits = meters_yaml[cNum].get('momDigits', None)
  151. if momDigits is None:
  152. momDigits = 3
  153. momFactor_conv1 = meters_yaml[cNum].get('momFactor_conv1', None)
  154. if momFactor_conv1 is None:
  155. momFactor_conv1 = 1
  156. momDigits_conv1 = meters_yaml[cNum].get('momDigits_conv1', None)
  157. if momDigits_conv1 is None:
  158. momDigits_conv1 = 3
  159. momFactor_conv2 = meters_yaml[cNum].get('momFactor_conv2', None)
  160. if momFactor_conv2 is None:
  161. momFactor_conv2 = 1
  162. momDigits_conv2 = meters_yaml[cNum].get('momDigits_conv2', None)
  163. if momDigits_conv2 is None:
  164. momDigits_conv2 = 3
  165. digits = meters_yaml[cNum].get('digits', None)
  166. influxMinWriteInterval = meters_yaml[cNum].get('influxMinWriteInterval_energy', None)
  167. if influxMinWriteInterval == None: influxMinWriteInterval = 0
  168. historyPublishInterval = meters_yaml[cNum].get('historyPublishInterval', 300)
  169. billingStartDate = meters_yaml[cNum].get('billingStartDate', None)
  170. # add meter name to JSON object
  171. if cName is not None and extendJSON:
  172. cJson['name'] = cName
  173. # check for date rollover since last impulse
  174. today = datetime.date.today()
  175. today_str = today.strftime('%Y%m%d')
  176. today_year_str = today.strftime('%Y')
  177. today_mon_str = today.strftime('%m')
  178. yesterday = today - datetime.timedelta(days = 1)
  179. yesterday_str = yesterday.strftime('%Y%m%d')
  180. yesterday_year_str = yesterday.strftime('%Y')
  181. yesterday_mon_str = yesterday.strftime('%m')
  182. # needed for calendar week
  183. isocal = datetime.date.fromisoformat(today.strftime('%Y-%m-%d')).isocalendar()
  184. week = isocal[1]
  185. year = isocal[0]
  186. #print(year)
  187. #print(week)
  188. # this week
  189. ## week_day1 = datetime.date.fromisocalendar(year, week, 1) # works only from python 3.8 - RPi only has 3.7.3
  190. week_d = str(year) + "-W" + str(week)
  191. #print(week_d)
  192. week_day1 = datetime.datetime.strptime(week_d + '-1', "%Y-W%W-%w") # for python 3.7
  193. week_day1_str = week_day1.strftime('%Y%m%d')
  194. #print(week_day1_str)
  195. week_day1_year_str = week_day1.strftime('%Y')
  196. week_day1_mon_str = week_day1.strftime('%m')
  197. # last week
  198. if week > 1:
  199. lastweek = week - 1
  200. lastweek_year = year
  201. else:
  202. # get highest week number of last year
  203. lastweek_year = year - 1
  204. #lastyearlastweek = datetime.date.fromisoformat(str(lastweek_year) + "-12-31").isocalendar().week #### named tuple output needs python 3.9+, on RPi only 3.7.3 is currently available
  205. lastyearlastweek = datetime.date.fromisoformat(str(lastweek_year) + "-12-31").isocalendar()[1] ## for Python 3.7.3
  206. lastweek = lastyearlastweek
  207. lastweek_d = str(lastweek_year) + "-W" + str(lastweek)
  208. #lastweek_day1 = datetime.date.fromisocalendar(lastweek_year, lastweek, 1) # works only from python 3.8 - RPi only has 3.7.3
  209. lastweek_day1 = datetime.datetime.strptime(lastweek_d + '-1', "%Y-W%W-%w") # for python 3.7
  210. lastweek_day1_str = lastweek_day1.strftime('%Y%m%d')
  211. lastweek_day1_year_str = lastweek_day1.strftime('%Y')
  212. lastweek_day1_mon_str = lastweek_day1.strftime('%m')
  213. # this month
  214. month = int(today.strftime('%m'))
  215. month_day1_str = str(year) + str(month) + "01"
  216. # last month
  217. lastmonth_year = None
  218. lastmonth = None
  219. if month > 1:
  220. lastmonth = month - 1
  221. lastmonth_year = year
  222. else:
  223. lastmonth = 12
  224. lastmonth_year = year - 1
  225. lastmonth_day1_str = str(lastmonth_year) + str(lastmonth) + "01"
  226. # billing start date
  227. if billingStartDate is not None:
  228. tmp_billdate = billingStartDate.split("-")
  229. bill_year = tmp_billdate[0]
  230. bill_month = tmp_billdate[1]
  231. bill_day = tmp_billdate[2]
  232. ##print("bill_year=" + str(bill_year))
  233. ##print("bill_month=" + str(bill_month))
  234. ##print("bill_day=" + str(bill_day))
  235. dateRollover = False
  236. savedtodaydate = saved_today_date.get(cName, False)
  237. if not savedtodaydate or savedtodaydate != today:
  238. if debug:
  239. print("date rollover happened or no date has been saved yet for meter " + str(cName))
  240. if savedtodaydate and savedtodaydate == yesterday:
  241. # date rollover just happened, so change todays date to current and proceed
  242. dateRollover = True
  243. #log.debug(savedtodaydate)
  244. saved_today_date[cName] = today
  245. strformat = "{:.3f}"
  246. cReading_formatted = None
  247. if digits is not None:
  248. strformat = "{:."+str(digits)+"f}"
  249. cReading_formatted = strformat.format(cReading)
  250. if extendJSON: cJson['reading'] = round(cReading, digits)
  251. # add unit to JSON object
  252. if unit is not None and extendJSON:
  253. cJson['unit'] = unit
  254. # calculate momentane usage
  255. # if impulses per unit are set and measured time between last impulses is known
  256. if impPerUnit and dTime is not None:
  257. if dTime == 0:
  258. momValue = 0.0
  259. else:
  260. momValue = (3600000 / dTime / impPerUnit) * momFactor
  261. # conversions of momValue
  262. momValue_conv1 = 0.0
  263. momValue_conv2 = 0.0
  264. if momType_conv1 is not None:
  265. momValue_conv1 = momValue * momFactor_conv1
  266. if momType_conv2 is not None:
  267. momValue_conv2 = momValue * momFactor_conv2
  268. # round value of momValue
  269. if momDigits > 0:
  270. momValue = round(momValue, momDigits)
  271. else:
  272. momValue = round(momValue)
  273. if momDigits_conv1 > 0:
  274. momValue_conv1 = round(momValue_conv1, momDigits_conv1)
  275. else:
  276. momValue_conv1 = round(momValue_conv1)
  277. if momDigits_conv2 > 0:
  278. momValue_conv2 = round(momValue_conv2, momDigits_conv2)
  279. else:
  280. momValue_conv2 = round(momValue_conv2)
  281. if momType is not None and extendJSON:
  282. cJson[momType] = momValue
  283. if momType_conv1 is not None and extendJSON:
  284. cJson[momType_conv1] = momValue_conv1
  285. if momType_conv2 is not None and extendJSON:
  286. cJson[momType_conv2] = momValue_conv2
  287. if statTopic and MQTTenabled:
  288. if momType is not None:
  289. mqttc.publish(statTopic + "/" + momType, str(momValue), qos=0, retain=False)
  290. if momType_conv1 is not None:
  291. mqttc.publish(statTopic + "/" + momType_conv1, str(momValue_conv1), qos=0, retain=False)
  292. if momType_conv2 is not None:
  293. mqttc.publish(statTopic + "/" + momType_conv2, str(momValue_conv2), qos=0, retain=False)
  294. # publish current reading to MQTT
  295. if statTopic:
  296. if MQTTenabled and cReading_formatted is not None:
  297. if readingType is not None:
  298. mqttc.publish(statTopic + "/" + readingType, str(cReading_formatted), qos=0, retain=False)
  299. else:
  300. mqttc.publish(statTopic + "/reading", str(cReading_formatted), qos=0, retain=False)
  301. data_energy[cNum][meters_yaml[cNum].get('influxFieldName_energy', 'energyTotal')] = round(float(cReading), digits)
  302. #if conv_unit is not None and conv_factor is not None and meters_yaml[cNum].get('influxFieldName_energy_conv', None) is not None::
  303. # data_energy[cNum][meters_yaml[cNum].get('influxFieldName_energy_conv', 'energyTotal_conv')] = round(momValue_conv1, digits)
  304. data_momentary[cNum][meters_yaml[cNum].get('influxFieldName_mom', 'momentaryUsage')] = round(float(momValue), momDigits)
  305. if momType_conv2 is not None and meters_yaml[cNum].get('influxFieldName_mom_conv1', None) is not None:
  306. data_momentary[cNum][meters_yaml[cNum].get('influxFieldName_mom_conv1', 'momentaryUsage_conv1')] = round(momValue_conv1, momDigits_conv1)
  307. if momType_conv2 is not None and meters_yaml[cNum].get('influxFieldName_mom_conv2', None) is not None:
  308. data_momentary[cNum][meters_yaml[cNum].get('influxFieldName_mom_conv2', 'momentaryUsage_conv2')] = round(momValue_conv2, momDigits_conv2)
  309. #print()
  310. #print("data_energy[cNum]")
  311. #print(data_energy[cNum])
  312. #print("data_momentary[cNum]")
  313. #print(data_momentary[cNum])
  314. #print()
  315. # check history publish interval - only publish if interval elapsed or first run after program start
  316. publishEnergyHistory = False
  317. ts = int(time.time())
  318. energyHistoryPublish_elapsedTime = 0
  319. if energy_history_lastPublished.get(cNum, None) != None:
  320. energyHistoryPublish_elapsedTime = ts - energy_history_lastPublished.get(cNum)
  321. if energyHistoryPublish_elapsedTime >= historyPublishInterval:
  322. # write interval elapsed -> write to InfluxDB
  323. publishEnergyHistory = True
  324. energy_history_lastPublished[cNum] = ts
  325. else:
  326. # first run -> write to InfluxDB immediately
  327. publishEnergyHistory = True
  328. energy_history_lastPublished[cNum] = ts
  329. # InfluxDB
  330. t_utc = datetime.datetime.utcnow()
  331. t_str = t_utc.isoformat() + 'Z'
  332. # write to InfluxDB after date rollover
  333. if dateRollover:
  334. write_energy_to_influxdb = True
  335. # check write interval - only write if interval elapsed or first run after program start
  336. ts = int(time.time())
  337. influxEnergyWrite_elapsedTime = 0
  338. if influxdb_energy_lastWriteTime.get(cNum, None) != None:
  339. influxEnergyWrite_elapsedTime = ts - influxdb_energy_lastWriteTime.get(cNum)
  340. if influxEnergyWrite_elapsedTime >= influxMinWriteInterval:
  341. # write interval elapsed -> write to InfluxDB
  342. write_energy_to_influxdb = True
  343. else:
  344. # first run -> write to InfluxDB immediately
  345. write_energy_to_influxdb = True
  346. # InfluxDB - energy readings
  347. influxInstance_energy = meters_yaml[cNum].get('influxInstance_energy', None)
  348. if influxInstance_energy is not None:
  349. if write_energy_to_influxdb:
  350. influx_measurement = meters_yaml[cNum].get('influxMeasurement_energy', None)
  351. if influx_measurement == None:
  352. influx_measurement = influxdb_yaml[influxInstance_energy].get('defaultMeasurement', 'energy')
  353. jsondata_energy = [
  354. {
  355. 'measurement': influx_measurement,
  356. 'tags': {
  357. 'meter': cName,
  358. },
  359. 'time': t_str,
  360. 'fields': data_energy[cNum]
  361. }
  362. ]
  363. if verbose:
  364. print("Writing ENERGY to InfluxDB:")
  365. print(json.dumps(jsondata_energy, indent = 4))
  366. try:
  367. influxclient[influxInstance_energy].write_points(jsondata_energy)
  368. # remember write time to maintain interval
  369. influxdb_energy_lastWriteTime[cNum] = int(time.time())
  370. except Exception as e:
  371. print('Data not written!')
  372. #log.error('Data not written!')
  373. print(e)
  374. #log.error(e)
  375. else:
  376. if verbose:
  377. print("Writing ENERGY to InfluxDB skipped (influxMinWriteInterval="+ str(influxMinWriteInterval) + ", elapsedTime=" + str(influxEnergyWrite_elapsedTime) + ")")
  378. # InfluxDB - momentary values
  379. influxInstance_mom = meters_yaml[cNum].get('influxInstance_mom', None)
  380. if influxInstance_mom is not None:
  381. influx_measurement = meters_yaml[cNum].get('influxMeasurement_mom', None)
  382. if influx_measurement == None:
  383. influx_measurement = influxdb_yaml[influxInstance_mom].get('defaultMeasurement', 'energy')
  384. jsondata_momentary = [
  385. {
  386. 'measurement': influx_measurement,
  387. 'tags': {
  388. 'meter': cName,
  389. },
  390. 'time': t_str,
  391. 'fields': data_momentary[cNum]
  392. }
  393. ]
  394. if verbose:
  395. print("Writing MOMENTARY to InfluxDB:")
  396. print(json.dumps(jsondata_momentary, indent = 4))
  397. try:
  398. influxclient[influxInstance_mom].write_points(jsondata_momentary)
  399. except Exception as e:
  400. print('Data not written!')
  401. #log.error('Data not written!')
  402. print(e)
  403. #log.error(e)
  404. # file log and calculation of today/yesterday values
  405. if config['filelog'].getboolean('enable'):
  406. # save and restore yesterday´s total energy to calculate today´s energy
  407. # check if total energy from yesterday is stored in memory, if not try to get it from saved file
  408. file_path_meter_base = conf_storage_path + cName + "/"
  409. file_path_meter_year = conf_storage_path + cName + "/" + today_year_str + "/"
  410. file_path_meter = conf_storage_path + cName + "/" + today_year_str + "/" + today_mon_str + "/"
  411. file_today_min = file_path_meter + today_str + "_min.txt"
  412. file_path_meter_year_yday = conf_storage_path + cName + "/" + yesterday_year_str + "/"
  413. file_path_meter_yday = conf_storage_path + cName + "/" + yesterday_year_str + "/" + yesterday_mon_str + "/"
  414. file_yesterday_total = file_path_meter_yday + yesterday_str + "_total.txt"
  415. file_yesterday_min = file_path_meter_yday + yesterday_str + "_min.txt"
  416. energy_today_min = saved_energy_today_min.get(cName, None)
  417. energy_today_total = 0
  418. energy_yesterday_min = 0
  419. energy_yesterday_total = None
  420. try:
  421. # handle energy_today_min
  422. if dateRollover:
  423. # after date rollover set to None in order to read from last saved file
  424. energy_today_min = None
  425. if energy_today_min == None:
  426. exists = os.path.isfile(file_today_min)
  427. if exists:
  428. # load energy_today_min from file if exists
  429. f = open(file_today_min, "r")
  430. if f.mode == 'r':
  431. contents = f.read()
  432. f.close()
  433. if contents != '':
  434. energy_today_min = float(contents)
  435. saved_energy_today_min[cName] = energy_today_min
  436. if verbose:
  437. print(cName + " - Energy Today min read from file -> = " + str(energy_today_min))
  438. else:
  439. #energy_today_min = None
  440. energy_today_min = cReading # file was empty - take current reading as value
  441. else:
  442. # save current energy_today to min-file
  443. try:
  444. if not os.path.exists(file_path_meter_base):
  445. os.mkdir(file_path_meter_base)
  446. if not os.path.exists(file_path_meter_year):
  447. os.mkdir(file_path_meter_year)
  448. if not os.path.exists(file_path_meter):
  449. os.mkdir(file_path_meter)
  450. except:
  451. e = sys.exc_info()[0]
  452. print( "Error creating directory: %s" % e )
  453. f = open(file_today_min, "w+")
  454. energy_today_min = cReading
  455. saved_energy_today_min[cName] = energy_today_min
  456. #f.write(str('{0:.3f}'.format(energy_today_min)))
  457. #f.write(str(energy_today_min))
  458. f.write(strformat.format(energy_today_min))
  459. f.close()
  460. # calculate energy_today_total
  461. if energy_today_min != None:
  462. energy_today_total = cReading - energy_today_min
  463. if verbose:
  464. print(cName + " - Energy Today total: " + str('{0:.3f}'.format(energy_today_total)))
  465. # handle energy_yesterday_total
  466. energy_yesterday_total = saved_energy_yesterday_total.get(cName, None)
  467. if dateRollover:
  468. # after date rollover set to None in order to read from last saved file
  469. energy_yesterday_total = None
  470. if energy_yesterday_total == None:
  471. exists = os.path.isfile(file_yesterday_total)
  472. if exists:
  473. # load energy_yesterday_total from file if exists
  474. f = open(file_yesterday_total, "r")
  475. if f.mode == 'r':
  476. contents = f.read()
  477. f.close()
  478. if contents != '':
  479. energy_yesterday_total = float(contents)
  480. saved_energy_yesterday_total[cName] = energy_yesterday_total
  481. if debug:
  482. print(cName + " - Energy Yesterday total read from file -> = " + str(energy_yesterday_total))
  483. else: energy_yesterday_total = None
  484. else:
  485. # no yesterday_total file exists - create one using file_yesterday_min
  486. exists = os.path.isfile(file_yesterday_min)
  487. if exists:
  488. # load yesterday_min from file
  489. f = open(file_yesterday_min, "r")
  490. if f.mode == 'r':
  491. contents =f.read()
  492. f.close()
  493. if contents != '':
  494. energy_yesterday_min = float(contents)
  495. if debug:
  496. print(cName + " - Energy yesterday min: " + str(energy_yesterday_min))
  497. else: energy_yesterday_min = None
  498. if energy_yesterday_min != None:
  499. energy_yesterday_total = round(energy_today_min - energy_yesterday_min, 3)
  500. saved_energy_yesterday_total[cName] = energy_yesterday_total
  501. try:
  502. if not os.path.exists(file_path_meter_base):
  503. os.mkdir(file_path_meter_base)
  504. if not os.path.exists(file_path_meter_year_yday):
  505. os.mkdir(file_path_meter_year_yday)
  506. if not os.path.exists(file_path_meter):
  507. os.mkdir(file_path_meter)
  508. except:
  509. e = sys.exc_info()[0]
  510. print( "Error creating directory: %s" % e )
  511. f = open(file_yesterday_total, "w+")
  512. f.write(strformat.format(energy_yesterday_total))
  513. f.close()
  514. except:
  515. e = sys.exc_info()[0]
  516. print( "Error in file log: %s" % e )
  517. # calculate and MQTT publish energy_today and conversions/cost
  518. if energy_today_total is not None and MQTTenabled:
  519. # today in base unit
  520. mqttc.publish(statTopic + "/Today__" + unit, str(round(energy_today_total, digits)), qos=0, retain=False)
  521. if extendJSON: cJson['Today__' + unit] = round(energy_today_total, digits)
  522. # conversion
  523. if conv_unit and conv_factor is not None:
  524. conv_value = energy_today_total * conv_factor
  525. mqttc.publish(statTopic + "/Today__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  526. if extendJSON: cJson['Today__' + conv_unit] = round(conv_value, conv_digits)
  527. # cost from conversion
  528. if cost_unit and cost_per_unit is not None and cost_from_conv:
  529. cost_value = round(conv_value * cost_per_unit, 2)
  530. mqttc.publish(statTopic + "/Today_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  531. if extendJSON: cJson['Today_cost__' + cost_unit] = round(cost_value, 2)
  532. # cost from base unit, not conversion
  533. if not cost_from_conv:
  534. cost_value = round(energy_today_total * cost_per_unit, 2)
  535. mqttc.publish(statTopic + "/Today_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  536. if extendJSON: cJson['Today_cost__' + cost_unit] = round(cost_value, 2)
  537. # calculate and MQTT publish energy_yesterday and conversions/cost
  538. if energy_yesterday_total is not None and MQTTenabled and publishEnergyHistory:
  539. # yesterday in base unit
  540. mqttc.publish(statTopic + "/Yesterday__" + unit, str(round(energy_yesterday_total, digits)), qos=0, retain=False)
  541. if extendJSON: cJson['Yesterday__' + unit] = round(energy_yesterday_total, digits)
  542. # conversion
  543. if conv_unit and conv_factor is not None:
  544. conv_value = energy_yesterday_total * conv_factor
  545. mqttc.publish(statTopic + "/Yesterday__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  546. if extendJSON: cJson['Yesterday__' + conv_unit] = round(conv_value, conv_digits)
  547. # cost from conversion
  548. if cost_unit and cost_per_unit is not None and cost_from_conv:
  549. cost_value = round(conv_value * cost_per_unit, 2)
  550. mqttc.publish(statTopic + "/Yesterday_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  551. if extendJSON: cJson['Yesterday_cost__' + cost_unit] = round(cost_value, 2)
  552. # cost from base unit, not conversion
  553. if not cost_from_conv:
  554. cost_value = round(energy_yesterday_total * cost_per_unit, 2)
  555. mqttc.publish(statTopic + "/Yesterday_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  556. if extendJSON: cJson['Yesterday_cost__' + cost_unit] = round(cost_value, 2)
  557. # calculate this weeks total
  558. energy_week_minfile = file_path_meter_base + week_day1_year_str + "/" + week_day1_mon_str + "/" + week_day1_str + "_min.txt"
  559. energy_week_min = None
  560. if os.path.isfile(energy_week_minfile) and MQTTenabled and publishEnergyHistory:
  561. # load energy_week_minfile from file if exists
  562. f = open(energy_week_minfile, "r")
  563. if f.mode == 'r':
  564. contents = f.read()
  565. f.close()
  566. if contents != '':
  567. energy_week_min = float(contents)
  568. energy_week_total = cReading - energy_week_min
  569. if verbose:
  570. print(cName + " - Energy Week min read from file '" + energy_week_minfile + "'")
  571. print(" -> = " + str(energy_week_min))
  572. mqttc.publish(statTopic + "/Week__" + unit, str(round(energy_week_total, digits)), qos=0, retain=False)
  573. if extendJSON: cJson['Week__' + unit] = round(energy_week_total, digits)
  574. if conv_unit and conv_factor is not None:
  575. conv_value = energy_week_total * conv_factor
  576. mqttc.publish(statTopic + "/Week__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  577. if extendJSON: cJson['Week__' + conv_unit] = round(conv_value, conv_digits)
  578. # cost from conversion
  579. if cost_unit and cost_per_unit is not None and cost_from_conv:
  580. cost_value = round(conv_value * cost_per_unit, 2)
  581. mqttc.publish(statTopic + "/Week_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  582. if extendJSON: cJson['Week_cost__' + cost_unit] = round(cost_value, 2)
  583. # cost from base unit, not conversion
  584. if not cost_from_conv:
  585. cost_value = round(energy_week_total * cost_per_unit, 2)
  586. mqttc.publish(statTopic + "/Week_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  587. if extendJSON: cJson['Week_cost__' + cost_unit] = round(cost_value, 2)
  588. # calculate last weeks total
  589. energy_lastweek_minfile = file_path_meter_base + lastweek_day1_year_str + "/" + lastweek_day1_mon_str + "/" + lastweek_day1_str + "_min.txt"
  590. if os.path.isfile(energy_lastweek_minfile) and MQTTenabled and energy_week_min is not None and publishEnergyHistory:
  591. # load energy_lastweek_minfile from file if exists
  592. f = open(energy_lastweek_minfile, "r")
  593. if f.mode == 'r':
  594. contents = f.read()
  595. f.close()
  596. if contents != '':
  597. energy_lastweek_min = float(contents)
  598. energy_lastweek_total = energy_week_min - energy_lastweek_min
  599. if verbose:
  600. print(cName + " - Energy LastWeek min read from file '" + energy_lastweek_minfile + "'")
  601. print(" -> = " + str(energy_week_min))
  602. mqttc.publish(statTopic + "/LastWeek__" + unit, str(round(energy_lastweek_total, digits)), qos=0, retain=False)
  603. if extendJSON: cJson['LastWeek__' + unit] = round(energy_lastweek_total, digits)
  604. if conv_unit and conv_factor is not None:
  605. conv_value = energy_lastweek_total * conv_factor
  606. mqttc.publish(statTopic + "/LastWeek__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  607. if extendJSON: cJson['LastWeek__' + conv_unit] = round(conv_value, conv_digits)
  608. # cost from conversion
  609. if cost_unit and cost_per_unit is not None and cost_from_conv:
  610. cost_value = round(conv_value * cost_per_unit, 2)
  611. mqttc.publish(statTopic + "/LastWeek_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  612. if extendJSON: cJson['LastWeek_cost__' + cost_unit] = round(cost_value, 2)
  613. # cost from base unit, not conversion
  614. if not cost_from_conv:
  615. cost_value = round(energy_lastweek_total * cost_per_unit, 2)
  616. mqttc.publish(statTopic + "/LastWeek_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  617. if extendJSON: cJson['LastWeek_cost__' + cost_unit] = round(cost_value, 2)
  618. # calculate this months total
  619. energy_month_minfile = file_path_meter_base + str(year) + "/" + str(month) + "/" + month_day1_str + "_min.txt"
  620. energy_month_min = None
  621. if os.path.isfile(energy_month_minfile) and MQTTenabled and publishEnergyHistory:
  622. # load energy_month_minfile from file if exists
  623. f = open(energy_month_minfile, "r")
  624. if f.mode == 'r':
  625. contents = f.read()
  626. f.close()
  627. if contents != '':
  628. energy_month_min = float(contents)
  629. energy_month_total = cReading - energy_month_min
  630. if verbose:
  631. print(cName + " - Energy Month min read from file '" + energy_month_minfile + "'")
  632. print(" -> = " + str(energy_month_min))
  633. mqttc.publish(statTopic + "/Month__" + unit, str(round(energy_month_total, digits)), qos=0, retain=False)
  634. if extendJSON: cJson['Month__' + unit] = round(energy_month_total, digits)
  635. if conv_unit and conv_factor is not None:
  636. conv_value = energy_month_total * conv_factor
  637. mqttc.publish(statTopic + "/Month__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  638. if extendJSON: cJson['Month__' + conv_unit] = round(conv_value, conv_digits)
  639. # cost from conversion
  640. if cost_unit and cost_per_unit is not None and cost_from_conv:
  641. cost_value = round(conv_value * cost_per_unit, 2)
  642. mqttc.publish(statTopic + "/Month_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  643. if extendJSON: cJson['Month_cost__' + cost_unit] = round(cost_value, 2)
  644. # cost from base unit, not conversion
  645. if not cost_from_conv:
  646. cost_value = round(energy_month_total * cost_per_unit, 2)
  647. mqttc.publish(statTopic + "/Month_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  648. if extendJSON: cJson['Month_cost__' + cost_unit] = round(cost_value, 2)
  649. # calculate last months total
  650. energy_lastmonth_minfile = file_path_meter_base + str(lastmonth_year) + "/" + str(lastmonth) + "/" + lastmonth_day1_str + "_min.txt"
  651. if os.path.isfile(energy_lastmonth_minfile) and MQTTenabled and energy_month_min is not None and publishEnergyHistory:
  652. # load energy_lastmonth_minfile from file if exists
  653. f = open(energy_lastmonth_minfile, "r")
  654. if f.mode == 'r':
  655. contents = f.read()
  656. f.close()
  657. if contents != '':
  658. energy_lastmonth_min = float(contents)
  659. energy_lastmonth_total = energy_month_min - energy_lastmonth_min
  660. if verbose:
  661. print(cName + " - Energy LastMonth min read from file '" + energy_lastmonth_minfile + "'")
  662. print(" -> = " + str(energy_lastmonth_min))
  663. mqttc.publish(statTopic + "/LastMonth__" + unit, str(round(energy_lastmonth_total, digits)), qos=0, retain=False)
  664. if extendJSON: cJson['LastMonth__' + unit] = round(energy_lastmonth_total, digits)
  665. if conv_unit and conv_factor is not None:
  666. conv_value = energy_lastmonth_total * conv_factor
  667. mqttc.publish(statTopic + "/LastMonth__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  668. if extendJSON: cJson['LastMonth__' + conv_unit] = round(conv_value, conv_digits)
  669. # cost from conversion
  670. if cost_unit and cost_per_unit is not None and cost_from_conv:
  671. cost_value = round(conv_value * cost_per_unit, 2)
  672. mqttc.publish(statTopic + "/LastMonth_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  673. if extendJSON: cJson['LastMonth_cost__' + cost_unit] = round(cost_value, 2)
  674. # cost from base unit, not conversion
  675. if not cost_from_conv:
  676. cost_value = round(energy_lastmonth_total * cost_per_unit, 2)
  677. mqttc.publish(statTopic + "/LastMonth_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  678. if extendJSON: cJson['LastMonth_cost__' + cost_unit] = round(cost_value, 2)
  679. # calculate this years total
  680. energy_year_minfile = file_path_meter_base + str(year) + "/01/" + str(year) + "0101_min.txt"
  681. energy_year_min = None
  682. if os.path.isfile(energy_year_minfile) and MQTTenabled and publishEnergyHistory:
  683. # load energy_year_minfile from file if exists
  684. f = open(energy_year_minfile, "r")
  685. if f.mode == 'r':
  686. contents = f.read()
  687. f.close()
  688. if contents != '':
  689. energy_year_min = float(contents)
  690. energy_year_total = cReading - energy_year_min
  691. if verbose:
  692. print(cName + " - Energy Year min read from file '" + energy_year_minfile + "'")
  693. print(" -> = " + str(energy_year_min))
  694. mqttc.publish(statTopic + "/Year__" + unit, str(round(energy_year_total, digits)), qos=0, retain=False)
  695. if extendJSON: cJson['Year__' + unit] = round(energy_year_total, digits)
  696. if conv_unit and conv_factor is not None:
  697. conv_value = energy_year_total * conv_factor
  698. mqttc.publish(statTopic + "/Year__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  699. if extendJSON: cJson['Year__' + conv_unit] = round(conv_value, conv_digits)
  700. # cost from conversion
  701. if cost_unit and cost_per_unit is not None and cost_from_conv:
  702. cost_value = round(conv_value * cost_per_unit, 2)
  703. mqttc.publish(statTopic + "/Year_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  704. if extendJSON: cJson['Year_cost__' + cost_unit] = round(cost_value, 2)
  705. # cost from base unit, not conversion
  706. if not cost_from_conv:
  707. cost_value = round(energy_year_total * cost_per_unit, 2)
  708. mqttc.publish(statTopic + "/Year_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  709. if extendJSON: cJson['Year_cost__' + cost_unit] = round(cost_value, 2)
  710. # calculate last years total
  711. energy_lastyear_minfile = file_path_meter_base + str(year-1) + "/01/" + str(year-1) + "0101_min.txt"
  712. if os.path.isfile(energy_lastyear_minfile) and MQTTenabled and energy_year_min is not None and publishEnergyHistory:
  713. # load energy_lastyear_minfile from file if exists
  714. f = open(energy_lastyear_minfile, "r")
  715. if f.mode == 'r':
  716. contents = f.read()
  717. f.close()
  718. if contents != '':
  719. energy_lastyear_min = float(contents)
  720. energy_lastyear_total = energy_year_min - energy_lastyear_min
  721. if verbose:
  722. print(cName + " - Energy LastYear min read from file '" + energy_lastyear_minfile + "'")
  723. print(" -> = " + str(energy_lastyear_min))
  724. mqttc.publish(statTopic + "/LastYear__" + unit, str(round(energy_lastyear_total, digits)), qos=0, retain=False)
  725. if extendJSON: cJson['LastYear__' + unit] = round(energy_lastyear_total, digits)
  726. if conv_unit and conv_factor is not None:
  727. conv_value = energy_lastyear_total * conv_factor
  728. mqttc.publish(statTopic + "/LastYear__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  729. if extendJSON: cJson['LastYear__' + conv_unit] = round(conv_value, conv_digits)
  730. # cost from conversion
  731. if cost_unit and cost_per_unit is not None and cost_from_conv:
  732. cost_value = round(conv_value * cost_per_unit, 2)
  733. mqttc.publish(statTopic + "/LastYear_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  734. if extendJSON: cJson['LastYear_cost__' + cost_unit] = round(cost_value, 2)
  735. # cost from base unit, not conversion
  736. if not cost_from_conv:
  737. cost_value = round(energy_lastyear_total * cost_per_unit, 2)
  738. mqttc.publish(statTopic + "/LastYear_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  739. if extendJSON: cJson['LastYear_cost__' + cost_unit] = round(cost_value, 2)
  740. # calculate total since last billing
  741. energy_bill_minfile = file_path_meter_base + str(bill_year) + "/" + str(bill_month) + "/" + str(bill_year) + str(bill_month) + str(bill_day) + "_min.txt"
  742. if os.path.isfile(energy_bill_minfile) and MQTTenabled and publishEnergyHistory and billingStartDate is not None:
  743. # load energy_bill_minfile from file if exists
  744. f = open(energy_bill_minfile, "r")
  745. if f.mode == 'r':
  746. contents = f.read()
  747. f.close()
  748. if contents != '':
  749. energy_bill_min = float(contents)
  750. energy_bill_total = cReading - energy_bill_min
  751. if verbose:
  752. print(cName + " - Energy Year min read from file '" + energy_bill_minfile + "'")
  753. print(" -> = " + str(energy_bill_min))
  754. mqttc.publish(statTopic + "/SinceLastBill__" + unit, str(round(energy_bill_total, digits)), qos=0, retain=False)
  755. if extendJSON: cJson['SinceLastBill__' + unit] = round(energy_bill_total, digits)
  756. if conv_unit and conv_factor is not None:
  757. conv_value = energy_bill_total * conv_factor
  758. mqttc.publish(statTopic + "/SinceLastBill__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  759. if extendJSON: cJson['SinceLastBill__' + conv_unit] = round(conv_value, conv_digits)
  760. # cost from conversion
  761. if cost_unit and cost_per_unit is not None and cost_from_conv:
  762. cost_value = round(conv_value * cost_per_unit, 2)
  763. mqttc.publish(statTopic + "/SinceLastBill_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  764. if extendJSON: cJson['SinceLastBill_cost__' + cost_unit] = round(cost_value, 2)
  765. # cost from base unit, not conversion
  766. if not cost_from_conv:
  767. cost_value = round(energy_bill_total * cost_per_unit, 2)
  768. mqttc.publish(statTopic + "/SinceLastBill_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  769. if extendJSON: cJson['SinceLastBill_cost__' + cost_unit] = round(cost_value, 2)
  770. # END file log
  771. if verbose:
  772. if consoleAddTimestamp:
  773. print ("[" + str(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]) + "] meterData:", json.dumps(cJson))
  774. else:
  775. print("meterData:", json.dumps(cJson))
  776. if MQTTenabled and publishJSON:
  777. mqttc.publish(statTopic + "/json", json.dumps(cJson), qos=0, retain=False)
  778. def publishStatMsg(data):
  779. if MQTTenabled:
  780. mqttc.publish(config['mqtt'].get('topic_stat'), data, qos=0, retain=False)
  781. def publishCmdResponseMsg(data):
  782. if MQTTenabled:
  783. mqttc.publish(config['mqtt'].get('topic_cmdresponse'), data, qos=0, retain=False)
  784. try:
  785. while True:
  786. serLine = ser.readline().strip()
  787. # catch exception on invalid char coming in: UnicodeDecodeError: 'ascii' codec can't decode byte 0xf4 in position 6: ordinal not in range(128)
  788. try:
  789. serLine = serLine.decode('ascii')
  790. except:
  791. serLine = ""
  792. if(serLine):
  793. if verbose:
  794. if consoleAddTimestamp:
  795. print ("[" + str(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]) + "] DEV_SENT: ", serLine)
  796. else:
  797. print ("DEV_SENT: ", serLine) #Echo the serial buffer bytes up to the CRLF back to screen
  798. # check if data looks like meter readings JSON - then process
  799. if serLine.startswith('{"C":'):
  800. Thread(target=processMeterData, args=(serLine,)).start()
  801. # response to a command
  802. elif serLine.startswith('cmd:'):
  803. Thread(target=publishCmdResponseMsg, args=(serLine,)).start()
  804. # other cases it is a normal "stat" message
  805. else:
  806. Thread(target=publishStatMsg, args=(serLine,)).start()
  807. except KeyboardInterrupt:
  808. print('\n')
  809. exit()