Nachtrag:

in der airQ_conrant.py in Zeile 374

if len(__altitude)==3: 
__altitude = weewx.units.ValueTuple(__altitude[0],__altitude[1],__altitude[2
]) 
else: __altitude = weewx.units.ValueTuple(__altitude[0],__altitude[1],
'group_altitude')

ÄNDERUNG in 

 if len(__altitude)==3:
     __altitude = weewx.units.ValueTuple(float(__altitude[0])
,__altitude[1],__altitude[2])
     else:
     __altitude = weewx.units.ValueTuple(float(__altitude[0]),
__altitude[1],'group_altitude')

sonst wird der Wert __altitude als String gelesen.

Meine kleine Erweiterung mit Fehlerwerten im Anhang.

Fehler nach wie vor bei 
Oct  2 12:30:34 wee2 weewx-weewx[13860] ERROR weewx.cheetahgenerator: 
Evaluation of template /home/weewx/skins/airQ/first_device.html.tmpl failed.
Oct  2 12:30:34 wee2 weewx-weewx[13860] ERROR weewx.cheetahgenerator: **** 
Ignoring template /home/weewx/skins/airQ/first_device.html.tmpl
Oct  2 12:30:34 wee2 weewx-weewx[13860] ERROR weewx.cheetahgenerator: **** 
Reason: cannot find 'gram_per_meter_cubed' while searching for '
og_HumAbs.gram_per_meter_cubed'
Oct  2 12:30:34 wee2 weewx-weewx[13860] ERROR weewx.cheetahgenerator: **** 
To debug, try inserting '#errorCatcher Echo' at top of template
Oct  2 12:30:34 wee2 weewx-weewx[13860] ERROR weewx.cheetahgenerator: 
Evaluation of template /home/weewx/skins/airQ/second_device.html.tmpl 
failed.
Oct  2 12:30:34 wee2 weewx-weewx[13860] ERROR weewx.cheetahgenerator: **** 
Ignoring template /home/weewx/skins/airQ/second_device.html.tmpl
Oct  2 12:30:34 wee2 weewx-weewx[13860] ERROR weewx.cheetahgenerator: **** 
Reason: cannot find 'gram_per_meter_cubed' while searching for '
eg_HumAbs.gram_per_meter_cubed'
Oct  2 12:30:34 wee2 weewx-weewx[13860] ERROR weewx.cheetahgenerator: **** 
To debug, try inserting '#errorCatcher Echo' at top of template
Oct  2 12:30:34 wee2 weewx-weewx[13860] INFO weewx.cheetahgenerator: 
Generated 0 files for report AirQsReport in 0.49 seconds
Oct  2 12:30:36 wee2 weewx-weewx[13860] INFO weewx.imagegenerator: 
Generated 29 images for report AirQsReport in 2.19 seconds

alle anderen Erweiterungen "ppb", "milligram_per_meter_cubed" werden 
angezeigt.

in prep_services wurde user.airQ_corant.AirqUnits eingefügt.

Hartmut
hesf...@gmail.com schrieb am Sonntag, 5. September 2021 um 15:55:45 UTC+2:

> Danke für die Erläuterungen, mir fehlt jetzt nur noch eine Idee für die 
> Feststellung der Fehlerwerte, aber Kommt Zeit Kommt Rat
> Hartmut
>
>
> kk44...@gmail.com schrieb am Sonntag, 5. September 2021 um 15:39:48 UTC+2:
>
>> hesf...@gmail.com schrieb am Sonntag, 5. September 2021 um 15:21:04 
>> UTC+2:
>>
>>> Die Berechnung für "Barometer" sollte doch für indoor airQ's mit 
>>> "temperature" vom airQ erfolgen, der Bezug zu "outTemp" geht meiner Meinung 
>>> nach fehl.
>>>
>>
>> Nein, das stimmt nicht. Für die Umrechnung des lokalen Luftdrucks (der im 
>> Zimmer derselbe ist wie draußen) auf Meeresniveau muß immer die 
>> Außentemperatur genommen werden. Diese Umrechnung soll ja die Werte 
>> verschiedener Meßstellen vergleichbar machen. Der Wert darf nicht davon 
>> abhängen, ob man sich gerade im Zimmer oder draußen befindet.
>>  
>> Luftdruckumrechnung DWD 
>> <https://www.dwd.de/DE/fachnutzer/luftfahrt/download/produkte/runwaymap/06_luftdruck_und_standardatmoshaere.pdf?__blob=publicationFile&v=2>
>>
>>

-- 
You received this message because you are subscribed to the Google Groups 
"weewx-user" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to weewx-user+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/weewx-user/32116144-a95e-457c-91df-5bc7b365fdeen%40googlegroups.com.
#!/usr/bin/python3
#
# WeeWX service to read data from the airQ device
#
# Copyright (C) 2021 Johanna Roedenbeck
# airQ API Copyright (C) Corant GmbH

