From 7edb3771717d15f7c36d8459fa12b3d6f76d7d9a Mon Sep 17 00:00:00 2001 From: Roman Bazalevsky Date: Tue, 6 Nov 2018 17:07:58 +0300 Subject: [PATCH] =?utf8?q?=D0=91=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD?= =?utf8?q?=D0=B5=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=B8=D0=B2?= =?utf8?q?=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=B2=D0=B0=D1=80=D0=B8=D0=B0?= =?utf8?q?=D0=BD=D1=82=20=D0=BD=D0=B0=20python.=20=D0=94=D0=BE=D0=B1=D0=B0?= =?utf8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20"=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D1=8B?= =?utf8?q?=D0=B9"=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=20=D0=B4=D0=BB?= =?utf8?q?=D1=8F=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?utf8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20hwmon=20=D0=B8=20iio-=D0=B8=D0=BD?= =?utf8?q?=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D0=BE=D0=B2=20=D0=B4?= =?utf8?q?=D0=B0=D1=82=D1=87=D0=B8=D0=BA=D0=BE=D0=B2.?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- weathermon | 446 ----------------------------------------------- weathermon-iio | 237 +++++++++++++++++++++++++ weathermon-light | 209 ---------------------- weathermon.lua | 14 +- weathermon.uci | 66 ++++++- 5 files changed, 303 insertions(+), 669 deletions(-) delete mode 100755 weathermon create mode 100755 weathermon-iio delete mode 100755 weathermon-light diff --git a/weathermon b/weathermon deleted file mode 100755 index 19688cb..0000000 --- a/weathermon +++ /dev/null @@ -1,446 +0,0 @@ -#!/usr/bin/python - -import serial - -from os import listdir,system -from os.path import isfile, join - -from pprint import pprint - -from termios import tcflush, TCIOFLUSH - -from time import sleep,time - -from uuid import getnode - -from hashlib import md5 - -import socket - -import sys,traceback - -import pycurl -from urllib import urlencode -from StringIO import StringIO - -searchpath = '/dev/serial/by-id/' -path = None -baud = None -timeout = None - -proc = None - -external_submit_interval = 320 -owm_submit_interval = 320 -expire_interval = 1200 -submit_time = time() -submit_time_owm = time() - -import MySQLdb -import ConfigParser - -import thread -from threading import Timer - -def find_port(): - - global serial_num - global path - - files = listdir(searchpath) - for f in files: - if serialnum in f: - return join(searchpath,f) - return None - -def open_port(path): - - global proc, debug - - if debug>0: - print "Opening path "+path - - if path == "-": - return sys.stdin - - if path[0] == "=": - import subprocess - sleep(3) - command=path[1:] - try: - command,args=command.split(' ',1) - proc = subprocess.Popen([command,args],stdout=subprocess.PIPE) - except: - proc = subprocess.Popen([command],stdout=subprocess.PIPE) - import subprocess - return proc.stdout - - ser = serial.Serial(path,baud,timeout=timeout) - if ser.portstr: - tcflush(ser,TCIOFLUSH); - return ser - -def read_port(ser): - - try: - timeout_timer = Timer(timeout, thread.interrupt_main) - timeout_timer.start() - - line='' - while line=='': - line = ser.readline() - line = line.strip() - - return line - - except KeyboardInterrupt: - return "<>" - finally: - timeout_timer.cancel() - -def read_loop(ser,callback): - - global proc - - while True: - - try: - line=read_port(ser) - if line=="<>": - if debug>0: - print "Reopening port..." - print line - ser.close() - if proc: - try: - if debug>0: - print "Terminating process..." - proc.terminate() - sleep(5) - finally: - None - ser=open_port(path) - if line: - callback(line) - except KeyboardInterrupt: - break - finally: - None - -def print_log(str): - global logging - if debug>0: - print str - if logging == "on": - system("logger -t weathermon \""+str+"\"") - -def submit_narodmon(): - - param = { 'ID':devid } - - c = database.cursor() - c.execute( - ''' - select nm_id,value from - ( - SELECT sensor_id,parameter_id,max(timestamp) timestamp,round(avg(value),1) value FROM meteo.sensor_values - where - timestamp>=date_add(now(), INTERVAL -300 SECOND) - group by sensor_id,parameter_id - ) v,meteo.ext_sensors e - where v.sensor_id=e.sensor_id and v.parameter_id=e.param_id - and nm_id is not null - ''' - ) - - queue=c.fetchall() - - if debug>1: - pprint(queue) - - for (sensor,value) in queue: - param[sensor] = value - - if debug>1: - pprint (param) - - url = "http://narodmon.ru/post.php" - - try: - - response_buffer = StringIO() - curl = pycurl.Curl() - - curl.setopt(curl.URL, url) - curl.setopt(curl.WRITEFUNCTION, response_buffer.write) - curl.setopt(curl.POST, 1) - curl.setopt(curl.POSTFIELDS, urlencode(param)) - - curl.perform() - curl.close() - - response_value = response_buffer.getvalue() - - print_log('Narodmon response: '+response_value) - - return True - - except: - - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_tb(exc_traceback, limit=1, file=sys.stdout) - traceback.print_exception(exc_type, exc_value, exc_traceback, - limit=2, file=sys.stdout) - return False - -def submit_owm(): - - url = "http://openweathermap.org/data/post" - params = {'name':owm_station, 'lat':owm_lat, 'long':owm_lon} - - c = database.cursor() - c.execute( - ''' - select owm_id,value from - ( - SELECT sensor_id,parameter_id,max(timestamp) timestamp,round(avg(value),1) value FROM meteo.sensor_values - where - timestamp>=date_add(now(), INTERVAL -300 SECOND) - group by sensor_id,parameter_id - ) v,meteo.ext_sensors e - where v.sensor_id=e.sensor_id and v.parameter_id=e.param_id - and owm_id is not null - ''' - ) - - queue=c.fetchall() - - if debug>1: - pprint(queue) - - for (sensor,value) in queue: - params[sensor]=value - if debug>1: - pprint (params) - - try: - - response_buffer = StringIO() - curl = pycurl.Curl() - - curl.setopt(curl.URL, url) - curl.setopt(curl.USERPWD, '%s:%s' % (owmuser, owmpasswd)) - curl.setopt(curl.WRITEFUNCTION, response_buffer.write) - curl.setopt(curl.POST, 1) - curl.setopt(curl.POSTFIELDS, urlencode(params)) - - curl.perform() - curl.close() - - response_value = response_buffer.getvalue() - - print_log('Openweathermap response: '+response_value) - - return True - - except: - - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_tb(exc_traceback, limit=1, file=sys.stdout) - traceback.print_exception(exc_type, exc_value, exc_traceback, - limit=2, file=sys.stdout) - return False - -def submit_data(sensor_type,sensor_id,sensor_param,param_value): - global submit_time - global submit_time_owm - global external_submit_interval - global owm_submit_interval - c = database.cursor() - c.execute('CALL meteo.submit_value(%s,%s,%s,%s,NULL)', (sensor_type,sensor_id,sensor_param,param_value)) - database.commit() - if narmon=='on' and time()>submit_time+external_submit_interval: - submit_narodmon() - submit_time=time() - if owmuser and time()>submit_time_owm+owm_submit_interval: - submit_owm() - submit_time_owm=time() - -def process_str(str): - print_log("Received: "+str) - try: - msg_type, msg_body = str.split(':') - except: - return - try: - if msg_type == 'STATUS': - print_log('Status: '+msg_body) - elif msg_type == 'ERROR': - print_log('Error: '+ msg_body) - elif msg_type == 'SENSOR': - sens = msg_body.split(',') - sensor = {} - sensor_type = None - sensor_id = None - for rec in sens: - key,value = rec.split('=') - value=value.strip() - if len(value)>0: - if key == 'TYPE': - sensor_type = value - elif key == 'ID': - sensor_id = value - else: - sensor[key] = value - if sensor_type: - if not sensor_id: - sensor_id=devid; - for key in sensor: - if sensor[key]: - print_log('Type = '+sensor_type+', ID = '+sensor_id+', Param = '+key+', Value = '+sensor[key]) - submit_data(sensor_type,sensor_id,key,sensor[key]) - else: - print_log('Error: got empty parameter value for '+sensor_type+'.'+sensor_id+'.'+key) - elif msg_type == "ALARM": - alarm = msg_body.split(',') - device_type = None - device_id = None - for rec in alarm: - key,value = rec.split('=') - value=value.strip() - if len(value)>0: - if key == 'TYPE': - device_type = value - if key == 'ID': - device_id = value - if device_type: - if not device_id: - device_id=devid; - print_log("Alarm: Type = "+device_type+", ID = "+device_id) - if alarm_script: - try: - proc = subprocess.Popen([alarm_script,device_type,device_id,msg_body]) - except: - print_log("Failed to execute alarm script") - except: - print_log('Exception processing...') - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_tb(exc_traceback, limit=1, file=sys.stdout) - traceback.print_exception(exc_type, exc_value, exc_traceback, - limit=5, file=sys.stdout) - try: - database.close() - except: - None - reconnect() - -def weather_mon(): - - global path - - if path is None: - path = find_port() - ser = open_port(path) - read_loop(ser,process_str) - -def reconnect(): - - connected = False - - while not connected: - - try: - - global database - database = MySQLdb.connect(host=dbhost,user=dbuser,passwd=dbpasswd,use_unicode=True,connect_timeout=10) - database.set_character_set('utf8') - c = database.cursor() - c.execute('SET NAMES utf8;') - print_log("Database connected...") - connected = True - - except: - - print_log("Error connecting database") - sleep(30) - -def main(): - weather_mon() - -def init(): - - global dbhost,dbuser,dbpasswd,path,serialnum,logging,debug; - global timeout,baud,narmon,devid; - global owmuser,owmpasswd,owm_temp,owm_pres,owm_humi,owm_lat,owm_lon,owm_station; - global alarm_script; - - try: - - cfg = ConfigParser.RawConfigParser(allow_no_value=True) - cfg.readfp(open('/etc/weathermon.conf')) - dbhost = cfg.get("mysql","host") - dbuser = cfg.get("mysql","user") - dbpasswd = cfg.get("mysql","passwd") - try: - debug = cfg.get("logging","debug") - except: - debug = 0 - try: - path = cfg.get("serial","port"); - except: - path = None - try: - serialnum = cfg.get("serial","id") - except: - serialnum = None - try: - logging = cfg.get("logging","enabled") - except: - logging = None - try: - timeout = int(cfg.get("serial","timeout")) - except: - timeout = 120 - try: - baud = int(cfg.get("serial","baud")) - except: - baud = 57600 - try: - narmon = cfg.get("narodmon","enable") - except: - narmon = 'off' - try: - devid = cfg.get("narodmon","devid") - except: - devid = "{:0>12X}".format(getnode()) - try: - owmuser = cfg.get("openweathermap","user") - owmpasswd = cfg.get("openweathermap",'passwd') - except: - owmuser = None - if owmuser: - owm_temp = cfg.get("openweathermap",'temp') - owm_pres = cfg.get("openweathermap",'pres') - owm_humi = cfg.get("openweathermap",'humi') - owm_lat = cfg.get("openweathermap",'lat') - owm_lon = cfg.get("openweathermap",'lon') - owm_station = cfg.get("openweathermap",'station') - try: - alarm_script = cfg.get("alarm","exec") - import subprocess - except: - alarm_script = None - - reconnect() - - except: - - print_log("Cannot intialize system") - exit() - -if __name__ == "__main__": - import sys - reload(sys) - sys.setdefaultencoding('utf-8') - init() - main() diff --git a/weathermon-iio b/weathermon-iio new file mode 100755 index 0000000..93b9cb5 --- /dev/null +++ b/weathermon-iio @@ -0,0 +1,237 @@ +#!/usr/bin/lua + +require "uci" +cur = uci.cursor() + +lfs = require "lfs" +json = require "json" +socket = require "socket" + +function split(str, pat) + local t = {} -- NOTE: use {n = 0} in Lua-5.0 + local fpat = "(.-)" .. pat + local last_end = 1 + local s, e, cap = str:find(fpat, 1) + while s do + if s ~= 1 or cap ~= "" then + table.insert(t,cap) + end + last_end = e+1 + s, e, cap = str:find(fpat, last_end) + end + if last_end <= #str then + cap = str:sub(last_end) + table.insert(t, cap) + end + return t +end + +function trim(s) + return (s:gsub("^%s*(.-)%s*$", "%1")) +end + +function get_device_list(config_name) + + local devices + devices = {} + + cur.foreach(config_name, "device", function(s) + devices[#devices+1] = s[".name"] + end) + + return devices + +end + +function get_device(config_name,device_name) + + local device + device = {} + + cur.foreach(config_name, "device", function(s) + if s[".name"] == device_name then + device = s + end + end) + + return device + +end + +function get_file_content(name) + local f = io.open(name,"r") + if f ~= nil then + local content = trim(f:read("*all")) + io.close(f) + return content + else + return false + end +end + +function find_device(name,subsystem) + + local search_base + + if subsystem == "iio" then + search_base = "/sys/bus/iio/devices" + for file in lfs.dir(search_base) do + if get_file_content(search_base.."/"..file.."/name") == name then + return search_base.."/"..file + end + end + elseif subsystem == "hwmon" then + search_base = "/sys/class/hwmon" + for file in lfs.dir(search_base) do + if get_file_content(search_base.."/"..file.."/device/name") == name then + return search_base.."/"..file.."/device" + end + end + end + + return nil + +end + +function init_device(device,parameters,i2c_bus) + + if not i2c_bus then + i2c_bus = 0 + end + + if device["module"] then + os.execute("modprobe "..device["module"]) + end + + if device["type"] then + + devtype = split(device["type"],":") + bus = devtype[1] + subsystem = devtype[2] + + if (bus == "i2c") and device["address"] and device["name"] then + pcall(function () + local f = io.open("/sys/class/i2c-dev/i2c-"..i2c_bus.."/device/new_device","w") + io.output(f) + io.write(device["name"].." "..device["address"]) + io.close(f) + end) + end + + device_path=find_device(device["name"],subsystem) + + if device_path and device["set_param"] then + for key,record in pairs(device["set_param"]) do + setparam = split(record,":") + setpath = device_path.."/"..setparam[1] + pcall(function () + local f = io.open(setpath,"w") + io.output(f) + io.write(setparam[2]) + io.close(f) + end) + end + end + + if device_path and device["parameter"] then + + for key,record in pairs(device["parameter"]) do + + getparam = split(record,":") + getparameter = {} + getparameter["path"] = device_path.."/"..getparam[1] + getparameter["name"] = getparam[2] + getscale = getparam[3] + getcorrection = getparam[4] + if not getscale then + getscale = 1 + end + if not getcorrection then + getcorrection = 0 + end + getparameter["scale"] = tonumber(getscale) + getparameter["sensor"] = device["name"]:upper() + getparameter["correction"] = tonumber(getcorrection) + + parameters[#parameters+1] = getparameter + + end + + end + + end + +end + +function init(config_name) + + local parameters= {} + + i2c_bus = uci.get(config_name,"hardware","i2c_bus") + + local devices = get_device_list(config_name) + + for key,devname in pairs(devices) do + + device = get_device(config_name,devname) + + if device then + init_device(device,parameters,i2c_bus) + end + + end + + return parameters + +end + +function get_parameter(parameter) + return tonumber(get_file_content(parameter["path"])) * parameter["scale"] + parameter["correction"] +end + +function get_parameters(parameters) + local results = {} + for key,record in pairs(parameters) do + if not results[record["sensor"]] then + results[record["sensor"]] = {} + end + results[record["sensor"]][record["name"]] = get_parameter(record) + end + return results +end + +config_name = arg[1] +if not config_name then + config_name = "weathermon" +end + +parameters = init(config_name) + +local delay = uci.get(config_name,"process","delay") + +local working_dir = uci.get(config_name,"process","working_dir") +local dump_file = uci.get(config_name,"process","dump_file") +if working_dir then + lfs.mkdir(working_dir) +end + +if not delay then + delay = 60 +end + +while true do + values = get_parameters(parameters) + for key,record in pairs(values) do + dump = record + dump["device"] = key + print(json.encode(dump)) + end + if dump_file then + local f = io.open(dump_file,"w") + io.output(f) + io.write(json.encode(values)) + io.close(f) + end + socket.sleep(delay) +end + \ No newline at end of file diff --git a/weathermon-light b/weathermon-light deleted file mode 100755 index 1827377..0000000 --- a/weathermon-light +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/python - -import serial - -from os import system - -from pprint import pprint - -from termios import tcflush, TCIOFLUSH - -from time import sleep - -from uuid import getnode - -import sys,traceback - -import pycurl -from urllib import urlencode -from StringIO import StringIO - -path = None -baud = None -timeout = None - -import ConfigParser - -import thread -from threading import Timer - -def open_port(path): - - global proc - - print "Opening path "+path - - ser = serial.Serial(path,1200); - ser.open(); - ser.close(); - - ser = serial.Serial(path,baud,timeout=timeout) - if ser.portstr: - tcflush(ser,TCIOFLUSH); - return ser - -def read_port(ser): - - try: - timeout_timer = Timer(timeout, thread.interrupt_main) - timeout_timer.start() - line = ser.readline() - return line.strip() - except KeyboardInterrupt: - return "<>" - finally: - timeout_timer.cancel() - -def read_loop(ser,callback): - - global proc - - while True: - - try: - line=read_port(ser) - if line=="<>": - print "Reopening port..." - ser.close() - ser=open_port(path) - if line: - callback(line) - except KeyboardInterrupt: - break - finally: - None - -def print_log(str): - global logging - print str - if logging == "on": - system("logger -t weathermon \""+str+"\"") - -def submit_data(stype,sid,param,value): - - global url,username,password - - params = {'stype':stype, 'sid':sid, 'param':param, 'value':value} - - pprint (params) - - try: - - response_buffer = StringIO() - curl = pycurl.Curl() - - curl.setopt(curl.URL, url) - if username: - curl.setopt(curl.USERPWD, '%s:%s' % (username, password)) - curl.setopt(curl.WRITEFUNCTION, response_buffer.write) - curl.setopt(curl.POST, 1) - curl.setopt(curl.POSTFIELDS, urlencode(params)) - - curl.perform() - curl.close() - - response_value = response_buffer.getvalue() - - print_log('Server response: '+response_value) - - return True - - except: - - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_tb(exc_traceback, limit=1, file=sys.stdout) - traceback.print_exception(exc_type, exc_value, exc_traceback, - limit=2, file=sys.stdout) - return False - -def process_str(str): - print_log("Received: "+str) - try: - msg_type, msg_body = str.split(':') - except: - return - try: - if msg_type == 'STATUS': - print_log('Status: '+msg_body) - elif msg_type == 'ERROR': - print_log('Error: '+ msg_body) - elif msg_type == 'SENSOR': - sens = msg_body.split(',') - sensor = {} - sensor_type = None - sensor_id = None - for rec in sens: - key,value = rec.split('=') - value=value.strip() - if len(value)>0: - if key == 'TYPE': - sensor_type = value - elif key == 'ID': - sensor_id = value - else: - sensor[key] = value - if sensor_type: - if not sensor_id: - sensor_id=devid; - for key in sensor: - if sensor[key]: - print_log('Type = '+sensor_type+', ID = '+sensor_id+', Param = '+key+', Value = '+sensor[key]) - submit_data(sensor_type,sensor_id,key,sensor[key]) - else: - print_log('Error: got empty parameter value for '+sensor_type+'.'+sensor_id+'.'+key) - except: - print_log('Exception processing...') - exc_type, exc_value, exc_traceback = sys.exc_info() - traceback.print_tb(exc_traceback, limit=1, file=sys.stdout) - traceback.print_exception(exc_type, exc_value, exc_traceback, - limit=5, file=sys.stdout) - -def weather_mon(): - - global path - - ser = open_port(path) - read_loop(ser,process_str) - -def main(): - weather_mon() - -try: - - cfg = ConfigParser.RawConfigParser(allow_no_value=True) - cfg.readfp(open('/etc/weathermon.conf')) - url = cfg.get("web","url") - path = cfg.get("serial","port"); - try: - username = cfg.get("web","user"); - password = cfg.get("web","password"); - except: - username = None - password = None - try: - logging = cfg.get("logging","enabled") - except: - logging = None - try: - timeout = int(cfg.get("serial","timeout")) - except: - timeout = 120 - try: - baud = int(cfg.get("serial","baud")) - except: - baud = 57600 - try: - devid = cfg.get("web","devid") - except: - devid = "{:0>12X}".format(getnode()) - -except: - - print_log("Cannot intialize system") - exit() - -if __name__ == "__main__": - import sys - reload(sys) - sys.setdefaultencoding('utf-8') - main() diff --git a/weathermon.lua b/weathermon.lua index 72e4895..3208e30 100755 --- a/weathermon.lua +++ b/weathermon.lua @@ -1,7 +1,7 @@ #!/usr/bin/lua -require("json") -require("socket") +json = require("json") +socket = require("socket") function startswith(String,Start) if String then @@ -211,6 +211,10 @@ function processJson(str) end end + if not sensor_id then + sensor_id = web_devid + end + if not (sensor_type==nil or sensor_id==nil or sensor_type=='' or sensor_id=='') then if next(sensor)==nil then sensor["command"]="alarm" @@ -237,7 +241,7 @@ function processJson(str) end end else - printLog("Cannot parse sensor input: "..msg_body) + printLog("Cannot parse sensor input: "..str) end end @@ -371,7 +375,7 @@ else return end while 1 do - line=serialin:read() + line=serialin:read("*l") if line == nil then break end @@ -379,6 +383,6 @@ while 1 do if startswith(line,'{') then processJson(line) else - processLine(line) + processLine(line) end end diff --git a/weathermon.uci b/weathermon.uci index 6c2f6e4..7a4162f 100644 --- a/weathermon.uci +++ b/weathermon.uci @@ -1,16 +1,64 @@ config internal 'web' - option url http://server/path/send.php + option url http://url-to-submit-meteo-data option user meteo - option password somestrictpassword - option iface eth0 + option password some-password + option iface wlan0 -config internal 'serial' - option port /dev/ttyUSB0 - option timeout 100 - option baud 9600 +config internal 'input' + option exec "/usr/bin/stdbuf -o0 /usr/bin/lua /usr/bin/weathermon-iio" +# option port /dev/ttyATH0 +# option timeout 100 +# option baud 57600 config internal 'logging' - option enabled on + option enabled off # on/stdout/syslog +# option touch_file /var/run/weathermon/weathermon.last config internal 'mqtt' - option host server + option host mqtt.host.name + option user meteo-user + option password some-password + +config internal 'alarm' +# option exec /usr/local/bin/alarm_received + +config internal 'hardware' + option i2c_bus 0 + +config internal 'process' + option delay 48 + option working_dir "/var/weather/" + option dump_file "/var/weather/weather.state" + +config device "bme280" + option module "bmp280_i2c" + option address "0x76" + option type "i2c:iio" + option name "bme280" + list set_param "in_humidityrelative_oversampling_ratio:4" + list set_param "in_temp_oversampling_ratio:8" + list set_param "in_pressure_oversampling_ratio:8" + list parameter "in_temp_input:T:0.001:-4" # source, name, scale, correction + list parameter "in_pressure_input:P:10" + list parameter "in_humidityrelative_input:H:0.001" + +config device "hmc5843" + option module "hmc5843_i2c" + option address "0x1e" + option type "i2c:iio" + option name "hmc5843" + list set_param "in_magn_meas_conf:normal" + list set_param "in_magn_sampling_frequency:2" + list set_param "in_magn_scale:0.000007692" + list parameter "in_magn_x_raw:MX" + list parameter "in_magn_y_raw:MY" + list parameter "in_magn_z_raw:MZ" + +config device "ads1115" + option module "ads1015" + option address "0x48" + option type "i2c:hwmon" + option name "ads1115" + list parameter "in4_input:CO" + list parameter "in5_input:CH4" + list parameter "in6_input:AIR" -- 2.34.1