import introspect import json import math import string import mqtt import path # command if running on dev machine # uncomment NOT running on tasmota device # import tasmota # import os # var path # path=os.path # UCI Utility Modules # in development set sys.path to find these modules import stringe import time # manipulates and loads/saves object to file import object # home assistant integration import ha def getfilename (file,opts) if isinstance(file, map) opts = file file = nil end opts = opts ? opts : {} var name = opts.find('name') var Device = tasmota.cmd('status')['Status']['DeviceName'] var device = string.tolower(string.split(Device,' ').concat('_')) file = file ? file : (name ? name : device) file = stringe.nospaces(stringe.tolower(file)) file = string.find(file,'.') < 0 ? file+'.cfg' : file return file end var configure = def (file,opts) var name if isinstance(file, map) opts = file file = nil end file = getfilename(file,opts) opts = opts ? opts : {} name = opts.find('name') if !name name = tasmota.cmd('status')['Status']['DeviceName'] end if path.exists(file) print('existing configuration file', file,' use `force`:true in the options to overwrite') if ! opts.find('force') return end end opts.remove('force') print('file name opts') print(file,name,opts) var p = object(opts,file) p.name = name p.id = string.tolower(string.split(p.name,' ').concat('_')) p.speed = number(p.speed ? p.speed : 1) # counter for annemometer p.speedunit = p.speedunit ? p.speedunit : "MPH" p.speeddebounce = p.speeddebounce ? p.speeddebounce : 10 # milliseconds p.speedcal = p.speedcal ? p.speedcal : p.speedunit == "MPH" ? 1.492 : 2.4 p.speeddur= number(p.speeddur ? p.speeddur : 5 ) p.speedtopic = p.speedtopic ? p.speedtopic : 'weather/' + p.id +'/wind_speed' p.direction = number(p.direction ? p.direction : 1) # default ADC A1 p.direction_file = p.direction_file ? p.direction_file : p.id+'-direction.json' p.direction_error = number(p.direction_error ? p.direction_error : 25) p.directiontopic = p.directiontopic ? p.directiontopic : 'weather/'+ p.id + '/wind_direction' p.rain = number(p.rain ? p.rain : 2) # assume adc1 p.raindebounce = p.raindebounce ? p.raindebounce : 10 # milliseconds p.rainunit = p.rainunit ? p.rainunit : "in" p.raincal = p.raincal ? p.raincal : p.rainunit == "in" ? 0.011 : 0.274 p.raintopic = p.raintopic ? p.raintopic : 'weather/'+ p.id + '/rain' p.tempunit = p.tempunit ? p.tempunit : "F" print('WEATHER STATION CONFIGURATION') print(p.get()) p.save() print("saved configuration to: ",p.file.name()) return p end # configuration settings as initially set by separate configure function # name # name for weather station instance # id # derived from name # sensor # temperature sensor name as used in telemetry # speed # tasmota number of annemometer switch (1 by default) # direction # tasmota configured number for ADC # temp/humid/press sensor name class Weather # private states var _temp # last temperature from the sensor var _time # time of last temperature reading var _acount # cumulative count for 1 minute average var _rain # cumulative rain for 24 hours as array with 0 current day var _atime # cumulative time for next 1 minute average var scounter # string for counter with number for commands var _wdtbl # wind lookup table var _mqtt # state of connection to broker var cfg # weather station configuration var data # persist live data to file static appname = 'weather' def init (file,opts) # TODO if opts has update:true, then other opts will update/add to existing configuration ## CONFIGURE ############################# if isinstance(file, map) opts = file file = nil end file = getfilename(file,opts) if path.exists(file) self.cfg = object(file) if self.cfg == nil print ("ERROR: loading configuration file", file) return else print('loaded configuration file',file,'for',self.cfg.name) print('configuration', self.cfg.get()) end else print('WARNING: no configuration file exists, attemping to make make one') self.cfg = configure(file,opts) if !self.cfg print('unable to make configurate file, aborting') return else print('created config file',file,' continuting to initialize') end end # CONFIGURE ############################### # wait for mqtt broker connect # TODO make this part of an mqtt module using mqtt functions in this module self.mqtt_wait(/ -> self.ha_create_entities()) # DIRECTION: ################ if path.exists(self.cfg.direction_file) var f try f = open(self.cfg.direction_file, "r") self._wdtbl = json.load(f.read()) f.close() except .. as e, m if f != nil f.close() end raise e, m end print('wind-direction table loaded, listening for new direction') tasmota.add_rule("ANALOG#A"+str(self.cfg.direction) , / value -> self.process_direction(value)) if debug[3] print(self._wdtbl) end else print('FATAL: missing wind direction',self.cfg.direction_file, ' direction detection disabled') end # END DIRECTION ################ ############ Temperature: ################ tasmota.cmd("SetOption8 "+str(self.cfg.tempunit == "F" ? "1" : "0")) var status = tasmota.cmd("status 10")['StatusSNS'] if debug[3] print(status) end if self.cfg.tempunit != status['TempUnit'] print ('FATAL ERROR: unable to set system temperature unit to ',self.cfg.tempunit) end # TODO need config and rules for temperature/humid/press/sensor if enabled # END TEMPERATURE # RAIN: ########################## ######## for using switch intead of counter ########## # tasmota.cmd("SetOption114 1") # tasmota.cmd("SwitchMode"+str(self.cfg.rain)+" 13") # tasmota.add_rule("Switch"+str(self.cfg.rain)+"#ACTION", / value -> self.process_rain(value)) ####################################################### tasmota.cmd('Counter'+str(self.cfg.rain)+' 0') # reset counter tasmota.cmd('CounterDebounce'+str(self.cfg.rain)+' '+str(self.cfg.raindebounce)) tasmota.add_rule("Counter#C"+str(self.cfg.rain), / value -> self.rain_event(value)) # load or set up rain datafile # TODO combined data file with rain data as a key, requires object module refactor var datafile = self.cfg.id+'_rain.dat' self._rain = object(datafile,{'autoload':false}) if path.exists(datafile) self._rain.load() else # TODO make current key with data underneath for current unsaved hour self._rain.unit = self.cfg.rainunit self._rain.today = 0 self._rain.current_hour = 0 self._rain.daily = {} self._rain.last_rain_time = '' self._rain.last_rain_etime = 0 self._rain.hourly = [] self._rain.hourly.resize(24) self._rain.save() end self._rain._autosave = true self._rain.status = 'init' if debug[1] print('initial rain data', self._rain.get()) end # TODO - do smarter recovery of current hour, and today totals. self._rain.current_hour = 0 self._rain.today = 0 tasmota.add_cron("0 0 * * * *", /-> self.rain_hour(), "rain-hour") ######### END RAIN ############ ######## SPEED: annemometer ##################### self._acount = 0 self._atime = 0 tasmota.cmd('CounterDebounce'+str(self.cfg.speed)+' '+str(self.cfg.speeddebounce)) tasmota.cmd('CounterDebounceLow'+str(self.cfg.speed)+' 0' ) tasmota.cmd('CounterDebounceHigh'+str(self.cfg.speed)+' 0' ) if debug[0] print('speed debounce:',tasmota.cmd('CounterDebounce'+str(self.cfg.speed))) end self.scounter = 'Counter'+str(self.cfg.speed) self.start_speed_timer() # END SPEED end # end init ################ WIND SPEED PROCESSING #################### # TODO allow settable timeout for speed calculation (default 5) def start_speed_timer() tasmota.cmd(self.scounter + ' 0') # reset counter tasmota.set_timer(5000,/-> self.process_speed()) end def process_speed () # TODO save all time max, save daily max, daily average, use cron def speed_calc(count,time) if !time time = 5 end # TODO will use cfg.speedtime return string.format('%.1f',count * self.cfg.speedcal / time) end var count=tasmota.cmd(self.scounter)[self.scounter] self.start_speed_timer() if self._atime >= 60 var aspeed = speed_calc(self._acount,60) if debug[2] print ('AVERAGE SPEED: count',self._acount,'speed', aspeed) end if self._mqtt mqtt.publish('weather/'+self.cfg.id+'/avespeed', aspeed) else print('WARNING: unable to publish speed, not connected to broker') end self._acount = 0 self._atime = 0 else self._acount += count self._atime += 5 end # average time var speed = speed_calc(count) # default is 5 if debug[0] && count > 0 print ('CURRENT SPEED: count', count, 'speed', speed, 'count for average', self._acount) end if self._mqtt mqtt.publish(self.cfg.speedtopic, speed) else print('WARNING: unable to publish speed, not connected to broker') end end #*********************************************************** def wdlookup(val) var raw if debug[2] print('looking up',val,'in table with error margin +-',self.cfg.direction_error) end for e:self._wdtbl raw = e.item('RAW') if debug[0] print(val,raw,math.abs(raw - val), self.cfg.direction_error) end # if math.abs(raw - val) < raw * number(self.cfg.direction_error)/100 if math.abs(raw - val) < self.cfg.direction_error if debug[2] print('matched a direction=>',e) end return e end end if debug[0] print('not able to match a direction for analog value', val) end return nil end def process_direction(val) var res res = self.wdlookup(val) if res if debug[0] print('Direction:',res.item('Direction')) end mqtt.publish(self.cfg.directiontopic, json.dump(res)) end end def rain_hour() var hour = number(time.prop('hour')) if debug[3] hour = 0 end if hour == 0 # new day if debug [3] print(time.today('ymd'),': new day saving last days rain day ##########') end self._rain.daily.setitem(time.yesterday('ymd'),{ 'total': self._rain.today, 'hourly': self._rain.hourly }) self._rain.today = 0 self._rain.hourly.clear() self._rain.hourly.resize(24) # inflate hourly back to 24 else if debug[3] print('saving last hours rain amount', hour, self._rain.current_hour) end self._rain.hourly[hour - 1]=self._rain.current_hour end self._rain.current_hour = 0 mqtt.publish(self.cfg.raintopic, self._rain.dump()) end def rain_event (value) if value > 0 && self._rain.status == 'active' var hour = number(time.prop('hour')) self._rain.today += self.cfg.raincal self._rain.current_hour += self.cfg.raincal self._rain.hourly[hour]=self._rain.current_hour self._rain.last_rain_time = time.now() self._rain.last_rain_etime = time.now('e') if debug[3] print(time.tod(),'rain event on:',time.day('dmy'),'todays, last hour',self._rain.today,self._rain.current_hour) print(self._rain.hourly) end mqtt.publish(self.cfg.raintopic, self._rain.dump()) end self._rain.status = 'active' # after first call rain gauge is active tasmota.cmd('Counter'+str(self.cfg.rain)+' 0') # reset counter end def config (ret) if ret == 'print' print('*****************************') print('----------Settings ----------') print('weather station name: '+ str(self.cfg.name)) print('weather id: '+ str(self.cfg.id)) print('annemometer switch number: '+ str(self.cfg.speed)) print('wind direction ADC channel number: '+ str(self.cfg.direction)) print('wind acceptable error from table value: '+ str(self.cfg.direction_error)) print('rain gauge switch number number: '+ str(self.cfg.rain)) print('******************************') return end if ! ret return self.cfg.get() end if ret == 'json' return json.dump(self.cfg.get()) end end # TODO create a data state for publishing # def state(format) # end # publish current state to mqtt # def publish(format) # end def ha_create_entities() print('creating HA entities') ha.entity_create('Wind Direction', { 'jsonkey':'Direction', 'icon':'mdi:compass-rose', 'topic': self.cfg.directiontopic }) ha.entity_create('Wind Speed', { 'icon':'mdi:weather-windy', 'topic': self.cfg.speedtopic, 'unit' : self.cfg.speedunit }) ha.entity_create('Todays Rain', { 'icon':'mdi:weather-pouring', 'jsonkey':'today', 'topic': self.cfg.raintopic, 'unit' : self.cfg.rainunit }) # TODO create entities for other rain data end # TODO pass a handler function to call def mqtt_wait(con,dis) if dis dis() end self._mqtt = false tasmota.remove_rule("mqtt#disconnected",2) tasmota.add_rule("mqtt#connected", /-> self.mqtt_connected(con),2) end # TODO pass a handler function to call def mqtt_connected(con) self._mqtt=true if con con() end tasmota.remove_rule("mqtt#connected",2) tasmota.add_rule("mqtt#disconnected", /-> self.mqtt_wait(),2) end def mqtt_connected_cmds() print("subscribing to weather station commands via mqtt") tasmota.cmd("Subscribe "+self.cfg.id,"/weather/set/"+self.cfg.id) tasmota.add_rule("Event#weather"+self.cfg.id, /-> self.mqtt_process_cmd()) end def mqtt_disconnected_cmds() tasmota.remove_rule("Event#weather"+self.cfg.id, 2) end def mqtt_process_cmd(payload) print("incoming command payload (as json) from mqtt") print(payload) var _p = json.load(payload) _p = _p ? _p : {} if _p.contains('set') && _p.contains('value') var prop = _p.item('set') introspect.set(self,prop,_p.item('value')) print('new value for ',prop,' is ',introspect.get(self,prop)) end if _p.contains('method') var f = introspect.get(self,_p.item('method')) call(f,self,_p.item('args')) end end end # end of Weather Class var weather = module('weather') weather.create = Weather weather.configure = configure return weather