"""

Hardware: https://www.air-q.com

Science option is required.


Most of the the observation types provided by the airQ device are
predefined within WeeWX. If no special configuration besides host
address and password is provided the measured values are stored to
those observation types.

More than one device can be used. That is done by configurating a
specific prefix for the observation types of each device.

Configuration in weewx.conf:

[airQ]

    query_interval = 5.0 # optional, default 5.0 seconds

    [[first_device]]
        host = replace_me_by_host_address_or_IP
        password = replace_me
        prefix = replace_me # optional
        altitude = 123, meter # optional, default station altitude
        query_interval = 5.0 # optional, default 5.0 seconds

    [[second_device]]
        ...

"""

VERSION = 4.8

# imports for airQ
import base64
from Cryptodome.Cipher import AES
import http.client
import json

# deal with differences between python 2 and python 3
try:
    # Python 3
    import queue
except ImportError:
    # Python 2
    # noinspection PyUnresolvedReferences
    import Queue as queue

# imports for WeeW
import six
import threading
import time
if __name__ != '__main__':
    # for use as service within WeeWX
    import weewx # WeeWX-specific exceptions, class Event
    from weewx.engine import StdService
    import weewx.units
    import weewx.accum
    import weeutil.weeutil
    from weewx.wxformulas import altimeter_pressure_Metric,sealevel_pressure_Metric
else:
    # for standalone testing
    import sys
    import collections
    sys.path.append('../../test')
    from testpasswd import airqIP,airqpass
    class StdService(object):
        def __init__(self, engine, config_dict):
            pass
        def bind(self,p1,p2):
            pass
    class weewx(object):
        NEW_LOOP_PACKET = 1
        class units(object):
            def convertStd(p1, p2):
                return p1
            def convert(p1, p2):
                return (p1[0],p2,p1[2])
            obs_group_dict = collections.ChainMap()
            conversionDict = collections.ChainMap()
            default_unit_format_dict = collections.ChainMap()
            default_unit_label_dict = collections.ChainMap()
        class accum(object):
            accum_dict = collections.ChainMap()
    class weeutil(object):
        class weeutil(object):
            def to_int(x):
                return int(x)
    class Event(object):
        packet = { 'usUnits':16 }
    class Engine(object):
        class stn_info(object):
            altitude_vt = (0,'meter','group_altitude')


try:
    # Test for new-style weewx logging by trying to import weeutil.logger
    import weeutil.logger
    import logging
    log = logging.getLogger("user.airQ")

    def logdbg(msg):
        log.debug(msg)

    def loginf(msg):
        log.info(msg)

    def logerr(msg):
        log.error(msg)

except ImportError:
    # Old-style weewx logging
    import syslog

    def logmsg(level, msg):
        syslog.syslog(level, 'user.airQ: %s' % msg)

    def logdbg(msg):
        logmsg(syslog.LOG_DEBUG, msg)

    def loginf(msg):
        logmsg(syslog.LOG_INFO, msg)

    def logerr(msg):
        logmsg(syslog.LOG_ERR, msg)


ACCUM_LAST_DICT = { 'accumulator':'firstlast','extractor':'last' }

##############################################################################
#   add additional units needed for airQ                                     #
##############################################################################

# unit g/m^2 and mg/m^2 for 'group_concentration'
weewx.units.conversionDict.setdefault('microgram_per_meter_cubed',{})
weewx.units.conversionDict.setdefault('milligram_per_meter_cubed',{})
weewx.units.conversionDict.setdefault('gram_per_meter_cubed',{})
weewx.units.conversionDict['gram_per_meter_cubed']['microgram_per_meter_cubed'] = lambda x : x*1000000
weewx.units.conversionDict['milligram_per_meter_cubed']['microgram_per_meter_cubed'] = lambda x : x*1000
weewx.units.conversionDict['microgram_per_meter_cubed']['gram_per_meter_cubed'] = lambda x : x*0.000001
weewx.units.conversionDict['microgram_per_meter_cubed']['milligram_per_meter_cubed'] = lambda x : x*0.001
weewx.units.conversionDict['milligram_per_meter_cubed']['gram_per_meter_cubed'] = lambda x : x*0.001
weewx.units.conversionDict['gram_per_meter_cubed']['milligram_per_meter_cubed'] = lambda x : x*1000
weewx.units.default_unit_format_dict.setdefault('gram_per_meter_cubed',"%.1f")
weewx.units.default_unit_label_dict.setdefault('gram_per_meter_cubed',u" g/m³")
weewx.units.default_unit_format_dict.setdefault('milligram_per_meter_cubed',"%.1f")
weewx.units.default_unit_label_dict.setdefault('milligram_per_meter_cubed',u" mg/m³")
# unit ppb for 'TVOC'
weewx.units.conversionDict.setdefault('ppb',{})
weewx.units.conversionDict.setdefault('ppm',{})
weewx.units.conversionDict['ppb']['ppm'] = lambda x:x*0.001
weewx.units.conversionDict['ppm']['ppb'] = lambda x:x*1000
weewx.units.default_unit_format_dict.setdefault('ppb',"%.0f")
weewx.units.default_unit_label_dict.setdefault('ppb',u" ppb")
weewx.units.default_unit_format_dict.setdefault('ppm',"%.0f")
weewx.units.default_unit_label_dict.setdefault('ppm',u" ppm")

