s0meters.py 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
  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. momUnit_conv1 = meters_yaml[cNum].get('momUnit_conv1', None)
  142. momType_conv1 = meters_yaml[cNum].get('momType_conv1', None)
  143. momUnit_conv2 = meters_yaml[cNum].get('momUnit_conv2', None)
  144. momType_conv2 = meters_yaml[cNum].get('momType_conv2', None)
  145. impPerUnit = meters_yaml[cNum].get('impPerUnit', None)
  146. momFactor = meters_yaml[cNum].get('momFactor', None)
  147. if momFactor is None:
  148. momFactor = 1
  149. momDigits = meters_yaml[cNum].get('momDigits', None)
  150. if momDigits is None:
  151. momDigits = 3
  152. momFactor_conv1 = meters_yaml[cNum].get('momFactor_conv1', None)
  153. if momFactor_conv1 is None:
  154. momFactor_conv1 = 1
  155. momDigits_conv1 = meters_yaml[cNum].get('momDigits_conv1', None)
  156. if momDigits_conv1 is None:
  157. momDigits_conv1 = 3
  158. momFactor_conv2 = meters_yaml[cNum].get('momFactor_conv2', None)
  159. if momFactor_conv2 is None:
  160. momFactor_conv2 = 1
  161. momDigits_conv2 = meters_yaml[cNum].get('momDigits_conv2', None)
  162. if momDigits_conv2 is None:
  163. momDigits_conv2 = 3
  164. digits = meters_yaml[cNum].get('digits', None)
  165. influxMinWriteInterval = meters_yaml[cNum].get('influxMinWriteInterval_energy', None)
  166. if influxMinWriteInterval == None: influxMinWriteInterval = 0
  167. historyPublishInterval = meters_yaml[cNum].get('historyPublishInterval', 300)
  168. billingStartDate = meters_yaml[cNum].get('billingStartDate', None)
  169. # add meter name to JSON object
  170. if cName is not None and extendJSON:
  171. cJson['name'] = cName
  172. # check for date rollover since last impulse
  173. today = datetime.date.today()
  174. today_str = today.strftime('%Y%m%d')
  175. today_year_str = today.strftime('%Y')
  176. today_mon_str = today.strftime('%m')
  177. yesterday = today - datetime.timedelta(days = 1)
  178. yesterday_str = yesterday.strftime('%Y%m%d')
  179. yesterday_year_str = yesterday.strftime('%Y')
  180. yesterday_mon_str = yesterday.strftime('%m')
  181. # needed for calendar week
  182. isocal = datetime.date.fromisoformat(today.strftime('%Y-%m-%d')).isocalendar()
  183. week = isocal[1]
  184. year = isocal[0]
  185. #print(year)
  186. #print(week)
  187. # this week
  188. ## week_day1 = datetime.date.fromisocalendar(year, week, 1) # works only from python 3.8 - RPi only has 3.7.3
  189. week_d = str(year) + "-W" + str(week)
  190. #print(week_d)
  191. week_day1 = datetime.datetime.strptime(week_d + '-1', "%Y-W%W-%w") # for python 3.7
  192. week_day1_str = week_day1.strftime('%Y%m%d')
  193. #print(week_day1_str)
  194. week_day1_year_str = week_day1.strftime('%Y')
  195. week_day1_mon_str = week_day1.strftime('%m')
  196. # last week
  197. if week > 1:
  198. lastweek = week - 1
  199. lastweek_year = year
  200. else:
  201. # get highest week number of last year
  202. lastweek_year = year - 1
  203. lastyearlastweek = datetime.date.fromisoformat(str(lastweek_year) + "-12-31").isocalendar().week
  204. lastweek = lastyearlastweek
  205. lastweek_d = str(lastweek_year) + "-W" + str(lastweek)
  206. #lastweek_day1 = datetime.date.fromisocalendar(lastweek_year, lastweek, 1) # works only from python 3.8 - RPi only has 3.7.3
  207. lastweek_day1 = datetime.datetime.strptime(lastweek_d + '-1', "%Y-W%W-%w") # for python 3.7
  208. lastweek_day1_str = lastweek_day1.strftime('%Y%m%d')
  209. lastweek_day1_year_str = lastweek_day1.strftime('%Y')
  210. lastweek_day1_mon_str = lastweek_day1.strftime('%m')
  211. # this month
  212. month = int(today.strftime('%m'))
  213. month_day1_str = str(year) + str(month) + "01"
  214. # last month
  215. lastmonth_year = None
  216. lastmonth = None
  217. if month > 1:
  218. lastmonth = month - 1
  219. lastmonth_year = year
  220. else:
  221. lastmonth = 12
  222. lastmonth_year = year - 1
  223. lastmonth_day1_str = str(lastmonth_year) + str(lastmonth) + "01"
  224. # billing start date
  225. if billingStartDate is not None:
  226. tmp_billdate = billingStartDate.split("-")
  227. bill_year = tmp_billdate[0]
  228. bill_month = tmp_billdate[1]
  229. bill_day = tmp_billdate[2]
  230. ##print("bill_year=" + str(bill_year))
  231. ##print("bill_month=" + str(bill_month))
  232. ##print("bill_day=" + str(bill_day))
  233. dateRollover = False
  234. savedtodaydate = saved_today_date.get(cName, False)
  235. if not savedtodaydate or savedtodaydate != today:
  236. if debug:
  237. print("date rollover happened or no date has been saved yet for meter " + str(cName))
  238. if savedtodaydate and savedtodaydate == yesterday:
  239. # date rollover just happened, so change todays date to current and proceed
  240. dateRollover = True
  241. #log.debug(savedtodaydate)
  242. saved_today_date[cName] = today
  243. strformat = "{:.3f}"
  244. cReading_formatted = None
  245. if digits is not None:
  246. strformat = "{:."+str(digits)+"f}"
  247. cReading_formatted = strformat.format(cReading)
  248. if extendJSON: cJson['reading'] = round(cReading, digits)
  249. # add unit to JSON object
  250. if unit is not None and extendJSON:
  251. cJson['unit'] = unit
  252. # calculate momentane usage
  253. # if impulses per unit are set and measured time between last impulses is known
  254. if impPerUnit and dTime is not None:
  255. if dTime == 0:
  256. momValue = 0.0
  257. else:
  258. momValue = (3600000 / dTime / impPerUnit) * momFactor
  259. # conversions of momValue
  260. momValue_conv1 = 0.0
  261. momValue_conv2 = 0.0
  262. if momType_conv1 is not None:
  263. momValue_conv1 = momValue * momFactor_conv1
  264. if momType_conv2 is not None:
  265. momValue_conv2 = momValue * momFactor_conv2
  266. # round value of momValue
  267. if momDigits > 0:
  268. momValue = round(momValue, momDigits)
  269. else:
  270. momValue = round(momValue)
  271. if momDigits_conv1 > 0:
  272. momValue_conv1 = round(momValue_conv1, momDigits_conv1)
  273. else:
  274. momValue_conv1 = round(momValue_conv1)
  275. if momDigits_conv2 > 0:
  276. momValue_conv2 = round(momValue_conv2, momDigits_conv2)
  277. else:
  278. momValue_conv2 = round(momValue_conv2)
  279. if momType is not None and extendJSON:
  280. cJson[momType] = momValue
  281. if momType_conv1 is not None and extendJSON:
  282. cJson[momType_conv1] = momValue_conv1
  283. if momType_conv2 is not None and extendJSON:
  284. cJson[momType_conv2] = momValue_conv2
  285. if statTopic and MQTTenabled:
  286. if momType is not None:
  287. mqttc.publish(statTopic + "/" + momType, str(momValue), qos=0, retain=False)
  288. if momType_conv1 is not None:
  289. mqttc.publish(statTopic + "/" + momType_conv1, str(momValue_conv1), qos=0, retain=False)
  290. if momType_conv2 is not None:
  291. mqttc.publish(statTopic + "/" + momType_conv2, str(momValue_conv2), qos=0, retain=False)
  292. # publish current reading to MQTT
  293. if statTopic:
  294. if MQTTenabled and cReading_formatted is not None:
  295. mqttc.publish(statTopic + "/reading", str(cReading_formatted), qos=0, retain=False)
  296. data_energy[cNum][meters_yaml[cNum].get('influxFieldName_energy', 'energyTotal')] = round(float(cReading), digits)
  297. #if conv_unit is not None and conv_factor is not None and meters_yaml[cNum].get('influxFieldName_energy_conv', None) is not None::
  298. # data_energy[cNum][meters_yaml[cNum].get('influxFieldName_energy_conv', 'energyTotal_conv')] = round(momValue_conv1, digits)
  299. data_momentary[cNum][meters_yaml[cNum].get('influxFieldName_mom', 'momentaryUsage')] = round(float(momValue), momDigits)
  300. if momType_conv2 is not None and meters_yaml[cNum].get('influxFieldName_mom_conv1', None) is not None:
  301. data_momentary[cNum][meters_yaml[cNum].get('influxFieldName_mom_conv1', 'momentaryUsage_conv1')] = round(momValue_conv1, momDigits_conv1)
  302. if momType_conv2 is not None and meters_yaml[cNum].get('influxFieldName_mom_conv2', None) is not None:
  303. data_momentary[cNum][meters_yaml[cNum].get('influxFieldName_mom_conv2', 'momentaryUsage_conv2')] = round(momValue_conv2, momDigits_conv2)
  304. #print()
  305. #print("data_energy[cNum]")
  306. #print(data_energy[cNum])
  307. #print("data_momentary[cNum]")
  308. #print(data_momentary[cNum])
  309. #print()
  310. # check history publish interval - only publish if interval elapsed or first run after program start
  311. publishEnergyHistory = False
  312. ts = int(time.time())
  313. energyHistoryPublish_elapsedTime = 0
  314. if energy_history_lastPublished.get(cNum, None) != None:
  315. energyHistoryPublish_elapsedTime = ts - energy_history_lastPublished.get(cNum)
  316. if energyHistoryPublish_elapsedTime >= historyPublishInterval:
  317. # write interval elapsed -> write to InfluxDB
  318. publishEnergyHistory = True
  319. energy_history_lastPublished[cNum] = ts
  320. else:
  321. # first run -> write to InfluxDB immediately
  322. publishEnergyHistory = True
  323. energy_history_lastPublished[cNum] = ts
  324. # InfluxDB
  325. t_utc = datetime.datetime.utcnow()
  326. t_str = t_utc.isoformat() + 'Z'
  327. # write to InfluxDB after date rollover
  328. if dateRollover:
  329. write_energy_to_influxdb = True
  330. # check write interval - only write if interval elapsed or first run after program start
  331. ts = int(time.time())
  332. influxEnergyWrite_elapsedTime = 0
  333. if influxdb_energy_lastWriteTime.get(cNum, None) != None:
  334. influxEnergyWrite_elapsedTime = ts - influxdb_energy_lastWriteTime.get(cNum)
  335. if influxEnergyWrite_elapsedTime >= influxMinWriteInterval:
  336. # write interval elapsed -> write to InfluxDB
  337. write_energy_to_influxdb = True
  338. else:
  339. # first run -> write to InfluxDB immediately
  340. write_energy_to_influxdb = True
  341. # InfluxDB - energy readings
  342. influxInstance_energy = meters_yaml[cNum].get('influxInstance_energy', None)
  343. if influxInstance_energy is not None:
  344. if write_energy_to_influxdb:
  345. influx_measurement = meters_yaml[cNum].get('influxMeasurement_energy', None)
  346. if influx_measurement == None:
  347. influx_measurement = influxdb_yaml[influxInstance_energy].get('defaultMeasurement', 'energy')
  348. jsondata_energy = [
  349. {
  350. 'measurement': influx_measurement,
  351. 'tags': {
  352. 'meter': cName,
  353. },
  354. 'time': t_str,
  355. 'fields': data_energy[cNum]
  356. }
  357. ]
  358. if verbose:
  359. print("Writing ENERGY to InfluxDB:")
  360. print(json.dumps(jsondata_energy, indent = 4))
  361. try:
  362. influxclient[influxInstance_energy].write_points(jsondata_energy)
  363. # remember write time to maintain interval
  364. influxdb_energy_lastWriteTime[cNum] = int(time.time())
  365. except Exception as e:
  366. print('Data not written!')
  367. #log.error('Data not written!')
  368. print(e)
  369. #log.error(e)
  370. else:
  371. if verbose:
  372. print("Writing ENERGY to InfluxDB skipped (influxMinWriteInterval="+ str(influxMinWriteInterval) + ", elapsedTime=" + str(influxEnergyWrite_elapsedTime) + ")")
  373. # InfluxDB - momentary values
  374. influxInstance_mom = meters_yaml[cNum].get('influxInstance_mom', None)
  375. if influxInstance_mom is not None:
  376. influx_measurement = meters_yaml[cNum].get('influxMeasurement_mom', None)
  377. if influx_measurement == None:
  378. influx_measurement = influxdb_yaml[influxInstance_mom].get('defaultMeasurement', 'energy')
  379. jsondata_momentary = [
  380. {
  381. 'measurement': influx_measurement,
  382. 'tags': {
  383. 'meter': cName,
  384. },
  385. 'time': t_str,
  386. 'fields': data_momentary[cNum]
  387. }
  388. ]
  389. if verbose:
  390. print("Writing MOMENTARY to InfluxDB:")
  391. print(json.dumps(jsondata_momentary, indent = 4))
  392. try:
  393. influxclient[influxInstance_mom].write_points(jsondata_momentary)
  394. except Exception as e:
  395. print('Data not written!')
  396. #log.error('Data not written!')
  397. print(e)
  398. #log.error(e)
  399. # file log and calculation of today/yesterday values
  400. if config['filelog'].getboolean('enable'):
  401. # save and restore yesterday´s total energy to calculate today´s energy
  402. # check if total energy from yesterday is stored in memory, if not try to get it from saved file
  403. file_path_meter_base = conf_storage_path + cName + "/"
  404. file_path_meter_year = conf_storage_path + cName + "/" + today_year_str + "/"
  405. file_path_meter = conf_storage_path + cName + "/" + today_year_str + "/" + today_mon_str + "/"
  406. file_today_min = file_path_meter + today_str + "_min.txt"
  407. file_path_meter_year_yday = conf_storage_path + cName + "/" + yesterday_year_str + "/"
  408. file_path_meter_yday = conf_storage_path + cName + "/" + yesterday_year_str + "/" + yesterday_mon_str + "/"
  409. file_yesterday_total = file_path_meter_yday + yesterday_str + "_total.txt"
  410. file_yesterday_min = file_path_meter_yday + yesterday_str + "_min.txt"
  411. energy_today_min = saved_energy_today_min.get(cName, None)
  412. energy_today_total = 0
  413. energy_yesterday_min = 0
  414. energy_yesterday_total = None
  415. try:
  416. # handle energy_today_min
  417. if dateRollover:
  418. # after date rollover set to None in order to read from last saved file
  419. energy_today_min = None
  420. if energy_today_min == None:
  421. exists = os.path.isfile(file_today_min)
  422. if exists:
  423. # load energy_today_min from file if exists
  424. f = open(file_today_min, "r")
  425. if f.mode == 'r':
  426. contents = f.read()
  427. f.close()
  428. if contents != '':
  429. energy_today_min = float(contents)
  430. saved_energy_today_min[cName] = energy_today_min
  431. if verbose:
  432. print(cName + " - Energy Today min read from file -> = " + str(energy_today_min))
  433. else:
  434. #energy_today_min = None
  435. energy_today_min = cReading # file was empty - take current reading as value
  436. else:
  437. # save current energy_today to min-file
  438. try:
  439. if not os.path.exists(file_path_meter_base):
  440. os.mkdir(file_path_meter_base)
  441. if not os.path.exists(file_path_meter_year):
  442. os.mkdir(file_path_meter_year)
  443. if not os.path.exists(file_path_meter):
  444. os.mkdir(file_path_meter)
  445. except:
  446. e = sys.exc_info()[0]
  447. print( "Error creating directory: %s" % e )
  448. f = open(file_today_min, "w+")
  449. energy_today_min = cReading
  450. saved_energy_today_min[cName] = energy_today_min
  451. #f.write(str('{0:.3f}'.format(energy_today_min)))
  452. #f.write(str(energy_today_min))
  453. f.write(strformat.format(energy_today_min))
  454. f.close()
  455. # calculate energy_today_total
  456. if energy_today_min != None:
  457. energy_today_total = cReading - energy_today_min
  458. if verbose:
  459. print(cName + " - Energy Today total: " + str('{0:.3f}'.format(energy_today_total)))
  460. # handle energy_yesterday_total
  461. energy_yesterday_total = saved_energy_yesterday_total.get(cName, None)
  462. if dateRollover:
  463. # after date rollover set to None in order to read from last saved file
  464. energy_yesterday_total = None
  465. if energy_yesterday_total == None:
  466. exists = os.path.isfile(file_yesterday_total)
  467. if exists:
  468. # load energy_yesterday_total from file if exists
  469. f = open(file_yesterday_total, "r")
  470. if f.mode == 'r':
  471. contents = f.read()
  472. f.close()
  473. if contents != '':
  474. energy_yesterday_total = float(contents)
  475. saved_energy_yesterday_total[cName] = energy_yesterday_total
  476. if debug:
  477. print(cName + " - Energy Yesterday total read from file -> = " + str(energy_yesterday_total))
  478. else: energy_yesterday_total = None
  479. else:
  480. # no yesterday_total file exists - create one using file_yesterday_min
  481. exists = os.path.isfile(file_yesterday_min)
  482. if exists:
  483. # load yesterday_min from file
  484. f = open(file_yesterday_min, "r")
  485. if f.mode == 'r':
  486. contents =f.read()
  487. f.close()
  488. if contents != '':
  489. energy_yesterday_min = float(contents)
  490. if debug:
  491. print(cName + " - Energy yesterday min: " + str(energy_yesterday_min))
  492. else: energy_yesterday_min = None
  493. if energy_yesterday_min != None:
  494. energy_yesterday_total = round(energy_today_min - energy_yesterday_min, 3)
  495. saved_energy_yesterday_total[cName] = energy_yesterday_total
  496. try:
  497. if not os.path.exists(file_path_meter_base):
  498. os.mkdir(file_path_meter_base)
  499. if not os.path.exists(file_path_meter_year_yday):
  500. os.mkdir(file_path_meter_year_yday)
  501. if not os.path.exists(file_path_meter):
  502. os.mkdir(file_path_meter)
  503. except:
  504. e = sys.exc_info()[0]
  505. print( "Error creating directory: %s" % e )
  506. f = open(file_yesterday_total, "w+")
  507. f.write(strformat.format(energy_yesterday_total))
  508. f.close()
  509. except:
  510. e = sys.exc_info()[0]
  511. print( "Error in file log: %s" % e )
  512. # calculate and MQTT publish energy_today and conversions/cost
  513. if energy_today_total is not None and MQTTenabled:
  514. # today in base unit
  515. mqttc.publish(statTopic + "/Today__" + unit, str(round(energy_today_total, digits)), qos=0, retain=False)
  516. if extendJSON: cJson['Today__' + unit] = round(energy_today_total, digits)
  517. # conversion
  518. if conv_unit and conv_factor is not None:
  519. conv_value = energy_today_total * conv_factor
  520. mqttc.publish(statTopic + "/Today__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  521. if extendJSON: cJson['Today__' + conv_unit] = round(conv_value, conv_digits)
  522. # cost from conversion
  523. if cost_unit and cost_per_unit is not None and cost_from_conv:
  524. cost_value = round(conv_value * cost_per_unit, 2)
  525. mqttc.publish(statTopic + "/Today_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  526. if extendJSON: cJson['Today_cost__' + cost_unit] = round(cost_value, 2)
  527. # cost from base unit, not conversion
  528. if not cost_from_conv:
  529. cost_value = round(energy_today_total * 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. # calculate and MQTT publish energy_yesterday and conversions/cost
  533. if energy_yesterday_total is not None and MQTTenabled and publishEnergyHistory:
  534. # yesterday in base unit
  535. mqttc.publish(statTopic + "/Yesterday__" + unit, str(round(energy_yesterday_total, digits)), qos=0, retain=False)
  536. if extendJSON: cJson['Yesterday__' + unit] = round(energy_yesterday_total, digits)
  537. # conversion
  538. if conv_unit and conv_factor is not None:
  539. conv_value = energy_yesterday_total * conv_factor
  540. mqttc.publish(statTopic + "/Yesterday__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  541. if extendJSON: cJson['Yesterday__' + conv_unit] = round(conv_value, conv_digits)
  542. # cost from conversion
  543. if cost_unit and cost_per_unit is not None and cost_from_conv:
  544. cost_value = round(conv_value * cost_per_unit, 2)
  545. mqttc.publish(statTopic + "/Yesterday_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  546. if extendJSON: cJson['Yesterday_cost__' + cost_unit] = round(cost_value, 2)
  547. # cost from base unit, not conversion
  548. if not cost_from_conv:
  549. cost_value = round(energy_yesterday_total * 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. # calculate this weeks total
  553. energy_week_minfile = file_path_meter_base + week_day1_year_str + "/" + week_day1_mon_str + "/" + week_day1_str + "_min.txt"
  554. energy_week_min = None
  555. if os.path.isfile(energy_week_minfile) and MQTTenabled and publishEnergyHistory:
  556. # load energy_week_minfile from file if exists
  557. f = open(energy_week_minfile, "r")
  558. if f.mode == 'r':
  559. contents = f.read()
  560. f.close()
  561. if contents != '':
  562. energy_week_min = float(contents)
  563. energy_week_total = cReading - energy_week_min
  564. if verbose:
  565. print(cName + " - Energy Week min read from file '" + energy_week_minfile + "'")
  566. print(" -> = " + str(energy_week_min))
  567. mqttc.publish(statTopic + "/Week__" + unit, str(round(energy_week_total, digits)), qos=0, retain=False)
  568. if extendJSON: cJson['Week__' + unit] = round(energy_week_total, digits)
  569. if conv_unit and conv_factor is not None:
  570. conv_value = energy_week_total * conv_factor
  571. mqttc.publish(statTopic + "/Week__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  572. if extendJSON: cJson['Week__' + conv_unit] = round(conv_value, conv_digits)
  573. # cost from conversion
  574. if cost_unit and cost_per_unit is not None and cost_from_conv:
  575. cost_value = round(conv_value * cost_per_unit, 2)
  576. mqttc.publish(statTopic + "/Week_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  577. if extendJSON: cJson['Week_cost__' + cost_unit] = round(cost_value, 2)
  578. # cost from base unit, not conversion
  579. if not cost_from_conv:
  580. cost_value = round(energy_week_total * 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. # calculate last weeks total
  584. energy_lastweek_minfile = file_path_meter_base + lastweek_day1_year_str + "/" + lastweek_day1_mon_str + "/" + lastweek_day1_str + "_min.txt"
  585. if os.path.isfile(energy_lastweek_minfile) and MQTTenabled and energy_week_min is not None and publishEnergyHistory:
  586. # load energy_lastweek_minfile from file if exists
  587. f = open(energy_lastweek_minfile, "r")
  588. if f.mode == 'r':
  589. contents = f.read()
  590. f.close()
  591. if contents != '':
  592. energy_lastweek_min = float(contents)
  593. energy_lastweek_total = energy_week_min - energy_lastweek_min
  594. if verbose:
  595. print(cName + " - Energy LastWeek min read from file '" + energy_lastweek_minfile + "'")
  596. print(" -> = " + str(energy_week_min))
  597. mqttc.publish(statTopic + "/LastWeek__" + unit, str(round(energy_lastweek_total, digits)), qos=0, retain=False)
  598. if extendJSON: cJson['LastWeek__' + unit] = round(energy_lastweek_total, digits)
  599. if conv_unit and conv_factor is not None:
  600. conv_value = energy_lastweek_total * conv_factor
  601. mqttc.publish(statTopic + "/LastWeek__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  602. if extendJSON: cJson['LastWeek__' + conv_unit] = round(conv_value, conv_digits)
  603. # cost from conversion
  604. if cost_unit and cost_per_unit is not None and cost_from_conv:
  605. cost_value = round(conv_value * cost_per_unit, 2)
  606. mqttc.publish(statTopic + "/LastWeek_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  607. if extendJSON: cJson['LastWeek_cost__' + cost_unit] = round(cost_value, 2)
  608. # cost from base unit, not conversion
  609. if not cost_from_conv:
  610. cost_value = round(energy_lastweek_total * 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. # calculate this months total
  614. energy_month_minfile = file_path_meter_base + str(year) + "/" + str(month) + "/" + month_day1_str + "_min.txt"
  615. energy_month_min = None
  616. if os.path.isfile(energy_month_minfile) and MQTTenabled and publishEnergyHistory:
  617. # load energy_month_minfile from file if exists
  618. f = open(energy_month_minfile, "r")
  619. if f.mode == 'r':
  620. contents = f.read()
  621. f.close()
  622. if contents != '':
  623. energy_month_min = float(contents)
  624. energy_month_total = cReading - energy_month_min
  625. if verbose:
  626. print(cName + " - Energy Month min read from file '" + energy_month_minfile + "'")
  627. print(" -> = " + str(energy_month_min))
  628. mqttc.publish(statTopic + "/Month__" + unit, str(round(energy_month_total, digits)), qos=0, retain=False)
  629. if extendJSON: cJson['Month__' + unit] = round(energy_month_total, digits)
  630. if conv_unit and conv_factor is not None:
  631. conv_value = energy_month_total * conv_factor
  632. mqttc.publish(statTopic + "/Month__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  633. if extendJSON: cJson['Month__' + conv_unit] = round(conv_value, conv_digits)
  634. # cost from conversion
  635. if cost_unit and cost_per_unit is not None and cost_from_conv:
  636. cost_value = round(conv_value * cost_per_unit, 2)
  637. mqttc.publish(statTopic + "/Month_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  638. if extendJSON: cJson['Month_cost__' + cost_unit] = round(cost_value, 2)
  639. # cost from base unit, not conversion
  640. if not cost_from_conv:
  641. cost_value = round(energy_month_total * 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. # calculate last months total
  645. energy_lastmonth_minfile = file_path_meter_base + str(lastmonth_year) + "/" + str(lastmonth) + "/" + lastmonth_day1_str + "_min.txt"
  646. if os.path.isfile(energy_lastmonth_minfile) and MQTTenabled and energy_month_min is not None and publishEnergyHistory:
  647. # load energy_lastmonth_minfile from file if exists
  648. f = open(energy_lastmonth_minfile, "r")
  649. if f.mode == 'r':
  650. contents = f.read()
  651. f.close()
  652. if contents != '':
  653. energy_lastmonth_min = float(contents)
  654. energy_lastmonth_total = energy_month_min - energy_lastmonth_min
  655. if verbose:
  656. print(cName + " - Energy LastMonth min read from file '" + energy_lastmonth_minfile + "'")
  657. print(" -> = " + str(energy_lastmonth_min))
  658. mqttc.publish(statTopic + "/LastMonth__" + unit, str(round(energy_lastmonth_total, digits)), qos=0, retain=False)
  659. if extendJSON: cJson['LastMonth__' + unit] = round(energy_lastmonth_total, digits)
  660. if conv_unit and conv_factor is not None:
  661. conv_value = energy_lastmonth_total * conv_factor
  662. mqttc.publish(statTopic + "/LastMonth__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  663. if extendJSON: cJson['LastMonth__' + conv_unit] = round(conv_value, conv_digits)
  664. # cost from conversion
  665. if cost_unit and cost_per_unit is not None and cost_from_conv:
  666. cost_value = round(conv_value * cost_per_unit, 2)
  667. mqttc.publish(statTopic + "/LastMonth_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  668. if extendJSON: cJson['LastMonth_cost__' + cost_unit] = round(cost_value, 2)
  669. # cost from base unit, not conversion
  670. if not cost_from_conv:
  671. cost_value = round(energy_lastmonth_total * 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. # calculate this years total
  675. energy_year_minfile = file_path_meter_base + str(year) + "/01/" + str(year) + "0101_min.txt"
  676. energy_year_min = None
  677. if os.path.isfile(energy_year_minfile) and MQTTenabled and publishEnergyHistory:
  678. # load energy_year_minfile from file if exists
  679. f = open(energy_year_minfile, "r")
  680. if f.mode == 'r':
  681. contents = f.read()
  682. f.close()
  683. if contents != '':
  684. energy_year_min = float(contents)
  685. energy_year_total = cReading - energy_year_min
  686. if verbose:
  687. print(cName + " - Energy Year min read from file '" + energy_year_minfile + "'")
  688. print(" -> = " + str(energy_year_min))
  689. mqttc.publish(statTopic + "/Year__" + unit, str(round(energy_year_total, digits)), qos=0, retain=False)
  690. if extendJSON: cJson['Year__' + unit] = round(energy_year_total, digits)
  691. if conv_unit and conv_factor is not None:
  692. conv_value = energy_year_total * conv_factor
  693. mqttc.publish(statTopic + "/Year__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  694. if extendJSON: cJson['Year__' + conv_unit] = round(conv_value, conv_digits)
  695. # cost from conversion
  696. if cost_unit and cost_per_unit is not None and cost_from_conv:
  697. cost_value = round(conv_value * cost_per_unit, 2)
  698. mqttc.publish(statTopic + "/Year_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  699. if extendJSON: cJson['Year_cost__' + cost_unit] = round(cost_value, 2)
  700. # cost from base unit, not conversion
  701. if not cost_from_conv:
  702. cost_value = round(energy_year_total * 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. # calculate last years total
  706. energy_lastyear_minfile = file_path_meter_base + str(year-1) + "/01/" + str(year-1) + "0101_min.txt"
  707. if os.path.isfile(energy_lastyear_minfile) and MQTTenabled and energy_year_min is not None and publishEnergyHistory:
  708. # load energy_lastyear_minfile from file if exists
  709. f = open(energy_lastyear_minfile, "r")
  710. if f.mode == 'r':
  711. contents = f.read()
  712. f.close()
  713. if contents != '':
  714. energy_lastyear_min = float(contents)
  715. energy_lastyear_total = energy_year_min - energy_lastyear_min
  716. if verbose:
  717. print(cName + " - Energy LastYear min read from file '" + energy_lastyear_minfile + "'")
  718. print(" -> = " + str(energy_lastyear_min))
  719. mqttc.publish(statTopic + "/LastYear__" + unit, str(round(energy_lastyear_total, digits)), qos=0, retain=False)
  720. if extendJSON: cJson['LastYear__' + unit] = round(energy_lastyear_total, digits)
  721. if conv_unit and conv_factor is not None:
  722. conv_value = energy_lastyear_total * conv_factor
  723. mqttc.publish(statTopic + "/LastYear__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  724. if extendJSON: cJson['LastYear__' + conv_unit] = round(conv_value, conv_digits)
  725. # cost from conversion
  726. if cost_unit and cost_per_unit is not None and cost_from_conv:
  727. cost_value = round(conv_value * cost_per_unit, 2)
  728. mqttc.publish(statTopic + "/LastYear_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  729. if extendJSON: cJson['LastYear_cost__' + cost_unit] = round(cost_value, 2)
  730. # cost from base unit, not conversion
  731. if not cost_from_conv:
  732. cost_value = round(energy_lastyear_total * 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. # calculate total since last billing
  736. energy_bill_minfile = file_path_meter_base + str(bill_year) + "/" + str(bill_month) + "/" + str(bill_year) + str(bill_month) + str(bill_day) + "_min.txt"
  737. if os.path.isfile(energy_bill_minfile) and MQTTenabled and publishEnergyHistory and billingStartDate is not None:
  738. # load energy_bill_minfile from file if exists
  739. f = open(energy_bill_minfile, "r")
  740. if f.mode == 'r':
  741. contents = f.read()
  742. f.close()
  743. if contents != '':
  744. energy_bill_min = float(contents)
  745. energy_bill_total = cReading - energy_bill_min
  746. if verbose:
  747. print(cName + " - Energy Year min read from file '" + energy_bill_minfile + "'")
  748. print(" -> = " + str(energy_bill_min))
  749. mqttc.publish(statTopic + "/SinceLastBill__" + unit, str(round(energy_bill_total, digits)), qos=0, retain=False)
  750. if extendJSON: cJson['SinceLastBill__' + unit] = round(energy_bill_total, digits)
  751. if conv_unit and conv_factor is not None:
  752. conv_value = energy_bill_total * conv_factor
  753. mqttc.publish(statTopic + "/SinceLastBill__" + conv_unit, str(round(conv_value, conv_digits)), qos=0, retain=False)
  754. if extendJSON: cJson['SinceLastBill__' + conv_unit] = round(conv_value, conv_digits)
  755. # cost from conversion
  756. if cost_unit and cost_per_unit is not None and cost_from_conv:
  757. cost_value = round(conv_value * cost_per_unit, 2)
  758. mqttc.publish(statTopic + "/SinceLastBill_cost__" + cost_unit, str(cost_value), qos=0, retain=False)
  759. if extendJSON: cJson['SinceLastBill_cost__' + cost_unit] = round(cost_value, 2)
  760. # cost from base unit, not conversion
  761. if not cost_from_conv:
  762. cost_value = round(energy_bill_total * 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. # END file log
  766. if verbose:
  767. if consoleAddTimestamp:
  768. print ("[" + str(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]) + "] meterData:", json.dumps(cJson))
  769. else:
  770. print("meterData:", json.dumps(cJson))
  771. if MQTTenabled and publishJSON:
  772. mqttc.publish(statTopic + "/json", json.dumps(cJson), qos=0, retain=False)
  773. def publishStatMsg(data):
  774. if MQTTenabled:
  775. mqttc.publish(config['mqtt'].get('topic_stat'), data, qos=0, retain=False)
  776. def publishCmdResponseMsg(data):
  777. if MQTTenabled:
  778. mqttc.publish(config['mqtt'].get('topic_cmdresponse'), data, qos=0, retain=False)
  779. try:
  780. while True:
  781. serLine = ser.readline().strip()
  782. # catch exception on invalid char coming in: UnicodeDecodeError: 'ascii' codec can't decode byte 0xf4 in position 6: ordinal not in range(128)
  783. try:
  784. serLine = serLine.decode('ascii')
  785. except:
  786. serLine = ""
  787. if(serLine):
  788. if verbose:
  789. if consoleAddTimestamp:
  790. print ("[" + str(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]) + "] DEV_SENT: ", serLine)
  791. else:
  792. print ("DEV_SENT: ", serLine) #Echo the serial buffer bytes up to the CRLF back to screen
  793. # check if data looks like meter readings JSON - then process
  794. if serLine.startswith('{"C":'):
  795. Thread(target=processMeterData, args=(serLine,)).start()
  796. # response to a command
  797. elif serLine.startswith('cmd:'):
  798. Thread(target=publishCmdResponseMsg, args=(serLine,)).start()
  799. # other cases it is a normal "stat" message
  800. else:
  801. Thread(target=publishStatMsg, args=(serLine,)).start()
  802. except KeyboardInterrupt:
  803. print('\n')
  804. exit()