#!/usr/local/bin/php 1 && $argv[1] == 'loop') { $loop = TRUE; } #===================================== # Fetch the list of rooms #===================================== $rooms = fetch_rooms($conn); if (! is_array($rooms) || count($rooms) < 1) { fatal('Unable to fetch any rooms'); } #===================================== # Fetch the list of equipment #===================================== $equips = fetch_equip($conn); if (! is_array($equips) || count($equips) < 1) { fatal('Unable to fetch any equipment', TRUE); } #===================================== # Fetch the list of dampers #===================================== $dampers = fetch_dampers($conn); if (! is_array($dampers) || count($dampers) < 1) { fatal('Unable to fetch any dampers'); } #===================================== # Fetch the list of room damper priorities #===================================== $room_dampers = fetch_room_dampers($conn); if (! is_array($room_dampers) || count($room_dampers) < 1) { fatal('Unable to fetch any room damper priorities'); } #===================================== # Cleanup #===================================== pg_close($conn); unset($conn); while (TRUE) { #===================================== # Reset the sub-command array #===================================== $sub_cmds = array( 'heat' => FALSE, 'cool' => FALSE, 'ice' => FALSE, 'equip_cool' => FALSE, 'switch' => FALSE, 'fan_only' => FALSE, 'after_heat' => FALSE, 'alt_cool' => FALSE, 'passive_cool' => FALSE ); #===================================== # Re-Init DB Connection #===================================== $conn = fetch_params(); #===================================== # Fetch the most recent room temperatures #===================================== $room_temps = fetch_room_temps($conn, $rooms); if (! is_array($room_temps) || count($room_temps) < 1) { fatal('Unable to fetch any room temperatures'); } # Find the average room temperatures $avg_temp = 0; $num_rooms = 0; $bad_rooms = array(); foreach ($rooms as $room) { # But only use valid readings if ($room_temps[$room] >= $params['min_room_temp'] && $room_temps[$room] <= $params['max_room_temp']) { $avg_temp += $room_temps[$room]; $num_rooms++; } else { $bad_rooms[$room] = true; printf("Invalid reading in room: %s: %d\n", $room, $room_temps[$room]); } } $avg_temp /= $num_rooms; unset($num_rooms); if ($DEBUG) { printf("Average temp: %d\n", $avg_temp); } # Sanity check if (count($bad_rooms) / count($rooms) > 2/3) { fatal('Too many bad or missing readings: ' . count($bad_rooms) . '/' . count($rooms)); } #===================================== # Fetch the most recent equipment temperatures #===================================== $equip_temps = fetch_equip_temps($conn, $equips); if (! is_array($equip_temps) || count($equip_temps) < 1) { fatal('Unable to fetch any equipment temperatures', TRUE); } #===================================== # Fetch HVAC status for the last 30 minutes #===================================== $recent_cmd = 'off'; for ($i = 0; $i < 30; $i++) { $hvac[$i] = fetch_hvac_status($conn, time() - (60 * $i)); # Find the most recent non-off/non-fan command if ($recent_cmd == 'off' || $recent_cmd == 'fan') { $recent_cmd = $hvac[$i]['command']; } } # Find the most recent command, period $last_cmd = $hvac[0]['command']; # Simplify mode names if ($recent_cmd == 'alt_cool') { $recent_cmd = 'cool'; } if ($last_cmd == 'alt_cool') { $last_cmd = 'cool'; } if ($DEBUG) { printf('Recent command: ' . $recent_cmd . "\n"); printf('Last command: ' . $last_cmd . "\n"); } # Prevent rapid execution if (strtotime($hvac[1]['ts']) > time() - 60) { fatal('This program can be run no more than once every 60 seconds'); } #===================================== # Fetch the current scheduled mode #===================================== $schedule = fetch_current_period($conn); $mode = $schedule['mode_name']; $mode_data = fetch_mode_data($conn, $mode); #===================================== # Fetch all active overrides #===================================== $override_mode = fetch_active_mode_override($conn); $override_mode = $override_mode['mode_name']; if ($override_mode) { $override_data = fetch_mode_data($conn, $override_mode); if ($DEBUG) { print 'Override mode active: ' . ucwords($override_mode) . "\n"; } } $overrides = fetch_active_room_overrides($conn); $override = array(); foreach ($overrides as $over) { $override[$over['room']] = array( 'min_temp' => $over['min_temp'], 'max_temp' => $over['max_temp']); } #===================================== # Check for proximity-based overrides # Proximity only overrides the scheduled mode, not the override mode (if any) #===================================== $nonproximity_mode = ''; $proximity_device = in_proximity($params['proximity_devices']); if ($proximity_device === FALSE) { $nonproximity_mode = $params['nonproximity_mode']; $nonproximity_data = fetch_mode_data($conn, $nonproximity_mode); if ($DEBUG) { print "Non-proximity mode active: No devices in range.\n"; } } else { if ($DEBUG) { print 'Proximity mode active for device: ' . ucwords($proximity_device) . "\n"; } } #===================================== # Combine schedule, mode, and override data #===================================== $min_temps = array(); $max_temps = array(); foreach ($rooms as $room) { # Base mode $min_temps[$room] = $mode_data[$room]['min_temp']; $max_temps[$room] = $mode_data[$room]['max_temp']; # Provide defaults if there's not active setting if (! $min_temps[$room]) { $min_temps[$room] = $params['default_min_temp']; } if (! $max_temps[$room]) { $max_temps[$room] = $params['default_max_temp']; } # Non-proximity override if ($nonproximity_mode) { $min_temps[$room] = $nonproximity_data[$room]['min_temp']; $max_temps[$room] = $nonproximity_data[$room]['max_temp']; } # Mode override if ($override_mode) { $min_temps[$room] = $override_data[$room]['min_temp']; $max_temps[$room] = $override_data[$room]['max_temp']; } # Room override if ($override[$room]) { $min_temps[$room] = $override[$room]['min_temp']; $max_temps[$room] = $override[$room]['max_temp']; } } #===================================== # Determine primary mode for each room #===================================== $cmd_modes = getRoomModes($room_temps, $max_temps, $min_temps, $last_cmd, 0); #===================================== # Check for other required data #===================================== $ice_equip = $params['ice_equip_name']; if ($equip_temps[$ice_equip] < 1 || $equip_temps[$ice_equip] > 200) { print 'No temperature for equipment: ice' . "\n"; $equip_temps[$ice_equip] = $params['ice_temp'] + 1; } #===================================== # Determine the temperature-based sub-command states #===================================== # Heating if (in_array('heat', $cmd_modes)) { $sub_cmds['heat'] = TRUE; } # Cooling if (in_array('cool', $cmd_modes)) { $sub_cmds['cool'] = TRUE; } # Evaporator De-Icing if ($sub_cmds['cool'] && $equip_temps[$ice_equip] < $params['ice_temp']) { $sub_cmds['ice'] = TRUE; if ($DEBUG) { print 'De-ice triggered by pipe temperature: ' . $equip_temps[$ice_equip] . "\n"; } } # Equipment cooling foreach ($params as $key => $value) { if (preg_match('/^equip_temp-(\w+)$/', $key, $matches)) { if ($equip_temps[$matches[1]] < 1 || $equip_temps[$matches[1]] > 200) { print('ERROR: No temperature for equipment: ' . $matches[1] . "\n"); } else if ($equip_temps[$matches[1]] > $value) { $sub_cmd['equip_cool'] = TRUE; foreach ($rooms as $room) { if ($room == $params['equip_cool_room']) { $cmd_modes[$room] = 'cool'; } else { $cmd_modes[$room] = 'off'; } } if ($DEBUG) { print 'Equip cool triggered by: ' . $matches[1] . "\n"; } } } } # Give fan-only a chance for heating and cooling if ($sub_cmds['heat'] || $sub_cmds['cool']) { $sub_cmds['fan_only'] = TRUE; if ($DEBUG) { print('Considering fan-only' . "\n"); } } # Ensure that the the fan will be sufficient for heating or cooling if ($sub_cmds['fan_only']) { if ($DEBUG) { print('Checking fan-only request' . "\n"); } # Check each room's current and regulation temperature foreach($cmd_modes as $room => $mode) { # Skip bad rooms if ($bad_rooms[$room]) { continue; } # Ensure that no room is outside the hysteresis range if (($mode == 'heat' && ($room_temps[$room] < $min_temps[$room] - $params['hysteresis_heat'])) || (! $sub_cmds['heat'] && $mode == 'cool' && $room_temps[$room] > $max_temps[$room] + $params['hysteresis_cool'])) { $sub_cmds['fan_only'] = FALSE; if ($DEBUG) { print('Fan-only canceled by out-of-range temperature in room: ' . ucwords($room) . "\n"); } break; } # Ensure that air of a sufficient average temperature exists if (($mode == 'heat' && $avg_temp - $params['fan_only_offset'] <= $min_temps[$room]) || (! $sub_cmds['heat'] && $mode == 'cool' && $avg_temp + $params['fan_only_offset'] >= $max_temps[$room])) { $sub_cmds['fan_only'] = FALSE; if ($DEBUG) { print('Fan-only canceled by insufficient average temperature for room: ' . ucwords($room) . "\n"); } break; } } # Cancel fan_only if it has been active for the enitre lookback period $only_fan = TRUE; foreach ($hvac as $hvac_part) { if (! $hvac_part['fan_only'] || $hvac_part['command'] != 'fan') { $only_fan = FALSE; break; } } if ($only_fan) { $sub_cmds['fan_only'] = FALSE; if ($DEBUG) { print('Fan-only canceled by excessive fan_only run time' . "\n"); } } # Cancel fan_only if the HVAC master command has not been off since last time fan_only was active $fan_start = FALSE; $end_end = FALSE; $fan_off = FALSE; # Reverse sort to move forward in time for ($i = count($hvac) - 1; $i >= 0; $i--) { if (! $fan_start && $hvac[$i]['fan_only'] && $hvac[$i]['command'] == 'fan') { $fan_start = TRUE; } else if ($fan_start && ! $fan_end && ! $hvac[$i]['fan_only'] && $hvac[$i]['command'] != 'fan') { $fan_end = TRUE; } if ($fan_end && $hvac[$i]['command'] == 'off') { $fan_off = TRUE; } } if ($fan_end && ! $fan_off) { $sub_cmds['fan_only'] = FALSE; if ($DEBUG) { print('Fan-only cancelation continued until the master command becomes "off"' . "\n"); } } # Require fan-only if any room is outside the under_temp_max/over_temp_max range # This overrides all other fan-only logic, to prevent overheating/overcooling foreach ($room_temps as $room => $temp) { # Skip bad rooms if ($bad_rooms[$room]) { continue; } # Prefer balancing to heating/cooling at extreme temperatures, as an error is likely if (($sub_cmds['heat'] && ($temp > $max_temps[$room] + $params['fan_only_over'])) || (! $sub_cmds['heat'] && ($temp < $min_temps[$room] - $params['fan_only_under']))) { $sub_cmds['fan_only'] = TRUE; if ($DEBUG) { print('Fan-only enforced by out-of-spec temperature for room: ' . ucwords($room) . "\n"); } } } } #===================================== # Determine the initial master command #===================================== # Select modes in this order of preference: equip_cool, fan, heat, cool, off $cmd = 'off'; if ($sub_cmds['equip_cool']) { $cmd = 'cool'; } else if ($sub_cmds['fan_only']) { $cmd = 'fan'; } else if ($sub_cmds['heat']) { $cmd = 'heat'; } else if ($sub_cmds['cool']) { $cmd = 'cool'; } if ($DEBUG) { print('Initial master command: ' . ucwords($cmd) . "\n"); } #===================================== # Apply mode-based sub-commands #===================================== # Run a one-cycle after-heat command if ($cmd != 'heat' && $last_cmd == 'heat') { $sub_cmds['after_heat'] = TRUE; } # Check for rapid switching if ($cmd == 'heat' && $recent_cmd == 'cool') { $sub_cmds['switch'] = TRUE; } else if ($cmd == 'cool' && $recent_cmd == 'heat') { $sub_cmds['switch'] = TRUE; } # Override the initial command as needed if ($sub_cmds['after_heat']) { $cmd = 'fan'; if ($DEBUG) { print("Initial master command canceled for after-heat cooldown\n"); } } else if ($cmd == 'cool' && $sub_cmds['ice']) { $cmd = 'fan'; if ($DEBUG) { print("Initial master command canceled due to evaporator icing\n"); } } else if ($sub_cmds['switch']) { $cmd = 'fan'; if ($DEBUG) { print("Initial master command canceled due to rapid switching\n"); } } #===================================== # Apply time-based override commands #===================================== # At 6 and 12 AM and PM, kill all the relays # This avoids a an error in the relay control software # Allow the after-heat mode to override this command if ($cmd != 'off' && intval(date('g')) % 6 == 0 && intval(date('i')) <= 3) { print('Periodic reset of all relays' . "\n"); $cmd = 'off'; } #===================================== # Determine the damper states #===================================== $open_dampers = 0; $damper_cmd = array(); foreach($dampers as $damper) { $damper_cmd[$damper] = FALSE; } # Open all dampers when running the fan if ($cmd == 'fan') { foreach($dampers as $damper) { $damper_cmd[$damper] = TRUE; $open_dampers++; } } # Open dampers for rooms with invalid sensors foreach ($bad_rooms as $room => $key) { foreach ($room_dampers[$room] as $damper) { $damper_cmd[$damper] = TRUE; $open_dampers++; } } # Loop until enough dampers are open $offset = 0; while ($open_dampers < $params['min_open_dampers'] && $cmd != 'off') { # Re-calculate the room modes with a temperature offset $modeOffset = $offset; if ($cmd == 'heat') { # Heating needs a negative offset $modeOffset *= -1; } $cmd_modes = getRoomModes($room_temps, $max_temps, $min_temps, $last_cmd, $modeOffset); # Open dampers for rooms with modes matching the current master mode foreach ($cmd_modes as $room => $mode) { if ($cmd == $mode) { foreach ($room_dampers[$room] as $damper) { $damper_cmd[$damper] = TRUE; } } } # Re-count the open dampers $open_dampers = 0; foreach ($damper_cmd as $damper) { if ($damper) { $open_dampers++; } } # Loop $offset++; if ($offset > 20) { fatal('Error determining damper states: Range too wide'); } } if ($DEBUG) { print('Dampers:' . "\n"); foreach ($damper_cmd as $name => $damper) { print("\t" . ucwords($name) . ': '); if ($damper) { print('Open' . "\n"); } else { print('Closed' . "\n"); } } } #===================================== # Determine if alternate cooling is sufficent #===================================== if ($cmd == 'cool') { if ($DEBUG) { print('Considering alternate cooling' . "\n"); } $sub_cmds['alt_cool'] = TRUE; # Cancel if we don't know the outdoor temperature $outside_temp = $equip_temps[$params['weather_equip_name']]; if (! strlen($outside_temp)) { $sub_cmds['alt_cool'] = FALSE; if ($DEBUG) { print('Alt-cool canceled by unknown outdoor temperature' . "\n"); } } # Cancel if we don't know the dew point $dew_point = $equip_temps[$params['humidity_equip_name']]; if (! strlen($dew_point)) { $sub_cmds['alt_cool'] = FALSE; if ($DEBUG) { print('Alt-cool canceled by unknown dew point' . "\n"); } } # Cancel if the dew point is too high if ($dew_point > $params['alt_cool_max_dew_point']) { $sub_cmds['alt_cool'] = FALSE; if ($DEBUG) { print('Alt-cool canceled by excessive dew point: ' . $dew_point . "\n"); } } # Ensure the outdoor temperature is low enough for all rooms that require cooling if ($sub_cmds['alt_cool']) { $sub_cmds['passive_cool'] = TRUE; $cmd_modes = getRoomModes($room_temps, $max_temps, $min_temps, $last_cmd, 0); foreach($cmd_modes as $room => $mode) { # Skip non-cooling rooms if ($mode != 'cool') { continue; } # Check the outside air temperature if ($outside_temp >= $max_temps[$room] - $params['alt_cool_passive_offset']) { $sub_cmds['passive_cool'] = FALSE; if ($DEBUG) { print('Passive-cool canceled by insufficient outdoor temperature for room: ' . ucwords($room) . "\n"); } } if ($outside_temp >= $max_temps[$room] - $params['alt_cool_offset']) { $sub_cmds['alt_cool'] = FALSE; if ($DEBUG) { print('Alt-cool canceled by insufficient outdoor temperature for room: ' . ucwords($room) . "\n"); } break; } } } } # Change the master command if cooling is required and passive_cool or alt_cool are allowed if ($cmd == 'cool' && $sub_cmds['alt_cool']) { if ($sub_cmds['passive_cool']) { $cmd = 'off'; } else { $cmd = 'alt_cool'; } } #===================================== # Final master command #===================================== if ($DEBUG) { print 'Master Command: ' . ucwords($cmd) . "\n"; print 'Sub-Commands:' . "\n"; foreach ($sub_cmds as $sub_cmd => $val) { if ($val) { print "\t" . ucwords($sub_cmd) . "\n"; } } } #===================================== # Execute the HVAC command settings #===================================== if ($DEBUG) { print('Regular HVAC command' . "\n"); } hvacCmd($conn, $cmd); #===================================== # Execute the damper command settings #===================================== #===================================== # Save the HVAC command status #===================================== save_hvac_cmd($conn, $sub_cmds, $cmd); #===================================== # Save the damper command status #===================================== if (! $res = @pg_prepare($conn, '', 'INSERT INTO damper_status (damper, open) '. 'VALUES ($1, $2)')) { fatal('DB Error: Unable to prepare damper status save query'); } foreach ($damper_cmd as $damper => $open) { if ($open) { $open = 'TRUE'; } else { $open = 'FALSE'; } if (! $res = @pg_execute($conn, '', array($damper, $open))) { fatal('DB Error: Unable to execute damper status save query'); } } #===================================== # Cleanup #===================================== pg_close($conn); unset($conn); #===================================== # Loop as requested #===================================== if (! $loop) { exit(0); } # Sleep between loops sleep($params['thermostat_delay']); } function fatal($msg, $no_die_debug = FALSE) { global $DEBUG; global $params; # Set mode to "off" if ($DEBUG) { print('Fatal reset' . "\n"); } hvacCmd(NULL); # Whine print($msg . "\n"); # If we aren't scheduled to live if (! $no_die_debug || ! $DEBUG) { # Reset everything print("Resetting all thermostat-related programs\n"); if (strlen($params['reset_prog']) && file_exists($params['reset_prog'])) { pcntl_exec($params['reset_prog']); } # Die (we shouldn't make it here) die('Unable to run reset program: Database or configuration error' . "\n"); } # Otherwise just return } # Determine the heating and cooling requirements of each room function getRoomModes($room_temps, $max_temps, $min_temps, $last_cmd, $offset) { global $DEBUG; global $params; global $rooms; global $bad_rooms; foreach ($rooms as $room) { # Skip bad rooms if ($bad_rooms[$room]) { if ($DEBUG && $offset == 0) { print('Ignoring bad room: ' . ucwords($room) . "\n"); } continue; } # Heat/Cool when out of bounds, continue until within hysteresis bounds if ($room_temps[$room] + $offset < $min_temps[$room]) { $cmd_modes[$room] = 'heat'; } else if ($room_temps[$room] + $offset - $params['hysteresis_heat'] < $min_temps[$room] && ($last_cmd == 'heat' || $last_cmd == 'fan') && $offset == 0) { $cmd_modes[$room] = 'heat'; if ($DEBUG) { print 'Hysteresis heat from mode ' . ucwords($last_cmd) . ' in room: ' . ucwords($room) . "\n"; } } else if ($room_temps[$room] + $offset > $max_temps[$room]) { $cmd_modes[$room] = 'cool'; } else if ($room_temps[$room] + $offset + $params['hysteresis_cool'] > $max_temps[$room] && ($last_cmd == 'cool' || $last_cmd == 'fan') && $offset == 0) { $cmd_modes[$room] = 'cool'; if ($DEBUG) { print 'Hysteresis cool from mode ' . ucwords($last_cmd) . ' in room: ' . ucwords($room) . "\n"; } } else { $cmd_modes[$room] = 'off'; } if ($DEBUG && $offset == 0) { print 'Room: ' . ucwords($room); print "\t" . 'Command: ' . ucwords($cmd_modes[$room]); print "\t" . 'Current: ' . sprintf('%d', $room_temps[$room]); print "\t" . 'Offset: ' . sprintf('%d', $offset); print "\t" . 'Min: ' . sprintf('%d', $min_temps[$room]); print "\t" . 'Max: ' . sprintf('%d', $max_temps[$room]); print "\n"; } } return $cmd_modes; } # Send the specified HVAC command function hvacCmd($conn, $cmd = NULL) { global $DEBUG; global $params; global $proc; # Look up the devices and relays $devs = array(); $relays = array(); if ($conn && strlen($cmd)) { $relays = fetch_relays($conn, TRUE); $devs = fetch_cmd_devices($conn, $cmd); } # If we're not failing $cmds = array(); if (count($relays)) { # Set each relay as the command defines foreach ($relays as $relay => $relay_num) { $mode = 0; if (in_array($relay, $devs)) { $mode = 1; } $cmds[] = sprintf('%s %s %d %d', $params['hvac_prog'], $params['hvac_dev'], $relay_num, $mode); } # Otherwise } else { # Set all relays to off $cmds[] = sprintf('%s %s %d %d', $params['hvac_prog'], $params['hvac_dev'], 0, 0); } # Send each command foreach ($cmds as $cmd) { if ($DEBUG) { print('Sending HVAC command: ' . $cmd . "\n"); } # Run the command $pipes = array(); $descs = array( 0 => array('pipe', 'r'), 1 => array('pipe', 'w') ); $proc = proc_open($cmd, $descs, $pipes); if (! is_resource($proc)) { die('Unable to fork HVAC control process' . "\n"); } # Wait for the command to return (at least for a while) for ($i = 0; $i < $params['hvac_send_timeout']; $i++) { $proc_info = proc_get_status($proc); if (! $proc_info['running'] || $proc_info['signaled'] || $proc_info['stopped']) { break; } sleep(1); } # Kill the child if it's still running if ($proc_info['running']) { kill_child(); } # Check the child's exit if ((! $proc_info['running'] && $proc_info['exitcode'] != 0) || $proc_info['stopped'] || $proc_info['signaled']) { die('Error sending HVAC command' . "\n"); } } } # Kill our child function kill_child() { global $proc; if (is_resource($proc)) { print('Killing child...'); proc_terminate($proc); print('Done' . "\n"); } unset($proc); } # Handle trapped signals function signal_handler($signo) { switch($signo) { # Reload on any captured signal case SIGHUP: case SIGTERM: case SIGINT: print('Resetting' . "\n"); kill_child(); if ($DEBUG) { print('Signal handler reset' . "\n"); } hvacCmd(NULL); exit(1); break; default: print('Unhandled signal' . "\n"); kill_child(); if ($DEBUG) { print('Signal handler reset' . "\n"); } hvacCmd(NULL); exit(1); break; } } # Build and execute the HVAC command status insert function save_hvac_cmd($conn, $sub_cmds, $cmd) { $i = 0; $sql = ''; $sql2 = ''; $sql_params = array(); # Build a parameter array and the necessary $X query text # Translate booleans to text for PHP's silly pgsql implementation foreach ($sub_cmds as $sub_cmd => $val) { if (strlen($sql)) { $sql .= ', '; } $sql .= $sub_cmd; if ($val) { $sql_params[] = 'TRUE'; } else { $sql_params[] = 'FALSE'; } $i++; $sql2 .= sprintf('$%d, ', $i); } $i++; # Complete SQL command $sql2 .= sprintf('$%d', $i); $sql = 'INSERT INTO hvac_status (' . $sql . ', command) VALUES (' . $sql2 . ')'; $sql_params[] = $cmd; # Prepare if (! $res = @pg_prepare($conn, '', $sql)) { fatal('DB Error: Unable to prepare hvac status save query'); } # Execute if (! $res = @pg_execute($conn, '', $sql_params)) { fatal('DB Error: Unable to execute hvac status save query'); } } ?>