##############################################################################
#   get data out of the airQ device                                          #
##############################################################################

def airQreply(htmlreply, passwd):
    """ convert the reply to json """
    # the reply is a json string
    _rtn = json.loads(htmlreply)
    # 'content' is base64 encoded and encrypted data
    if 'content' in _rtn:
        # convert base64 to plain text
        _crtxt = base64.b64decode(_rtn['content'])
        # convert passwd to bytes
        _aeskey = passwd.encode('utf-8')
        # adjust to 32 bytes of length
        _aeskey = _aeskey.ljust(32,b'0')
        # decode AES256
        _cipher = AES.new(key=_aeskey, mode=AES.MODE_CBC, IV=_crtxt[:16])
        _txt = _cipher.decrypt(_crtxt[16:]).decode('utf-8')
        _rtn['content'] =  json.loads(_txt[:-ord(_txt[-1])])
    # reply converted to python dict with 'content' decoded
    return _rtn

def airQget(host, page, passwd):
    """ get page from airQ """
    try:
        connection = http.client.HTTPConnection(host)
        connection.request('GET', page)
        _response = connection.getresponse()
        if _response.status==200:
            # successful --> get response
            reply = airQreply(_response.read(), passwd)
        else:
            # HTML error
            reply = {'content':{}}
        reply['replystatus'] = _response.status
        reply['replyreason'] = _response.reason
        reply['replyexception'] = ""
    except http.client.HTTPException as e:
        reply = {
            'replystatus': 503,
            'replyreason': "HTTPException %s" % e,
            'replyexception': "HTTPException",
            'content': {}}
    except OSError as e:
        # device not found
        # connection reset
        if e.__class__.__name__ in ['ConnectionError','ConnectionResetError','ConnectionAbortedError','ConnectionRefusedError']:
            __status = 503
        else:
            __status = 404
        reply = {
            'replystatus': __status,
            'replyreason': "OSError %s - %s" % (e.__class__.__name__,e),
            'replyexception': e.__class__.__name__,
            'content': {}}
    finally:
        connection.close()

    airq_dat = open("/home/weewx/archive/airq_data.json", "w")
    wert = json.dumps(reply)
    airq_dat.write(wert)
    airq_dat.close()
    return reply

##############################################################################
#    Thread to retrieve data from the air-Q device                           #
##############################################################################

class AirqThread(threading.Thread):
    """ retrieve data from airQ device """
    
    def __init__(self, q, name, address, passwd, log_success, log_failure, query_interval):
        """ initialize thread """
        super(AirqThread,self).__init__()
        self.queue = q
        self.name = name
        self.address = address
        self.passwd = passwd
        self.log_success = log_success
        self.log_failure = log_failure
        self.query_interval = query_interval
        self.running = True
        loginf("thread '%s', host '%s': initialized" % (self.name,self.address))
        
    def shutdown(self):
        """ stop thread """
        self.running = False
        
    def run(self):
        """ run thread """
        loginf("thread '%s', host '%s': starting" % (self.name,self.address))
        errsleep = 60
        laststatuschange = time.time()
        while self.running:
            reply = airQget(self.address, '/data', self.passwd)
            if reply['replystatus']==200:
                if errsleep:
                    if self.log_success:
                        loginf("thread '%s', host '%s': %s - %s" % (self.name,self.address,reply['replystatus'],reply['replyreason']))
                    errsleep = 0
                    laststatuschange = time.time()
                self.queue.put(reply['content'])
                time.sleep(self.query_interval)
            else:
                if errsleep==0: laststatuschange = time.time()
                if self.log_failure:
                    logerr("thread '%s', host '%s': %s - %s - %.0f s since last success" % (self.name,self.address,reply['replystatus'],reply['replyreason'],time.time()-laststatuschange))
                # wait
                time.sleep(errsleep)
                if errsleep<300: errsleep+=60
        loginf("thread '%s', host '%s': stopped" % (self.name,self.address))
        

##############################################################################
#   data_services: augment LOOP packet with airQ readings                    #
##############################################################################

