s0meters.py 49 KB

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