Ejemplo n.º 1
0
def set_output():
    """Activate triacs according to shift register state."""

    with gv.rs_lock:
        if gv.use_i2c:
            # todo fix for radio zones
            for b in range((gv.sd['nst'] - gv.sd['radiost']) // 8):
                if b not in gv.in_bootloader:
                    zone_state = 0
                    for z in range(8):
                        if gv.srvals[b * 8 + z]:
                            zone_state |= 1 << z
                    try:
                        set_state = zone_state & 0xff
                        clear_state = (~set_state) & 0xff
                        gv.logger.debug('Zone set for board ' + str(b + 1) +
                                        ' is: ' + hex(set_state))
                        i2c.i2c_write(i2c.ADDRESS + b, i2c.ZONE_SET, set_state)
                        gv.logger.debug('Zone clear for board ' + str(b + 1) +
                                        ' is: ' + hex(clear_state))
                        i2c.i2c_write(i2c.ADDRESS + b, i2c.ZONE_CLEAR,
                                      clear_state)
                    except:
                        gv.logger.error(
                            'Could not modify zone state for board: ' +
                            str(b + 1))
                    cur_state = i2c.i2c_read(i2c.ADDRESS + b, i2c.ZONE_STATE)
                    gv.logger.debug('Zone state for board ' + str(b + 1) +
                                    ' is: ' + hex(cur_state))
                    if cur_state != set_state:
                        gv.logger.critical('Erroneous zone state for board ' +
                                           str(b + 1) + ' read: ' +
                                           hex(cur_state) + ' desired: ' +
                                           hex(set_state))

            gv.remote_zones = {}  # reconstruct from ground 0
            radio_zones = {}
            for z in range(gv.sd['radiost']):
                radio_bd = gv.sd['radio_zones'][z]['radio_bd']
                if radio_bd == '':
                    continue
                if radio_bd not in radio_zones:
                    gv.remote_zones[radio_bd] = 0
                if gv.srvals[gv.sd['nst'] - gv.sd['radiost'] +
                             z] and gv.sd['radio_zones'][z]['zone_pos'] >= 0:
                    gv.remote_zones[
                        radio_bd] |= 1 << gv.sd['radio_zones'][z]['zone_pos']
        else:
            disableShiftRegisterOutput()
            setShiftRegister(
                gv.srvals)  # gv.srvals stores shift register state
            enableShiftRegisterOutput()
        zone_change.send()
Ejemplo n.º 2
0
def set_output():
    """Activate triacs according to shift register state."""

    with gv.rs_lock:
        if gv.use_i2c:
            # todo fix for radio zones
            for b in range((gv.sd['nst']-gv.sd['radiost'])//8):
                if b not in gv.in_bootloader:
                    zone_state = 0
                    for z in range(8):
                        if gv.srvals[b*8+z]:
                            zone_state |= 1<<z
                    try:
                        set_state = zone_state & 0xff
                        clear_state = (~set_state) & 0xff
                        gv.logger.debug('Zone set for board ' + str(b+1) + ' is: ' + hex(set_state))
                        i2c.i2c_write(i2c.ADDRESS+b, i2c.ZONE_SET, set_state)
                        gv.logger.debug('Zone clear for board ' + str(b+1) + ' is: ' + hex(clear_state))
                        i2c.i2c_write(i2c.ADDRESS+b, i2c.ZONE_CLEAR, clear_state)
                    except:
                        gv.logger.error('Could not modify zone state for board: ' + str(b+1))
                    cur_state = i2c.i2c_read(i2c.ADDRESS+b, i2c.ZONE_STATE)
                    gv.logger.debug('Zone state for board ' + str(b+1) + ' is: ' + hex(cur_state))
                    if cur_state != set_state:
                        gv.logger.critical('Erroneous zone state for board ' + str(b+1) + ' read: ' + hex(cur_state) + ' desired: ' + hex(set_state))

            gv.remote_zones = {} # reconstruct from ground 0
            radio_zones = {}
            for z in range(gv.sd['radiost']):
                radio_bd = gv.sd['radio_zones'][z]['radio_bd']
                if radio_bd == '':
                    continue
                if radio_bd not in radio_zones:
                    gv.remote_zones[radio_bd] = 0
                if gv.srvals[gv.sd['nst']-gv.sd['radiost']+z] and gv.sd['radio_zones'][z]['zone_pos'] >= 0:
                    gv.remote_zones[radio_bd] |= 1 << gv.sd['radio_zones'][z]['zone_pos']
        else:
            disableShiftRegisterOutput()
            setShiftRegister(gv.srvals)  # gv.srvals stores shift register state
            enableShiftRegisterOutput()
        zone_change.send()
Ejemplo n.º 3
0
Archivo: sip.py Proyecto: bkoblenz/HVAC
def timing_loop():
    """ ***** Main timing algorithm. Runs in a separate thread.***** """
    last_min = 0
    zc = 0
    supply_temp_readings = []
    return_temp_readings = []
#    last_mode = gv.sd['mode']
    last_mode = 'Invalid Mode' # force intialization
    last_temp_log = 0
    failed_temp_read = 0
    last_dewpoint_adjust = 0

    # Log the image and all the vsb board fw
    try:
        with open('data/version', 'r') as f:
            image_version = f.read()
        gv.logger.info('Image version: ' + image_version)
    except:
        pass
    boards = i2c.get_vsb_boards()
    for board, version in boards.items():
        gv.logger.info('VSB Firmware for board: ' + str(board) + ' value: ' + hex(version))

    for delay in range(15):
        time.sleep(1) # wait for ip addressing to settle but keep updating time
        gv.nowt = time.localtime()   # Current time as time struct.  Updated once per second.
        gv.now = timegm(gv.nowt)   # Current time as timestamp based on local time from the Pi. Updated once per second.

    start_time = gv.now
    check_and_update_upnp()
    last_upnp_refresh = gv.now

    # one_way_cooling_adjustments tracks the direction of the last valve change and how many changes we have made
    # in that direction without going the other direction.  This stops us from constantly moving the valve when the
    # heatpump is off or we cannot achieve our target.
    one_way_cooling_adjustments = 0
    last_ave_supply_temp = None

    # force everything off

    set_output()
    while True:  # infinite loop
        time.sleep(1)
        gv.nowt = time.localtime()   # Current time as time struct.  Updated once per second.
        gv.now = timegm(gv.nowt)   # Current time as timestamp based on local time from the Pi. Updated once per second.
        # perform once per minute processing
        if gv.now // 60 != last_min:  # only check programs once a minute
            last_min = gv.now // 60
            update_radio_present()
            max_bd = -1
            boards = i2c.get_vsb_boards()
            for bd, version in boards.items():
                if bd not in gv.in_bootloader:
                    try:
                        max_bd = max(max_bd, bd)
                        v = i2c_read(i2c.ADDRESS+bd, 0xc) # verify scratch value is as expected.  Acts as a touch of the vsb too!
                        if v != bd+16:
                            gv.logger.critical('Main bad scratch value on board: ' + str(bd) + ' value: ' + str(v))
                            i2c_write(i2c.ADDRESS+bd, 0xc, bd+16) # write scratch register as keepalive
                    except:
                        gv.logger.critical('Cant access scratch register on board: ' + str(bd))
                        pass

                    # read deadman debug register ignoring all errors.
                    try:
                        v = i2c_read(i2c.ADDRESS+bd, 0xd)
                        if v != 0:
                             gv.logger.critical('Deadman register triggered on board: ' + str(bd) + ' value: ' + str(v))
                    except:
                        pass

            cur_bd = (gv.sd['nst']-gv.sd['radiost'])//8
            if max_bd+1 > cur_bd: # ensure at nbrd captures all attached VSMs
                gv.logger.info('Changing nbrd based on attached boards: ' + str(max_bd+1))
                adjust_gv_nbrd(max_bd+1)
                gv.sd['nst'] += 8*(max_bd+1-cur_bd)
                jsave(gv.sd, 'sd')

            cur_ip = get_ip()
            ext_ip_addr = get_external_ip()
            if gv.sd['master'] and (ext_ip_addr != gv.external_ip or cur_ip != gv.last_ip):
                gv.external_ip = ext_ip_addr
                try:
                    # send email if ip addressing changed
                    if gv.sd['teipchange'] and gv.plugin_data['te']['tesender']:
                        subject = "Report from Irricloud"
                        body = 'IP change.  Local IP: ' + cur_ip
                        if gv.sd['htp'] != 0 and gv.sd['htp'] != 80:
                            body += ':' + str(gv.sd['htp'])
                        body += ' External IP: ' + ext_ip_addr
                        if gv.sd['external_htp'] != 0:
                            body += ':' + str(gv.sd['external_htp'])
                        gv.plugin_data['te']['tesender'].try_mail(subject, body)
                except:
                    pass

            if cur_ip != gv.last_ip:
                gv.logger.info('IP changed from: ' + gv.last_ip + ' to: ' + cur_ip)
                gv.last_ip = cur_ip
                if cur_ip != "No IP Settings":
                    # find router, set up upnp port mapping
                    check_and_update_upnp(cur_ip)
                    last_upnp_refresh = gv.now

            if gv.sd['upnp_refresh_rate'] > 0 and \
                   (gv.now-last_upnp_refresh)//60 >= gv.sd['upnp_refresh_rate']:
                check_and_update_upnp(cur_ip)
                last_upnp_refresh = gv.now

        process_actions()
        last_zc = zc
        zc = read_sensor_value('zone_call')
        if zc == None:
            zc = last_zc
            log_event('Failed to read zone_call')
        boiler_md = get_boiler_mode()
        heatpump_md = get_heatpump_mode()
        if gv.sd['mode'] != last_mode: # turn everything off
            log_event('change mode.  Turn off boiler, heatpump, circ_pump')
            if boiler_md != 'none':
                set_boiler_mode('none')
            if heatpump_md != 'none':
                set_heatpump_mode('none')
            gv.srvals[circ_pump] = 0
            set_output()
            last_zc = 0 # mark as was off
            last_mode = gv.sd['mode']
            remove_action({'what':'set_valve_change'})
            if gv.sd['mode'] in ['Heatpump Cooling'] or \
                   (gv.sd['mode'] in ['Boiler Only'] and not boiler_through_buffer_tank): # use only return water
                insert_action(gv.now, {'what':'set_valve_change', 'valve_change_percent':-100})
            else:
                insert_action(gv.now, {'what':'set_valve_change', 'valve_change_percent':100})

        try:
            temps = read_temps()
            failed_temp_read = 0
            # rather than tracking serial # of thermistors, just assume higher readings are supply
            # and cooler readings are return (if heating) and vice versa if cooling
            min_temp = min(temps)
            max_temp = max(temps)
            if min_temp < 0 or max_temp < 0:
                log_event('Bad min/max temps.  min: ' + str(min_temp) + ' max: ' + str(max_temp))
            if gv.sd['mode'] == 'Heatpump Cooling':
                supply_temp_readings.append(min_temp)
                return_temp_readings.append(max_temp)
            else:
                supply_temp_readings.append(max_temp)
                return_temp_readings.append(min_temp)
        except:
            if gv.now - start_time > 120: # let things start up before capturing errors
                failed_temp_read += 1
                if failed_temp_read < 300:
                    if failed_temp_read % 10 == 1: # first exception should get cleared by reconnect and is normal
                        log_event('cant read temperatures.  Failcount: ' + str(failed_temp_read))
                elif failed_temp_read == 300:
                    log_event('TEMPERATURE SENSOR FAILURE')
                    email('Heating', 'TEMPERATURE SENSOR FAILURE')
                elif failed_temp_read % 300 == 0:
                    log_event('Ongoing temp failure.  Failcount: ' + str(failed_temp_read))

        if len(supply_temp_readings) > 5:
            supply_temp_readings.pop(0)
        if len(return_temp_readings) > 5:
            return_temp_readings.pop(0)
        try:
            ave_supply_temp = sum(supply_temp_readings)/float(len(supply_temp_readings))
            if ave_supply_temp < 0:
                log_event('Bad ave_supply_temp: ' + str(ave_supply_temp))
        except ZeroDivisionError:
            ave_supply_temp = -1
        try:
            ave_return_temp = sum(return_temp_readings)/float(len(return_temp_readings))
            if ave_return_temp < 0:
                log_event('Bad ave_return_temp: ' + str(ave_return_temp))
        except ZeroDivisionError:
            ave_return_temp = -1

        if gv.now - last_temp_log >= 600:
            last_temp_log = gv.now
            ast_c = ave_supply_temp
            ast_f = ast_c*1.8 + 32
            art_c = ave_return_temp
            art_f = art_c*1.8 + 32
            dew_f = dew*1.8 + 32
            log_event('supply temp: ' + "{0:.2f}".format(ast_c) + 'C ' + "{0:.2f}".format(ast_f) + 'F' + '; ' + \
                      'return temp: ' + "{0:.2f}".format(art_c) + 'C ' + "{0:.2f}".format(art_f) + 'F' + '; ' + \
                      'dewpoint: ' + "{0:.2f}".format(dew) + 'C ' + "{0:.2f}".format(dew_f) + 'F')

        if zc != last_zc: # change in zone call
            if gv.sd['mode'] == 'None':
                zc = last_zc # dont do anything in terms of moving water
            elif last_zc == 0: # was off, now on?
                supply_temp_readings = []
                return_temp_readings = []
                last_ave_supply_temp = None
                log_event('zone call on; enable circ pump')
                gv.srvals[circ_pump] = 1
                set_output()
                # for cooling or boiler operation start with only return water.  For heating, only buffer tank water
                if gv.sd['mode'] in ['Heatpump Cooling']:
                    remove_action({'what':'set_valve_change'})
                    insert_action(gv.now, {'what':'set_valve_change', 'valve_change_percent':-100})
                    one_way_cooling_adjustments = 0
                elif not boiler_through_buffer_tank:
                    remove_action({'what':'set_valve_change'})
                    if gv.sd['mode'] in ['Boiler Only']:
                        insert_action(gv.now, {'what':'set_valve_change', 'valve_change_percent':-100})
                    else:
                        insert_action(gv.now, {'what':'set_valve_change', 'valve_change_percent':100})
 
                if gv.sd['mode'] in ['Boiler Only', 'Boiler and Heatpump']:
                    log_event('zone call on; enable boiler')
                    set_boiler_mode('heating')
            else: # was on, now off
                msg_start = 'zone call off; '
                gv.srvals[circ_pump] = 0
                set_output()
                if boiler_md == 'heating' and \
                        gv.sd['mode'] in ['Boiler Only', 'Boiler and Heatpump', 'Heatpump then Boiler']:
                    msg_start += 'disable boiler; '
                    set_boiler_mode('none')
                if heatpump_md == 'heating' and \
                        gv.sd['mode'] in ['Boiler and Heatpump', 'Heatpump then Boiler', 'Heatpump Only']:
                    msg_start += 'disable heatpump; '
                    set_heatpump_mode('none')
                if heatpump_md == 'cooling' and gv.sd['mode'] == 'Heatpump Cooling':
                    msg_start += 'disable heatpump; '
                    set_heatpump_mode('none')
                log_event(msg_start + 'supply: ' + "{0:.2f}".format(ave_supply_temp) + ' return: ' + "{0:.2f}".format(ave_return_temp))
        elif zc == 1: # still on?
            if len(supply_temp_readings) < 5 or len(return_temp_readings) < 5:
                continue
            if gv.sd['mode'] in ['Heatpump Only', 'Boiler and Heatpump', 'Heatpump then Boiler']:
                if ave_supply_temp < heatpump_setpoint_h-5:
                    if heatpump_md == 'none' and gv.now-last_heatpump_off > 3*60:
#                        log_event('reenable heatpump; supply: ' + str(ave_supply_temp))
                        set_heatpump_mode('heating')
                if ave_supply_temp > heatpump_setpoint_h-2:
                    if heatpump_md == 'heating' and gv.now-last_heatpump_on > 3*60:
#                        log_event('disable heatpump; supply: ' + str(ave_supply_temp))
                        set_heatpump_mode('none')
            if gv.sd['mode'] == 'Heatpump then Boiler':
                if ave_supply_temp < heatpump_setpoint_h-13 or ave_return_temp < 33:
                    if boiler_md == 'none' and gv.now-last_boiler_off > 2*60 and \
                             gv.now-last_heatpump_on > 3*60:
                        log_event('reenable boiler; supply: ' + "{0:.2f}".format(ave_supply_temp) + ' return: ' + "{0:.2f}".format(ave_return_temp))
                        # Use only boiler for a while
                        if not boiler_through_buffer_tank:
                            remove_action({'what':'set_valve_change'})
                            insert_action(gv.now, {'what':'set_valve_change', 'valve_change_percent':-100})
                        set_heatpump_mode('none')
                        set_boiler_mode('heating')
                        insert_action(gv.now+45*60, {'what':'set_boiler_mode', 'mode':'none'})
            if gv.sd['mode'] == 'Heatpump Cooling' and gv.now-last_dewpoint_adjust >= 60:
                 dewpoint_margin = 1.5
                 target = max(dew+dewpoint_margin+1, 10.)
                 adjust = 0
                 if ave_supply_temp <= dew+dewpoint_margin and one_way_cooling_adjustments > min_cooling_adjustments:
                     remove_action({'what':'set_valve_change'})
                     insert_action(gv.now, {'what':'set_valve_change', 'valve_change_percent':-100})
                     msg = 'Close valve; avoid condensation'
                     log_event(msg + ': ' + "{0:.2f}".format(dew) + 'C target: ' + "{0:.2f}".format(target) + 'C supply: ' + "{0:.2f}".format(ave_supply_temp) + 'C')
                     last_dewpoint_adjust = gv.now
                     last_ave_supply_temp = None
                     one_way_cooling_adjustments = min_cooling_adjustments
                 elif target < ave_supply_temp - .1:
                     remove_action({'what':'set_valve_change'})
                     adjust = 0
                     if one_way_cooling_adjustments < 0:
                         one_way_cooling_adjustments = 0
                         adjust += 2
                     adjust += cooling_adjust_per_degree * (ave_supply_temp - target)
                     if last_ave_supply_temp != None and last_ave_supply_temp - ave_supply_temp > 0 and \
                            gv.now-last_dewpoint_adjust <= 180: # already going down?  Be patient
                         new_adjust = adjust - 2*cooling_adjust_per_degree * (last_ave_supply_temp - ave_supply_temp)
                         msg = 'already going down'
                         gv.logger.debug(msg + ': ' + \
                                         ' adjust: ' + "{0:.2f}".format(adjust) + \
                                         ' new_adjust: ' + "{0:.2f}".format(new_adjust))
                         adjust = max(0, new_adjust)
                     adjust = int(round(adjust))
                     msg = 'Ignoring request for more buffer tank water'
                     if adjust > 0 and one_way_cooling_adjustments < max_cooling_adjustments:
                         insert_action(gv.now, {'what':'set_valve_change', 'valve_change_percent':adjust})
                         last_dewpoint_adjust = gv.now
                         last_ave_supply_temp = ave_supply_temp
                         msg = 'More buffer tank water'
                 elif target > ave_supply_temp + .1:
                     remove_action({'what':'set_valve_change'})
                     adjust = 0
                     if one_way_cooling_adjustments > 0:
                         one_way_cooling_adjustments = 0
                         adjust -= 2
                     adjust += cooling_adjust_per_degree * (ave_supply_temp - target)
                     if last_ave_supply_temp != None and last_ave_supply_temp - ave_supply_temp < 0 and \
                            gv.now-last_dewpoint_adjust <= 180: # already going up?  Be patient
                         new_adjust = adjust - 2*cooling_adjust_per_degree * (last_ave_supply_temp - ave_supply_temp)
                         msg = 'already going up'
                         gv.logger.debug(msg + ': ' + \
                                         ' adjust: ' + "{0:.2f}".format(adjust) + \
                                         ' new_adjust: ' + "{0:.2f}".format(new_adjust))
                         adjust = min(0, new_adjust)
                     adjust = int(round(adjust))
                     msg = 'Ignoring request for more return water'
                     if adjust < 0 and one_way_cooling_adjustments > min_cooling_adjustments:
                         insert_action(gv.now, {'what':'set_valve_change', 'valve_change_percent':adjust})
                         last_dewpoint_adjust = gv.now
                         last_ave_supply_temp = ave_supply_temp
                         msg = 'More return water'
                 else:
                     msg = 'Not changing valve'
                 gv.logger.debug(msg + ': ' + str(one_way_cooling_adjustments) + \
                                 ' dew: ' + "{0:.2f}".format(dew) + 'C' + \
                                 ' target: ' + "{0:.2f}".format(target) + 'C' + \
                                 ' supply: ' + "{0:.2f}".format(ave_supply_temp) + 'C')
                 if (adjust > 0 and one_way_cooling_adjustments < max_cooling_adjustments) or \
                        (adjust < 0 and one_way_cooling_adjustments > min_cooling_adjustments):
                     one_way_cooling_adjustments += adjust
Ejemplo n.º 4
0
def timing_loop():
    """ ***** Main timing algorithm. Runs in a separate thread.***** """
    last_min = 0
    zc = 0
    supply_temp_readings = []
    return_temp_readings = []
    #    last_mode = gv.sd['mode']
    last_mode = 'Invalid Mode'  # force intialization
    last_temp_log = 0
    failed_temp_read = 0
    failed_cold_supply = 0
    last_dewpoint_adjust = 0

    # Log the image and all the vsb board fw
    try:
        with open('data/version', 'r') as f:
            image_version = f.read()
        gv.logger.info('Image version: ' + image_version)
    except:
        pass
    boards = i2c.get_vsb_boards()
    for board, version in boards.items():
        gv.logger.info('VSB Firmware for board: ' + str(board) + ' value: ' +
                       hex(version))

    for delay in range(15):
        time.sleep(
            1)  # wait for ip addressing to settle but keep updating time
        gv.nowt = time.localtime(
        )  # Current time as time struct.  Updated once per second.
        gv.now = timegm(
            gv.nowt
        )  # Current time as timestamp based on local time from the Pi. Updated once per second.

    start_time = gv.now
    check_and_update_upnp()
    last_upnp_refresh = gv.now

    # one_way_cooling_adjustments tracks the direction of the last valve change and how many changes we have made
    # in that direction without going the other direction.  This stops us from constantly moving the valve when the
    # heatpump is off or we cannot achieve our target.
    one_way_cooling_adjustments = 0
    last_ave_supply_temp = None

    # force everything off

    set_output()
    while True:  # infinite loop
        try:
            time.sleep(1)
            gv.nowt = time.localtime(
            )  # Current time as time struct.  Updated once per second.
            gv.now = timegm(
                gv.nowt
            )  # Current time as timestamp based on local time from the Pi. Updated once per second.
            # perform once per minute processing
            if gv.now // 60 != last_min:  # only check programs once a minute
                boiler_supply_crossover = gv.sd['boiler_supply_temp'] if gv.sd[
                    'tu'] == 'C' else (gv.sd['boiler_supply_temp'] - 32) / 1.8
                last_min = gv.now // 60
                update_radio_present()
                max_bd = -1
                boards = i2c.get_vsb_boards()
                for bd, version in boards.items():
                    if bd not in gv.in_bootloader:
                        try:
                            max_bd = max(max_bd, bd)
                            v = i2c_read(
                                i2c.ADDRESS + bd, 0xc
                            )  # verify scratch value is as expected.  Acts as a touch of the vsb too!
                            if v != bd + 16:
                                gv.logger.critical(
                                    'Main bad scratch value on board: ' +
                                    str(bd) + ' value: ' + str(v))
                                i2c_write(
                                    i2c.ADDRESS + bd, 0xc, bd +
                                    16)  # write scratch register as keepalive
                        except:
                            gv.logger.critical(
                                'Cant access scratch register on board: ' +
                                str(bd))
                            pass

                        # read deadman debug register ignoring all errors.
                        try:
                            v = i2c_read(i2c.ADDRESS + bd, 0xd)
                            if v != 0:
                                gv.logger.critical(
                                    'Deadman register triggered on board: ' +
                                    str(bd) + ' value: ' + str(v))
                        except:
                            pass

                cur_bd = (gv.sd['nst'] - gv.sd['radiost']) // 8
                if max_bd + 1 > cur_bd:  # ensure at nbrd captures all attached VSMs
                    gv.logger.info('Changing nbrd based on attached boards: ' +
                                   str(max_bd + 1))
                    adjust_gv_nbrd(max_bd + 1)
                    gv.sd['nst'] += 8 * (max_bd + 1 - cur_bd)
                    jsave(gv.sd, 'sd')

                cur_ip = get_ip()
                ext_ip_addr = get_external_ip()
                if gv.sd['master'] and (ext_ip_addr != gv.external_ip
                                        or cur_ip != gv.last_ip):
                    gv.external_ip = ext_ip_addr
                    try:
                        # send email if ip addressing changed
                        if gv.sd['teipchange'] and gv.plugin_data['te'][
                                'tesender']:
                            subject = "Report from Irricloud"
                            body = 'IP change.  Local IP: ' + cur_ip
                            if gv.sd['htp'] != 0 and gv.sd['htp'] != 80:
                                body += ':' + str(gv.sd['htp'])
                            body += ' External IP: ' + ext_ip_addr
                            if gv.sd['external_htp'] != 0:
                                body += ':' + str(gv.sd['external_htp'])
                            gv.plugin_data['te']['tesender'].try_mail(
                                subject, body)
                    except:
                        log_event('ip change email send failed')

                if cur_ip != gv.last_ip:
                    gv.logger.info('IP changed from: ' + gv.last_ip + ' to: ' +
                                   cur_ip)
                    gv.last_ip = cur_ip
                    if cur_ip != "No IP Settings":
                        # find router, set up upnp port mapping
                        check_and_update_upnp(cur_ip)
                        last_upnp_refresh = gv.now

                if gv.sd['upnp_refresh_rate'] > 0 and \
                       (gv.now-last_upnp_refresh)//60 >= gv.sd['upnp_refresh_rate']:
                    check_and_update_upnp(cur_ip)
                    last_upnp_refresh = gv.now

            if gv.now % 60 == 0:
                gv.logger.info('timing_loop 0')
            process_actions()
            last_zc = zc
            zc = read_sensor_value('zone_call')
            if zc == None:
                zc = last_zc
                log_event('Failed to read zone_call')
            boiler_md = get_boiler_mode()
            heatpump_md = get_heatpump_mode()
            if gv.sd['mode'] != last_mode:  # turn everything off
                log_event('change mode.  Turn off boiler, heatpump, circ_pump')
                if boiler_md != 'none':
                    set_boiler_mode('none')
                if heatpump_md != 'none':
                    set_heatpump_mode('none')
                gv.srvals[circ_pump] = 0
                set_output()
                last_zc = 0  # mark as was off
                last_mode = gv.sd['mode']
                remove_action({'what': 'set_valve_change'})
                if gv.sd['mode'] in ['Heatpump Cooling'] or \
                       (gv.sd['mode'] in ['Boiler Only'] and not boiler_through_buffer_tank): # use only return water
                    insert_action(gv.now, {
                        'what': 'set_valve_change',
                        'valve_change_percent': -100
                    })
                else:
                    insert_action(gv.now, {
                        'what': 'set_valve_change',
                        'valve_change_percent': 100
                    })

            try:
                temps = read_temps()
                failed_temp_read = 0
                # rather than tracking serial # of thermistors, just assume higher readings are supply
                # and cooler readings are return (if heating) and vice versa if cooling
                min_temp = min(temps)
                max_temp = max(temps)
                if min_temp < 0 or max_temp < 0:
                    log_event('Bad min/max temps.  min: ' + str(min_temp) +
                              ' max: ' + str(max_temp))
                if gv.sd['mode'] == 'Heatpump Cooling':
                    supply_temp_readings.append(min_temp)
                    return_temp_readings.append(max_temp)
                else:
                    supply_temp_readings.append(max_temp)
                    return_temp_readings.append(min_temp)
            except:
                if gv.now - start_time > 120:  # let things start up before capturing errors
                    failed_temp_read += 1
                    if failed_temp_read < 300:
                        if failed_temp_read % 10 == 1:  # first exception should get cleared by reconnect and is normal
                            log_event('cant read temperatures.  Failcount: ' +
                                      str(failed_temp_read))
                    elif failed_temp_read == 300:
                        log_event('TEMPERATURE SENSOR FAILURE')
                        try:
                            gv.plugin_data['te']['tesender'].try_mail(
                                'Heating', 'TEMPERATURE SENSOR FAILURE')
                        except:
                            log_event('temp sensor failure email send failed')
                    elif failed_temp_read % 300 == 0:
                        log_event('Ongoing temp failure.  Failcount: ' +
                                  str(failed_temp_read))

            if len(supply_temp_readings) > 5:
                supply_temp_readings.pop(0)
            if len(return_temp_readings) > 5:
                return_temp_readings.pop(0)
            try:
                ave_supply_temp = sum(supply_temp_readings) / float(
                    len(supply_temp_readings))
                if ave_supply_temp < 0:
                    log_event('Bad ave_supply_temp: ' + str(ave_supply_temp))
            except ZeroDivisionError:
                ave_supply_temp = -1
            try:
                ave_return_temp = sum(return_temp_readings) / float(
                    len(return_temp_readings))
                if ave_return_temp < 0:
                    log_event('Bad ave_return_temp: ' + str(ave_return_temp))
            except ZeroDivisionError:
                ave_return_temp = -1

            if gv.now - last_temp_log >= 600:
                last_temp_log = gv.now
                ast_c = ave_supply_temp
                ast_f = ast_c * 1.8 + 32
                art_c = ave_return_temp
                art_f = art_c * 1.8 + 32
                dew_f = dew * 1.8 + 32
                log_event('supply temp: ' + "{0:.2f}".format(ast_c) + 'C ' + "{0:.2f}".format(ast_f) + 'F' + '; ' + \
                          'return temp: ' + "{0:.2f}".format(art_c) + 'C ' + "{0:.2f}".format(art_f) + 'F' + '; ' + \
                          'dewpoint: ' + "{0:.2f}".format(dew) + 'C ' + "{0:.2f}".format(dew_f) + 'F')

            if zc != last_zc:  # change in zone call
                if gv.sd['mode'] == 'None':
                    zc = last_zc  # dont do anything in terms of moving water
                elif last_zc == 0:  # was off, now on?
                    supply_temp_readings = []
                    return_temp_readings = []
                    last_ave_supply_temp = None
                    low_supply_count = 0
                    log_event('zone call on; enable circ pump')
                    gv.srvals[circ_pump] = 1
                    set_output()
                    # for cooling or boiler operation start with only return water.  For heating, only buffer tank water
                    if gv.sd['mode'] in ['Heatpump Cooling']:
                        remove_action({'what': 'set_valve_change'})
                        insert_action(
                            gv.now, {
                                'what': 'set_valve_change',
                                'valve_change_percent': -100
                            })
                        one_way_cooling_adjustments = 0
                    elif not boiler_through_buffer_tank:
                        remove_action({'what': 'set_valve_change'})
                        if gv.sd['mode'] in ['Boiler Only']:
                            insert_action(
                                gv.now, {
                                    'what': 'set_valve_change',
                                    'valve_change_percent': -100
                                })
                        else:
                            insert_action(
                                gv.now, {
                                    'what': 'set_valve_change',
                                    'valve_change_percent': 100
                                })

                    if gv.sd['mode'] in ['Boiler Only', 'Boiler and Heatpump']:
                        log_event('zone call on; enable boiler')
                        set_boiler_mode('heating')
                else:  # was on, now off
                    msg_start = 'zone call off; '
                    gv.srvals[circ_pump] = 0
                    set_output()
                    if boiler_md == 'heating' and \
                            gv.sd['mode'] in ['Boiler Only', 'Boiler and Heatpump', 'Heatpump then Boiler']:
                        msg_start += 'disable boiler; '
                        set_boiler_mode('none')
                    if heatpump_md == 'heating' and \
                            gv.sd['mode'] in ['Boiler and Heatpump', 'Heatpump then Boiler', 'Heatpump Only']:
                        msg_start += 'disable heatpump; '
                        set_heatpump_mode('none')
                    if heatpump_md == 'cooling' and gv.sd[
                            'mode'] == 'Heatpump Cooling':
                        msg_start += 'disable heatpump; '
                        set_heatpump_mode('none')
                    log_event(msg_start + 'supply: ' +
                              "{0:.2f}".format(ave_supply_temp) + ' return: ' +
                              "{0:.2f}".format(ave_return_temp))
            elif zc == 1:  # still on?
                if len(supply_temp_readings) < 5 or len(
                        return_temp_readings) < 5:
                    continue
                if gv.sd['mode'] in [
                        'Heatpump Only', 'Boiler and Heatpump',
                        'Heatpump then Boiler'
                ]:
                    if ave_supply_temp < heatpump_setpoint_h - 8:
                        if heatpump_md == 'none' and gv.now - last_heatpump_off > 3 * 60:
                            #                        log_event('reenable heatpump; supply: ' + str(ave_supply_temp))
                            set_heatpump_mode('heating')
                    if ave_supply_temp > heatpump_setpoint_h - 4:
                        if heatpump_md == 'heating' and gv.now - last_heatpump_on > 3 * 60:
                            #                        log_event('disable heatpump; supply: ' + str(ave_supply_temp))
                            set_heatpump_mode('none')
                if gv.sd['mode'] == 'Heatpump then Boiler':
                    #                if ave_supply_temp < heatpump_setpoint_h-13 or ave_return_temp < 32:
                    if ave_supply_temp < boiler_supply_crossover:
                        if low_supply_count <= 750:  # about 12.5 mins.
                            # Typically takes 300-450 seconds from low point to reach ok, and once starts trending up stays trending uo
                            if low_supply_count % 150 == 0:
                                log_event('low_supply: ' +
                                          str(low_supply_count) + ' supply: ' +
                                          "{0:.2f}".format(ave_supply_temp) +
                                          ' return: ' +
                                          "{0:.2f}".format(ave_return_temp))
                            low_supply_count += 1
                        elif boiler_md == 'none' and gv.now-last_boiler_off > 2*60 and \
                                 gv.now-last_heatpump_on > 3*60:
                            log_event('reenable boiler; supply: ' +
                                      "{0:.2f}".format(ave_supply_temp) +
                                      ' return: ' +
                                      "{0:.2f}".format(ave_return_temp))
                            # Use only boiler for a while
                            if not boiler_through_buffer_tank:
                                remove_action({'what': 'set_valve_change'})
                                insert_action(
                                    gv.now, {
                                        'what': 'set_valve_change',
                                        'valve_change_percent': -100
                                    })
                            # try an hour to warm things up and allow defrost mode on hp to finish and rewarm tank
                            last_on_min_gap = (gv.now - last_boiler_on) // 60
                            #                        gv.logger.info('last_on_min_gap: ' + str(last_on_min_gap) + ' now: ' + str(gv.now) + ' on: ' + str(last_boiler_on))
                            extra_min = 0 if last_on_min_gap >= 4 * 60 else 15 if last_on_min_gap >= 3 * 60 else 30
                            set_heatpump_mode('none')
                            set_boiler_mode('heating')
                            insert_action(gv.now + (extra_min + 59) * 60, {
                                'what': 'set_boiler_mode',
                                'mode': 'none'
                            })
                    else:
                        low_supply_count = 0
                if gv.sd[
                        'mode'] == 'Heatpump Cooling' and gv.now - last_dewpoint_adjust >= 60:
                    dewpoint_margin = 1.5
                    target = max(dew + dewpoint_margin + 1, 10.)
                    adjust = 0
                    if ave_supply_temp >= 20:
                        failed_cold_supply += 1
                        if failed_cold_supply == 300:
                            log_event('COLD SUPPLY WATER FAILURE')
                            try:
                                gv.plugin_data['te']['tesender'].try_mail(
                                    'Cooling', 'COLD SUPPLY WATER FAILURE')
                            except:
                                log_event(
                                    'cold supply water failure email send failed'
                                )
                        elif failed_cold_supply % 300 == 0:
                            log_event(
                                'Ongoing cold supply water failure.  Failcount: '
                                + str(failed_cold_supply))
                    else:
                        failed_cold_supply = 0
                    if ave_supply_temp <= dew + dewpoint_margin and one_way_cooling_adjustments > min_cooling_adjustments:
                        remove_action({'what': 'set_valve_change'})
                        insert_action(
                            gv.now, {
                                'what': 'set_valve_change',
                                'valve_change_percent': -100
                            })
                        msg = 'Close valve; avoid condensation'
                        log_event(msg + ': ' + "{0:.2f}".format(dew) +
                                  'C target: ' + "{0:.2f}".format(target) +
                                  'C supply: ' +
                                  "{0:.2f}".format(ave_supply_temp) + 'C')
                        last_dewpoint_adjust = gv.now
                        last_ave_supply_temp = None
                        one_way_cooling_adjustments = min_cooling_adjustments
                    elif target < ave_supply_temp - .1:
                        remove_action({'what': 'set_valve_change'})
                        adjust = 0
                        if one_way_cooling_adjustments < 0:
                            one_way_cooling_adjustments = 0
                            adjust += 2
                        adjust += cooling_adjust_per_degree * (
                            ave_supply_temp - target)
                        if last_ave_supply_temp != None and last_ave_supply_temp - ave_supply_temp > 0 and \
                               gv.now-last_dewpoint_adjust <= 180: # already going down?  Be patient
                            new_adjust = adjust - 2 * cooling_adjust_per_degree * (
                                last_ave_supply_temp - ave_supply_temp)
                            msg = 'already going down'
                            gv.logger.debug(msg + ': ' + \
                                            ' adjust: ' + "{0:.2f}".format(adjust) + \
                                            ' new_adjust: ' + "{0:.2f}".format(new_adjust))
                            adjust = max(0, new_adjust)
                        adjust = int(round(adjust))
                        msg = 'Ignoring request for more buffer tank water'
                        if adjust > 0 and one_way_cooling_adjustments < max_cooling_adjustments:
                            insert_action(
                                gv.now, {
                                    'what': 'set_valve_change',
                                    'valve_change_percent': adjust
                                })
                            last_dewpoint_adjust = gv.now
                            last_ave_supply_temp = ave_supply_temp
                            msg = 'More buffer tank water'
                    elif target > ave_supply_temp + .1:
                        remove_action({'what': 'set_valve_change'})
                        adjust = 0
                        if one_way_cooling_adjustments > 0:
                            one_way_cooling_adjustments = 0
                            adjust -= 2
                        adjust += cooling_adjust_per_degree * (
                            ave_supply_temp - target)
                        if last_ave_supply_temp != None and last_ave_supply_temp - ave_supply_temp < 0 and \
                               gv.now-last_dewpoint_adjust <= 180: # already going up?  Be patient
                            new_adjust = adjust - 2 * cooling_adjust_per_degree * (
                                last_ave_supply_temp - ave_supply_temp)
                            msg = 'already going up'
                            gv.logger.debug(msg + ': ' + \
                                            ' adjust: ' + "{0:.2f}".format(adjust) + \
                                            ' new_adjust: ' + "{0:.2f}".format(new_adjust))
                            adjust = min(0, new_adjust)
                        adjust = int(round(adjust))
                        msg = 'Ignoring request for more return water'
                        if adjust < 0 and one_way_cooling_adjustments > min_cooling_adjustments:
                            insert_action(
                                gv.now, {
                                    'what': 'set_valve_change',
                                    'valve_change_percent': adjust
                                })
                            last_dewpoint_adjust = gv.now
                            last_ave_supply_temp = ave_supply_temp
                            msg = 'More return water'
                    else:
                        msg = 'Not changing valve'
                    gv.logger.debug(msg + ': ' + str(one_way_cooling_adjustments) + \
                                    ' dew: ' + "{0:.2f}".format(dew) + 'C' + \
                                    ' target: ' + "{0:.2f}".format(target) + 'C' + \
                                    ' supply: ' + "{0:.2f}".format(ave_supply_temp) + 'C')
                    if (adjust > 0 and one_way_cooling_adjustments < max_cooling_adjustments) or \
                           (adjust < 0 and one_way_cooling_adjustments > min_cooling_adjustments):
                        one_way_cooling_adjustments += adjust

        except:
            gv.logger.exception('BUG')
Ejemplo n.º 5
0
def timing_loop():
    """ ***** Main timing algorithm. Runs in a separate thread.***** """
    last_min = 0
    last_day = 0
    last_master_station_running = 0
    master_turn_on = 0
    mton_delay = 0
    gv.sd['bsy'] = 0

    check_and_update_upnp()
    last_upnp_refresh = gv.now

    # Log the image and all the vsb board fw
    try:
        with open('data/version', 'r') as f:
            image_version = f.read()
        gv.logger.info('Image version: ' + image_version)
    except:
        pass
    boards = i2c.get_vsb_boards()
    for board, version in boards.items():
        gv.logger.info('VSB Firmware for board: ' + str(board) + ' value: ' + hex(version))

    for delay in range(15):
        time.sleep(1) # wait for ip addressing to settle but keep updating time
        gv.nowt = time.localtime()   # Current time as time struct.  Updated once per second.
        gv.now = timegm(gv.nowt)   # Current time as timestamp based on local time from the Pi. Updated once per second.

    for i, p in enumerate(gv.pd):  # get future instances of programs that recur but started earlier in the day
        if prog_match(p, True):
            schedule_recurring_instances(i)

    ip_countdown = 2
    while True:  # infinite loop
        gv.nowt = time.localtime()   # Current time as time struct.  Updated once per second.
        gv.now = timegm(gv.nowt)   # Current time as timestamp based on local time from the Pi. Updated once per second.
        if gv.sd['en'] and not gv.sd['mm']:
            if gv.now // 60 != last_min:  # only check programs once a minute
                last_min = gv.now // 60
                update_radio_present()
                max_bd = -1
                boards = i2c.get_vsb_boards()
                for bd, version in boards.items():
                    if bd not in gv.in_bootloader:
                        try:
                            max_bd = max(max_bd, bd)
                            v = i2c_read(i2c.ADDRESS+bd, 0xc) # verify scratch value is as expected.  Acts as a touch of the vsb too!
                            if v != bd+16:
                                gv.logger.critical('Main bad scratch value on board: ' + str(bd) + ' value: ' + str(v))
                                i2c_write(i2c.ADDRESS+bd, 0xc, bd+16) # write scratch register as keepalive
                        except:
                            gv.logger.critical('Cant access scratch register on board: ' + str(bd))
                            pass

                        # read deadman debug register ignoring all errors.
                        try:
                            v = i2c_read(i2c.ADDRESS+bd, 0xd)
                            if v != 0:
                                 gv.logger.critical('Deadman register triggered on board: ' + str(bd) + ' value: ' + str(v))
                        except:
                            pass

                cur_bd = (gv.sd['nst']-gv.sd['radiost'])//8
                if max_bd+1 > cur_bd: # ensure at nbrd captures all attached VSMs
                    gv.logger.info('Changing nbrd based on attached boards: ' + str(max_bd+1))
                    adjust_gv_nbrd(max_bd+1)
                    gv.sd['nst'] += 8*(max_bd+1-cur_bd)
                    jsave(gv.sd, 'sd')

                cur_ip = get_ip()
                ext_ip_addr = get_external_ip()
                ip_countdown = 2 if cur_ip == gv.last_ip else ip_countdown-1
                # when establishing bridge, wlan0 appears to drop then come back....just take a couple of iterations to
                # be sure we have an ip change.
                if ip_countdown == 0:
                    ip_countdown = 2 # reset
                    if gv.sd['master'] and (ext_ip_addr != gv.external_ip or cur_ip != gv.last_ip):
                        gv.external_ip = ext_ip_addr
                        try:
                            # send email if ip addressing changed
                            if gv.sd['teipchange'] and gv.plugin_data['te']['tesender']:
                                subject = "Report from Irricloud"
                                body = 'IP change.  Local IP: ' + cur_ip
                                if gv.sd['htp'] != 0 and gv.sd['htp'] != 80:
                                    body += ':' + str(gv.sd['htp'])
                                body += ' External IP: ' + ext_ip_addr
                                if gv.sd['external_htp'] != 0:
                                    body += ':' + str(gv.sd['external_htp'])
                                gv.plugin_data['te']['tesender'].try_mail(subject, body)
                        except:
                            pass

                    if cur_ip != gv.last_ip:
                        gv.logger.info('IP changed from: ' + gv.last_ip + ' to: ' + cur_ip)
                        gv.last_ip = cur_ip
                        if cur_ip != "No IP Settings":
                            # find router, set up upnp port mapping
                            check_and_update_upnp(cur_ip)
                            last_upnp_refresh = gv.now

                    if gv.sd['upnp_refresh_rate'] > 0 and \
                           (gv.now-last_upnp_refresh)//60 >= gv.sd['upnp_refresh_rate']:
                        check_and_update_upnp(cur_ip)
                        last_upnp_refresh = gv.now

                cur_day = gv.now//86400
                if cur_day != last_day:
                    for i in range(gv.sd['nst']):
                        for j in range(len(gv.rs[i])-1,0,-1):
                            if gv.rs[i][j]['rs_stop_sec'] < gv.now:
                                gv.logger.critical('Left over data on gv.rs['+str(i)+']')
                                del gv.rs[i][j]
                            elif gv.rs[i][j]['rs_stop_sec'] > gv.now + 86400: # indefinite program?
                                gv.rs[i][j]['rs_stop_sec'] += 86400 # keep extending
                        update_rs_order(i)
                    last_day = cur_day

                for i, p in enumerate(gv.pd):  # get both index and prog item
                    if prog_match(p):
                        gv.logger.debug('prog_match for program: ' + p[-1])
                        run_program(i)
                        schedule_recurring_instances(i)

                for e in gv.recur[:]: # sorted by start times of recurring programs to run
                    now_min = (gv.now%86400)//60
                    if e[0] <= now_min:
                        try:
                            gv.logger.debug('recur match for program: ' + gv.pd[e[1]][-1])
                        except:
                            pass
                        run_program(e[1]) # pid
                        del gv.recur[0] # always deleting what is (now) first in list
                    else:
                        break

        if gv.sd['bsy']:
            with gv.rs_lock:
                program_running = False
                masid = gv.sd['mas']-1
                for sid in range(gv.sd['nst']):  # Check each station once a second
                    b = sid >> 3
                    s = sid % 8
                    prog_id = gv.rs[sid][len(gv.rs[sid])-1]['rs_program_id']
                    p = None if prog_id >= len(gv.pd) else gv.pd[prog_id-1]
                        
                    if gv.now >= gv.rs[sid][len(gv.rs[sid])-1]['rs_stop_sec'] and len(gv.rs[sid]) > 1:  # check if time is up
                        stop_station(sid, **{'stop_only_current':1})
                    elif gv.rs[sid][len(gv.rs[sid])-1]['rs_start_sec'] <= gv.now < gv.rs[sid][len(gv.rs[sid])-1]['rs_stop_sec']:
                        if (gv.sbits[b] & (1<<s)) == 0: # not yet displayed?
                            duration = gv.rs[sid][len(gv.rs[sid])-1]['rs_stop_sec'] - gv.now
                            gv.logger.info('start station display sid: ' + str(sid+1) + ' for: ' + to_relative_time(duration))
                            gv.sbits[b] |= 1 << s  # Set display to on
                            gv.ps[sid][0] = prog_id
                            if gv.ps[sid][1] != duration:
                                gv.logger.info('sip_loop ps_set: change ps sid: ' + str(sid+1) + ' from: ' + to_relative_time(gv.ps[sid][1])+ ' to: ' + to_relative_time(duration))
                                gv.ps[sid][1] = duration
                            if p is None or (p[gv.p_flags]&2) == 0:  # if not ban program
                                gv.logger.info('turn on sid: ' + str(sid+1) + ' prog: ' + str(prog_id) + ' (ps set) dur: ' + to_relative_time(duration))
                                gv.srvals[sid] = 1
                                set_output()
                            else: # already stopped what was running in schedule_stations
                                gv.logger.info('turn on ban sid: ' + str(sid+1) + ' prog: ' + str(prog_id) + ' (ps set) dur: ' + to_relative_time(duration))
                            if sid == gv.sd['mas']-1: # when we turn on master, start mton countdown
                                mton_delay = -gv.sd['mton']
                    elif gv.rs[sid][len(gv.rs[sid])-1]['rs_start_sec'] > gv.now and gv.ps[sid][0] == 0:
                        duration = gv.rs[sid][len(gv.rs[sid])-1]['rs_stop_sec'] - gv.rs[sid][len(gv.rs[sid])-1]['rs_start_sec']
                        gv.ps[sid][0] = prog_id
                        if gv.ps[sid][1] != duration:
                            gv.logger.info('sip_loop ps_change: change ps sid: ' + str(sid+1) + ' from: ' + to_relative_time(gv.ps[sid][1]) + ' to: ' + to_relative_time(duration))
                        gv.ps[sid][1] = duration 
                        gv.logger.info('future: ' + to_relative_time(gv.rs[sid][len(gv.rs[sid])-1]['rs_start_sec']) +
                                        ' turn on sid: ' + str(sid+1) + ' prog: ' + str(prog_id) + ' (ps set) dur: ' + to_relative_time(duration))

                    if masid >= 0 and sid != masid and gv.srvals[sid] and gv.sd['mo'][b]&(1<<s):  # Master settings
                        last_master_station_running = gv.now
                    if len(gv.rs[sid]) > 1:  # if any station is scheduled or on
                        program_running = True

                # delays may have screwed up master scheduling.
                # If a station requiring master is running and master is not started, start it.
                if masid >= 0 and gv.srvals[masid] == 0:
                    if master_turn_on == 0 and last_master_station_running == gv.now:
                        master_turn_on = last_master_station_running + gv.sd['mton']
                    if master_turn_on != 0 and master_turn_on <= gv.now:
                        gv.logger.info('turn on master without prescheduling mton: ' + str(master_turn_on))
                        gv.sbits[masid>>3] |= 1 << (masid%8)  # Set display to on
                        gv.ps[masid][0] = 98
                        gv.ps[masid][1] = 0
                        gv.srvals[masid] = 1
                        set_output()
                        master_turn_on = 0

                # If no station requiring master is running and master is not stopped, stop it.
                if masid >= 0 and gv.srvals[masid] == 1:
                    if mton_delay >= 0:
                        mton_delay -= 1
                    if (mton_delay < 0 and # allow for windown where master starts early
                        last_master_station_running < gv.now and
                        ((gv.sd['mtoff'] <= 0 and last_master_station_running >= gv.now + gv.sd['mtoff']) or \
                         (gv.sd['mtoff'] > 0 and last_master_station_running + gv.sd['mtoff'] <= gv.now))):
                        gv.logger.info('turn off master without prescheduling')
                        stop_station(masid, **{'stop_active':1})
                        mton_delay = 0
                    else:
                        program_running = True # give time for master to shut down

                if program_running:
                    # todo check stop_onrain for ban programs
                    if gv.sd['urs'] and gv.sd['rs']:  # Stop stations if use rain sensor and rain detected.
                        stop_onrain()  # Clear schedule for stations that do not ignore rain.
                    for sid in range(len(gv.rs)):  # loop through program schedule (gv.ps)
                        if (gv.sbits[sid>>3] & (1<<(sid%8))) != 0:  # If station is on, decrement time remaining display
                            if gv.ps[sid][1] < 86400: # dont reduce indefinite program
                                if gv.ps[sid][1] == -1:
                                    gv.logger.critical('sip negative ps decrement sid: ' + str(sid+1) + ' to: ' + to_relative_time(gv.ps[sid][1]) + \
                                                    ' start: ' + to_relative_time(gv.rs[sid][len(gv.rs[sid])-1]['rs_start_sec']) + \
                                                    ' stop: '  + to_relative_time(gv.rs[sid][len(gv.rs[sid])-1]['rs_stop_sec']))
                                gv.ps[sid][1] -= 1
                            else:
                                gv.logger.debug('sip_ indefinite program sid: ' + str(sid+1) + ' to: ' + to_relative_time(gv.ps[sid][1]) + \
                                                   ' start: ' + to_relative_time(gv.rs[sid][len(gv.rs[sid])-1]['rs_start_sec']) + \
                                                   ' stop: '  + to_relative_time(gv.rs[sid][len(gv.rs[sid])-1]['rs_stop_sec']))
                                pass

                if not program_running:
                    gv.sd['bsy'] = 0
                    for sid in range(gv.sd['nst']):
                        b = sid >> 3
                        s = sid % 8
                        if gv.srvals[sid]:
                            gv.logger.critical('srval set with no program running sid: ' + str(sid+1))
                        if gv.sbits[b]&(1<<s):
                            gv.logger.critical('sbits set with no program running sid: ' + str(sid+1))
                        if gv.ps[sid][0]:
                            gv.logger.critical('ps[0] set with no program running sid: ' + str(sid+1))
                        if gv.ps[sid][1]:
                            gv.logger.critical('ps[1] set with no program running sid: ' + str(sid+1))

        if gv.sd['urs']:
            check_rain()  # in helpers.py

        if gv.sd['rd'] and gv.now >= gv.sd['rdst']:  # Check of rain delay time is up
            gv.sd['rd'] = 0
            gv.sd['rdst'] = 0  # Rain delay stop time
            jsave(gv.sd, 'sd')

        new_now = timegm(time.localtime())
        if new_now - gv.now == 0: # try to avoid drift
            time.sleep(1)