class AirqService(StdService):

    # observation types
    AIRQ_DATA = {
        'DeviceID':    ('airqDeviceID',    None,None,lambda x:x),
        'Status':      ('airqStatus',      None,None,lambda x:x),
        'timestamp':   None,
        'measuretime': ('airqMeasuretime', None, None, lambda x:int(x)),
        'uptime':      ('airqUptime',      None, None, lambda x:int(x)),
        'temperature': ('airqTemp',        'degree_C', 'group_temperature',lambda x:float(x[0])),
        'humidity':    ('airqHumidity',    'percent', 'group_percent',lambda x:float(x[0])),
        'humidity_abs':('airqHumAbs',      'gram_per_meter_cubed', 'group_concentration', lambda x:float(x[0])),
        'dewpt':       ('airqDewpoint',    'degree_C', 'group_temperature',lambda x:float(x[0])),
        'pressure':    ('airqPressure',    'mbar',    'group_pressure',lambda x:float(x[0])),
        'altimeter':   ('airqAltimeter',   'mbar',    'group_pressure',lambda x:float(x[0])),
        'barometer':   ('airqBarometer',   'mbar',    'group_pressure',lambda x:float(x[0])),
        'co':          ('airqco_m',        'milligram_per_meter_cubed', 'group_concentration',lambda x:float(x[0])),
        'co_vol':      ('airqco',          'ppm',     'group_fraction',lambda x:x),
        'co2_m':       ('airqco2_m',       'microgram_per_meter_cubed', 'group_concentration', lambda x: x),
        'co2':         ('airqco2',         'ppm',     'group_fraction',lambda x:float(x[0])),
        'h2s':         ('airqh2s',         "microgram_per_meter_cubed", "group_concentration",lambda x:x[0]),
        'no2':         ('airqno2_m',       "microgram_per_meter_cubed", "group_concentration",lambda x:float(x[0])),
        'no2_vol':     ('airqno2',         'ppb',     'group_fraction',lambda x:x[0]),
        'pm1':         ('airqpm1_0',       "microgram_per_meter_cubed", "group_concentration",lambda x:x[0]),
        'pm2_5':       ('airqpm2_5',       "microgram_per_meter_cubed", "group_concentration",lambda x:x[0]),
        'pm10':        ('airqpm10_0',      "microgram_per_meter_cubed", "group_concentration",lambda x:x[0]),
        'o3':          ('airqo3_m',        "microgram_per_meter_cubed", "group_concentration",lambda x:float(x[0])),
        'o3_vol':      ('airqo3',          "ppb",                       "group_fraction",lambda x:x),
        'so2':         ('airqso2_m',       "microgram_per_meter_cubed", "group_concentration",lambda x:float(x[0])),
        'so2_vol':     ('airqso2',         'ppb',     'group_fraction',lambda x:x[0]),
        'tvoc':        ('airqTVOC',        'ppb',     'group_fraction',lambda x:float(x[0])),
        'tvoc_m':      ('airqTVOC_m',      'microgram_per_meter_cubed', 'group_concentration', lambda x: float(x[0])),
        'oxygen':      ('airqo2',          'percent', 'group_percent',lambda x:float(x[0])),
        'sound':       ('airqnoise',       'dB',      'group_db', lambda x:float(x[0])),
        'performance': ('airqPerfIdx',     'percent', 'group_percent', lambda x:float(x)/10),
        'health':      ('airqHealthIdx',   'percent', 'group_percent', lambda x:x/10),
        'cnt0_3':      ('airqcnt0_3',      'count',   'group_count', lambda x:int(x[0])),
        'cnt0_5':      ('airqcnt0_5',      'count',   'group_count', lambda x:int(x[0])),
        'cnt1':        ('airqcnt1_0',      'count',   'group_count', lambda x:int(x[0])),
        'cnt2_5':      ('airqcnt2_5',      'count',   'group_count', lambda x:int(x[0])),
        'cnt5':        ('airqcnt5_0',      'count',   'group_count', lambda x:int(x[0])),
        'cnt10':       ('airqcnt10_0',     'count',   'group_count', lambda x:int(x[0])),
        'TypPS':       ('airqTypPS',       None, None, lambda x:x),
        'bat':         ('airqBattery',     None, None, lambda x:x[0]),
        'door_event':  ('airqDoorEvent',   None, None, lambda x:int(x)),
        'dHdt':        ('airqHumAbsDelta', None, None, lambda x: x),
        'dCO2dt':      ('airqCO2delta',    None, None, lambda x: x),
        'bat_e':         ('airqBattery_e',     None, None, lambda x:x[1]),
        'temperature_e': ('airqTemp_e',      'percent', 'group_percent', lambda x:float(x[1])),
        'humidity_e':    ('airqHumidity_e',  'percent', 'group_percent', lambda x:float(x[1])),
        'humidity_abs_e':('airqHumAbs_e',    'percent', 'group_percent', lambda x:float(x[1])),
        'dewpt_e':       ('airqDewpoint_e',  'percent', 'group_percent', lambda x:float(x[1])),
        'pressure_e':    ('airqPressure_e',  'percent', 'group_percent', lambda x:float(x[1])),
        'co_e':          ('airqco_e',        'percent', 'group_percent', lambda x:float(x[1])),
        'co2_e':         ('airqco2_e',       'percent', 'group_percent', lambda x:float(x[1])),
        'no2_e':         ('airqno2_e',       'percent', 'group_percent', lambda x:float(x[1])),
        'pm1_e':         ('airqpm1_0_e',     'percent', 'group_percent', lambda x:float(x[1])),
        'pm2_5_e':       ('airqpm2_5_e',     'percent', 'group_percent', lambda x:float(x[1])),
        'pm10_e':        ('airqpm10_0_e',    'percent', 'group_percent', lambda x:float(x[1])),
        'o3_e':          ('airqo3_e',        'percent', 'group_percent', lambda x:float(x[1])),
        'so2_e':         ('airqso2_e',       'percent', 'group_percent', lambda x:float(x[1])),
        'tvoc_e':        ('airqTVOC_e',      'percent', 'group_percent', lambda x:float(x[1])),
        'oxygen_e':      ('airqo2_e',        'percent', 'group_percent', lambda x:float(x[1])),
        'sound_e':       ('airqnoise_e',     'percent', 'group_percent', lambda x:float(x[1])),
        'cnt0_3_e':      ('airqcnt0_3_e',    'percent', 'group_percent', lambda x:float(x[1])),
        'cnt0_5_e':      ('airqcnt0_5_e',    'percent', 'group_percent', lambda x:float(x[1])),
        'cnt1_e':        ('airqcnt1_0_e',    'percent', 'group_percent', lambda x:float(x[1])),
        'cnt2_5_e':      ('airqcnt2_5_e',    'percent', 'group_percent', lambda x:float(x[1])),
        'cnt5_e':        ('airqcnt5_0_e',    'percent', 'group_percent', lambda x:float(x[1])),
        'cnt10_e':       ('airqcnt10_0_e',   'percent', 'group_percent', lambda x:float(x[1])),
        }

    # which readings are to accumulate calculating average
    AVG_GROUPS = [
        'group_temperature',
        'group_concentration',
        'group_fraction']

    # which readings are non-numeric
    ACCUM_LAST = [
        'DeviceID',
        'Status']
        #'bat']

    # conversion volume to mass according to Dr. Daniel Lehmann of Corant
    # valid up to firmware version 1.74 only
    CONV_V_M = {
        'co':  1.15,
        'co2': 1.54,
        'no2': 1.88,
        'o3':  1.96,
        'so2': 2.62}

    CONV_M_V = {
        'co2': 1.54,
        'tvoc': 2.909,
        }


    # valid from firmware version 1.75 on g/mol
    MOL_MASS = {
        'co':  28.0101,
        'co2': 44.0099,
        'no2': 46.0055,
        'h2s': 65.1378,
        'o3':  47.9982,
        'so2': 64.0638,
        'o2':  31.9988,
        'tvoc': 78.9516}


    # which readings are errors
    ERR_VAL = [
        'tvoc',
        'pm2_5',
        'humidity',
        'cnt0_3',
        'sound',
        'temperature',
        'cnt0_5',
        'co',
        'humidity_abs',
        'co2',
        'so2',
        'cnt2_5',
        'o3',
        'cnt10',
        'no2',
        'cnt5',
        'pressure',
        'cnt1',
        'pm1',
        'oxygen',
        'dewpt',
        'pm10',
        ]


    def __init__(self, engine, config_dict):
        super(AirqService,self).__init__(engine, config_dict)
        loginf("air-Q %s service" % VERSION)
        # logging configuration
        self.log_success = config_dict.get('log_success',True)
        self.log_failure = config_dict.get('log_failure',True)
        self.debug = weeutil.weeutil.to_int(config_dict.get('debug',0))
        if self.debug>0:
            self.log_success = True
            self.log_failure = True
        # conversion between volume and mass
        self.volume_mass_method = weeutil.weeutil.to_int(config_dict.get('airQ',{}).get('volume_mass_method',1))
        loginf("volume_mass_method %s" % self.volume_mass_method)
        # dict of devices and threads
        self.threads={}
        # devices
        ct = 0
        if 'airQ' in config_dict:
            for device in config_dict['airQ'].sections:
                # altitude to calculate altimeter value
                if 'altitude' in config_dict['airQ'][device]:
                    __altitude = config_dict['airQ'][device]['altitude']
                    loginf("Altitude ermittelt %s "  % (__altitude))
                    if len(__altitude)==3:
                        __altitude = weewx.units.ValueTuple(float(__altitude[0]),__altitude[1],__altitude[2])
                    else:
                        __altitude = weewx.units.ValueTuple(float(__altitude[0]),__altitude[1],'group_altitude')
                else:
                    __altitude = engine.stn_info.altitude_vt
                __altitude = weewx.units.convert(__altitude,'meter')[0]
                # create thread
                if self._create_thread(device,
                    config_dict['airQ'][device].get('host'),
                    config_dict['airQ'][device].get('password'),
                    config_dict['airQ'][device].get('prefix'),
                    __altitude,
                    config_dict['airQ'][device].get('query_interval',config_dict['airQ'].get('query_interval',5.0))):
                    ct+=1
            if ct>0:
                self.bind(weewx.NEW_LOOP_PACKET, self.new_loop_packet)
        if ct==1:
            loginf("1 air-Q device found")
        else:
            loginf("%s air-Q devices found" % ct)

    def _create_thread(self, thread_name, address, passwd, prefix, altitude, query_interval):
        if address is None or address=='': 
            logerr("device '%s': not host address defined" % thread_name)
            return False
        if passwd is None or passwd=='':
            logerr("device '%s': no password defined" % thread_name)
            return False
        # report config data from weewx.conf to syslog
        loginf("device '%s' host address '%s' prefix '%s' query interval %.1f s altitude %.0f m" % (thread_name,address,prefix,query_interval,altitude))
        # get config data out of device and log
        try:
            devconf = airQget(address,'/config',passwd)
            devconf = devconf.get('content',{})
        except:
            logerr("device '%s': could not read config out of the device" % thread_name)
            devconf = {}
        loginf("device '%s' device id: %s" % (thread_name,devconf.get('id','unknown')))
        loginf("device '%s' firmware version: %s" % (thread_name,devconf.get('air-Q-Software-Version','unknown')))
        loginf("device '%s' sensors: %s" % (thread_name,devconf.get('sensors','unkown')))
        loginf("device '%s' concentration units config: %s" % (thread_name,'ppb&ppm' if devconf.get('ppb&ppm',False) else 'µg/m^3'))
        # initialize thread
        self.threads[thread_name] = {}
        self.threads[thread_name]['queue'] = queue.Queue()
        self.threads[thread_name]['thread'] = AirqThread(self.threads[thread_name]['queue'], thread_name, address, passwd, self.log_success, self.log_failure, query_interval)
        self.threads[thread_name]['prefix'] = prefix
        self.threads[thread_name]['altitude'] = altitude
        self.threads[thread_name]['QFF_temperature_source'] = 'outTemp'
        self.threads[thread_name]['ppb&ppm'] = devconf.get('ppb&ppm',False)
        self.threads[thread_name]['RoomType'] = devconf.get('RoomType')
        self.threads[thread_name]['state'] = {'init':'1'}
        # log settings for calculating the barometer value
        if self.isDeviceOutdoor(thread_name):
            loginf("device '%s' QFF calculation temperature source: airQ temperature reading" % thread_name)
        else:
            loginf("device '%s' QFF calculation temperature source: %s" % (thread_name,self.threads[thread_name]['QFF_temperature_source']))
        # set accumulators for non-numeric observation types
        _accum = {}
        for ii in self.ACCUM_LAST:
            _obs_conf = self.AIRQ_DATA[ii]
            if _obs_conf:
                _accum[self.obstype_with_prefix(_obs_conf[0],prefix)] = ACCUM_LAST_DICT
            else:
                _accum[self.obstype_with_prefix(ii,prefix)] = ACCUM_LAST_DICT
        weewx.accum.accum_dict.maps.append(_accum)
        # set units for observation types
        for ii in self.AIRQ_DATA:
            _obs_conf = self.AIRQ_DATA[ii]
            if _obs_conf and _obs_conf[2] is not None:
                #weewx.units.obs_group_dict.setdefault(self.obstype_with_prefix(_obs_conf[0],prefix),_obs_conf[2])
                weewx.units.obs_group_dict[self.obstype_with_prefix(_obs_conf[0],prefix)] = _obs_conf[2]
        # start thread
        self.threads[thread_name]['thread'].start()
        return True

    def shutdown(self):
        for ii in self.threads:
            loginf("shutting down connection to '%s'" % ii)
            self.threads[ii]['thread'].shutdown()

    def new_loop_packet(self, event):
        for ii in self.threads:
            data = {}
            avg_sum = {}
            avg_ct = {}
            last_ts = 0
            while True:
                try:
                    # get new data packet from queue
                    reply = self.threads[ii]['queue'].get(block=False)
                    # check timestamp
                    if reply['timestamp']<=last_ts:
                        logdbg("New record is older than last record.")
                        continue
                    # check status
                    try:
                        if reply.get('Status','')=='OK':
                            airqstate = {}
                        else:
                            airqstate = json.loads(reply['Status'])
                            if 'Status' in airqstate:
                                airqstate = airqstate['Status']
                        if airqstate!=self.threads[ii]['state']:
                            self.threads[ii]['state'] = airqstate
                            if airqstate:
                                logerr("thread '%s': state %s" % (ii,airqstate))
                            else:
                                loginf("thread '%s': state OK" % ii)
                    except (KeyError,ValueError,IndexError,TypeError):
                        airqstate = {}
                    # process values
                    for jj in reply:
                        try:
                            unit_group = self.AIRQ_DATA.get(jj)[2]
                        except (IndexError,TypeError):
                            unit_group = ""
                        if jj in airqstate:
                            # observation type is mentioned in status,
                            # that means the value is invalid
                            val = None
                        else:
                            # otherwise try to get the value
                            try:
                                val_e = 0
                                xx = self.AIRQ_DATA.get(jj)
                                val = xx[3](reply[jj]) if xx is not None else reply[jj]
                                if jj in self.ERR_VAL:
                                    val_e = reply[jj][1]

                                if jj not in self.ACCUM_LAST:
                                    if val<0.0: val = None
                            except (ValueError,TypeError,IndexError,KeyError) as e:
                                val = None

                        # loginf("val %s - %s - %s ++ %s" % (jj,reply[jj],val,val_e))
                        data.update({jj+'_e':val_e})

                        if unit_group in self.AVG_GROUPS:
                            # if observation type is in AVG_GROUPS, then
                            # add values for calculating averages
                            if val:
                                avg_sum[jj] = avg_sum.get(jj,0)+val
                                avg_ct[jj] = avg_ct.get(jj,0)+1
                        else:
                            # otherwise remember the last value of the
                            # loop period
                            data.update({jj:val})
                except queue.Empty:
                    break
                except KeyError:
                    # instead of queue.Empty KeyError was raised
                    break
                except (IndexError,ValueError,TypeError) as e:
                    logerr("new_loop_packet %s" % e)
            # calculate average
            for jj in avg_sum:
                data.update({jj:avg_sum[jj]/avg_ct[jj]})
            # calculate altimeter value from pressure reading
            if 'pressure' in data and 'altimeter' not in data:
                try:
                    data['altimeter'] = altimeter_pressure_Metric(data['pressure'],self.threads[ii]['altitude'])
                except (ValueError,TypeError,IndexError,KeyError):
                    pass
            # calculate barometer value from pressure and temperature reading
            if not self.isDeviceOutdoor(ii) and self.threads[ii]['QFF_temperature_source'] in event.packet:
                # As outTemp is not within every LOOP packet and airQ
                # readings are not available for every LOOP packet,
                # remember the outTemp reading for the next 5 minutes.
                # Only necessary if 'RoomType' is indoor.
                self.threads[ii]['outTemp_vt'] = weewx.units.as_value_tuple(
                    event.packet,
                    self.threads[ii]['QFF_temperature_source'])
                self.threads[ii]['outTempValid'] = time.time()+300
            if 'pressure' in data and 'barometer' not in data:
                try:
                    if self.isDeviceOutdoor(ii):
                        # if the airQ device is located outdoor, use the
                        # temperature measured by the device
                        t_C = data['temperature']
                    else:
                        # if the airQ device is located indoor, use the
                        # observation type 'outTemp'
                        if time.time()>self.threads[ii]['outTempValid']:
                            raise ValueError("no recent outTemp reading")
                        t_C = weewx.units.convert(self.threads[ii]['outTemp_vt'],'degree_C')[0]

                    data['barometer'] = sealevel_pressure_Metric(data['pressure'],self.threads[ii]['altitude'],t_C)

                except (ValueError,TypeError,IndexError,KeyError):
                    pass
            # volume or mass
            try:
                if self.threads[ii]['ppb&ppm']:
                    for vmobs in self.CONV_V_M:
                        if vmobs in data and vmobs in self.AIRQ_DATA:
                            data[vmobs+'_vol'] = data[vmobs]
                            data[vmobs] = self.convert_to_m(ii,vmobs,data[vmobs],data.get('temperature'),data.get('pressure'))
                else:
                    for vmobs in self.CONV_V_M:
                        if vmobs in data and vmobs in self.AIRQ_DATA:
                            data[vmobs+'_vol'] = self.convert_to_v(ii,vmobs,data[vmobs],data.get('temperature'),data.get('pressure'))
                            logdbg("%s: mass %.3f vol %.3f" % (vmobs,data[vmobs],data[vmobs+'_vol']))
                            # loginf("%s: mass %.3f vol %.3f" % (vmobs,data[vmobs],data[vmobs+'_vol']))
                            pass

                    for vmobs in self.CONV_M_V:
                        if vmobs in data and vmobs in self.AIRQ_DATA:
                            data[vmobs+'_m'] = self.convert_to_m(ii,vmobs,data[vmobs],data.get('temperature'),data.get('pressure'))
                            logdbg("%s: mass %.3f vol %.3f" % (vmobs,data[vmobs],data[vmobs+'_m']))
                            # loginf("%s: mass %.3f vol %.3f" % (vmobs,data[vmobs],data[vmobs+'_m']))
                            pass

            except (ValueError,TypeError,IndexError,KeyError) as e:
                pass

            # convert airQ to WeeWX observation type names and
            # values to archive unit system
            data = self.airq_to_weewx(data, self.threads[ii].get('prefix'), event.packet.get('usUnits'))
            # 'dateTime' and 'interval' must not be in data
            if data.get('dateTime'): del data['dateTime']
            if data.get('interval'): del data['interval']
            # log
            if self.debug>=3:
                logdbg("PACKET %s" % data)
            # loginf("PACKET %s" % data)
            # update loop packet with airQ data
            event.packet.update(data)

    def _volume_mass_factor(self, obs, temp, pressure):
        """ conversion factor between mass and volume """
        if not temp or not pressure or not self.volume_mass_method:
            return self.CONV_V_M[obs]
        return (self.MOL_MASS[obs]/22.4) * (273.15/(273.15+temp)) * (pressure/1013.25)

    def convert_to_m(self, thread_name, obs, val, temp, pressure):
        """ convert volume to mass """
        if not val: return None
        #if not self.threads[thread_name]['ppb&ppm']: return val
        return val * self._volume_mass_factor(obs, temp, pressure)

    def convert_to_v(self, thread_name, obs, val, temp, pressure):
        """ convert mass to volume """
        if not val: return None
        if self.threads[thread_name]['ppb&ppm']: return val
        return val / self._volume_mass_factor(obs, temp, pressure)


    @staticmethod
    def obstype_with_prefix(obs_type,prefix):
        """ prepend prefix if given """
        return prefix + '_' + obs_type.replace('airq','') if prefix else obs_type

    def isDeviceOutdoor(self, thread):
        """ check if the airQ device is located outdoor
            according to its configuration """
        return self.threads[thread]['RoomType']=='outdoor'

    def airq_to_weewx(self, data, prefix, usUnits):
        """ convert field names """
        _data = {}
        for key in data:
            val = data[key]
            # adapt name and convert value
            if key in self.AIRQ_DATA:
                # if no value tuple is given, ignore that key
                if self.AIRQ_DATA[key] is None: continue
                # get the WeeWX observation type
                weewx_key = self.AIRQ_DATA[key][0]
                # if unit and unit group are given, convert to archive unit
                if self.AIRQ_DATA[key][1]:
                    try:
                        val = weewx.units.convertStd(
                            (val, self.AIRQ_DATA[key][1], self.AIRQ_DATA[key][2]),
                            usUnits)[0]
                    except (ValueError,KeyError,IndexError):
                        val = None
            else:
                # if key not in self.AIRQ_DATA use value as is
                weewx_key = key
            # if prefix is set prepend key with prefix
            weewx_key = self.obstype_with_prefix(weewx_key,prefix)
            #if prefix: weewx_key = prefix + '_' + weewx_key.replace('airq','')
            if False:
                if weewx_key not in weewx.units.obs_group_dict:
                    loginf("obstype '%s' not in weewx.units.obs_group_dict (airQ '%s')" % (weewx_key,key))
            _data[weewx_key] = val
        return _data

