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()
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()
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
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')
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)