##############################################################################
#   prep_services: augment units.py                                          #
##############################################################################

class AirqUnits(StdService):

    def __init__(self, engine, config_dict):
        super(AirqUnits,self).__init__(engine, config_dict)
        loginf("air-Q %s initialize units" % VERSION)

        if 'airQ' in config_dict:
            for device in config_dict['airQ'].sections:
                self._augment_obs_group_dict(device, config_dict['airQ'][device].get('prefix'))

    def _augment_obs_group_dict(self, device, prefix):
        """ set units for observation types """
        log_dict = {}
        for ii in AirqService.AIRQ_DATA:
            _obs_conf = AirqService.AIRQ_DATA[ii]
            if _obs_conf and _obs_conf[2] is not None:
                weewx_key = AirqService.obstype_with_prefix(_obs_conf[0],prefix)
                weewx.units.obs_group_dict[weewx_key] = _obs_conf[2]
                log_dict[weewx_key] = _obs_conf[2]
        loginf("device '%s': observation group dict %s" % (device,log_dict))

    
# To test the service, run it directly as follows:
if __name__ == '__main__':
    if False:
        connection = http.client.HTTPConnection(airqIP)
        reply = airQget(connection, '/data', airqpass) 
        connection.close()
        print("Status {} - {}".format(reply['replystatus'],reply['replyreason']))
        #print(reply['content'])
        for ii in reply['content']:
            print("%15s: %s" % (ii,reply['content'][ii]))
    else:
        CONF = {
            'airQ': {
                '1': {
                    'host':airqIP,
                    'password':airqpass
                    }
                }
            }
        srv = AirqService(Engine(),CONF)
        print("weewx.accum.accum_dict = ")
        print(weewx.accum.accum_dict)
        print("weewx.units.conversionDict =")
        print(weewx.units.conversionDict)
        print("-----------")
        for jj in range(1):
            time.sleep(11)
            evt = Event()
            srv.new_loop_packet(evt)
            for ii in evt.packet:
                print("%15s: %s" % (ii,evt.packet[ii]))
            print("------------")
        srv.shutdown()
        

Reply via email to