def adjust_target_with_rh(add_extra_info, target_inside_temp, **kwargs): dew_point, ts = get_temp([receive_fmi_dew_point], max_ts_diff=6 * 60) add_extra_info('Dew point: %s' % decimal_round(dew_point)) if dew_point is not None: min_temp_with_80_rh = estimate_temperature_with_rh( dew_point, Decimal('0.8')) add_extra_info('Temp with 80%% RH: %s' % decimal_round(min_temp_with_80_rh, 1)) target_inside_temp = max(target_inside_temp, min_temp_with_80_rh) add_extra_info('Target inside temperature: %s' % decimal_round(target_inside_temp, 1)) return {'target_inside_temp': target_inside_temp}
def update_controller(add_extra_info, error, error_without_hysteresis, persistent_data, **kwargs): controller = persistent_data.get('controller') degrees_per_hour_slope = Decimal('0.05') lowest_heating_value = Decimal(0) - Decimal('0.01') highest_heating_value = Decimal(1) + Decimal('0.01') controller.set_i_low_limit(lowest_heating_value - degrees_per_hour_slope * controller.kd) controller.set_i_high_limit(highest_heating_value + degrees_per_hour_slope * controller.kd) controller_output, controller_log = controller.update(error, error_without_hysteresis) add_extra_info('Controller: %s (%s)' % (decimal_round(controller_output, 2), controller_log)) return {'controller_output': controller_output}
def receive_open_weather_map_temperature( ) -> Tuple[Optional[Decimal], Optional[arrow.Arrow]]: temp, ts = None, None try: result = get_url( 'http://api.openweathermap.org/data/2.5/weather?q={place}&units=metric&appid={key}' .format(key=config.OPEN_WEATHER_MAP_KEY, place=config.OPEN_WEATHER_MAP_LOCATION)) except Exception as e: logger.exception(e) else: if result.status_code != 200: logger.error('%d: %s' % (result.status_code, result.content)) else: result_json = result.json() temp = decimal_round(result_json['main']['temp']) ts = arrow.get(result_json['dt']).to(config.TIMEZONE) logger.info('temp:%s ts:%s', temp, ts) return temp, ts
def send_to_lambda(target_inside_temp: Decimal, inside_temp: Optional[Decimal], outside_temp_ts: TempTs, persistent_data: Dict, **kwargs): data = { 'sensorId': {'S': 'controller'}, 'ts': {'S': get_now_isoformat()}, 'temperatures': { 'M': { 'target_inside': {'S': decimal_round(target_inside_temp, decimals=2)}, 'outside': {'S': outside_temp_ts.temp}, }, }, } if inside_temp is not None: data['temperatures']['M']['inside'] = {'S': inside_temp} last_command = persistent_data.get('last_command') if last_command is not None and last_command.temp is not None: data['temperatures']['M']['command'] = {'S': last_command.temp} post_url(url=config.STORAGE_ROOT_URL + 'addOne', data=data)
def get_outside(add_extra_info, mean_forecast, **kwargs): outside_temp, outside_ts = get_temp([ receive_ulkoilma_temperature, receive_fmi_temperature, receive_open_weather_map_temperature ]) add_extra_info('Outside temperature: %s' % outside_temp) if outside_temp is None: valid_outside = False outside_ts = arrow.now() if mean_forecast is not None: outside_temp = mean_forecast add_extra_info('Using mean forecast as outside temp: %s' % decimal_round(mean_forecast)) else: outside_temp = PREDEFINED_OUTSIDE_TEMP add_extra_info('Using predefined outside temperature: %s' % outside_temp) else: valid_outside = True return { 'outside_temp_ts': TempTs(temp=outside_temp, ts=outside_ts), 'valid_outside': valid_outside }
def hysteresis(add_extra_info, target_inside_temp, **kwargs): hyst = Decimal('0.0') add_extra_info('Hysteresis: %s (%s)' % (decimal_round(hyst), decimal_round(target_inside_temp + hyst))) return {'hysteresis': hyst}
def target_inside_temp(add_extra_info, mean_forecast, outside_temp_ts: TempTs, forecast: Union[Forecast, None], persistent_data, **kwargs): minimum_inside_temp = persistent_data.get('minimum_inside_temp') allowed_min_inside_temp = config.ALLOWED_MINIMUM_INSIDE_TEMP cooling_time_buffer = config.COOLING_TIME_BUFFER if mean_forecast: outside_for_target_calc = TempTs(mean_forecast, arrow.now()) else: outside_for_target_calc = outside_temp_ts # print('target_inside_temperature', '-' * 50) # from pprint import pprint # pprint(forecast) cooling_time_buffer_hours = cooling_time_buffer_resolved(cooling_time_buffer, outside_for_target_calc.temp, forecast) add_extra_info('Buffer is %s h at %s C' % ( decimal_round(cooling_time_buffer_hours), decimal_round(outside_for_target_calc.temp))) valid_forecast = [] if outside_for_target_calc: valid_forecast.append(outside_for_target_calc) if forecast and forecast.temps: for f in forecast.temps: if f.ts > valid_forecast[-1].ts: valid_forecast.append(f) # if valid_forecast: # outside_after_forecast = mean(t.temp for t in valid_forecast) # while len(valid_forecast) < config.COOLING_TIME_BUFFER: # valid_forecast.append(TempTs(temp=outside_after_forecast, ts=valid_forecast[-1].ts.shift(hours=1))) reversed_forecast = list(reversed(valid_forecast)) # pprint(reversed_forecast) # pprint(reversed_forecast[-1].ts) iteration_inside_temp = allowed_min_inside_temp iteration_ts = arrow.now().shift(hours=float(cooling_time_buffer_hours)) # print('iteration_ts', iteration_ts) # if reversed_forecast[0].ts < iteration_ts: outside_after_forecast = mean(t.temp for t in reversed_forecast) # print('outside_after_forecast', outside_after_forecast) while iteration_ts > reversed_forecast[0].ts: hours_to_forecast_start = Decimal((iteration_ts - reversed_forecast[0].ts).total_seconds() / 3600.0) assert hours_to_forecast_start >= 0, hours_to_forecast_start this_iteration_hours = min([Decimal(1), hours_to_forecast_start]) outside_inside_diff = outside_after_forecast - iteration_inside_temp temp_drop = config.COOLING_RATE_PER_HOUR_PER_TEMPERATURE_DIFF * outside_inside_diff * this_iteration_hours if outside_after_forecast <= -17: # When outside temp is about -17 or colder, then the pump heating power will decrease a lot logger.debug('Forecast temp <= -17: %.1f' % outside_after_forecast) temp_drop *= 2 iteration_inside_temp -= temp_drop iteration_ts = iteration_ts.shift(hours=float(-this_iteration_hours)) # from pprint import pprint # pprint({ # 'iteration_ts': iteration_ts, # 'temp_drop': temp_drop, # 'iteration_inside_temp': iteration_inside_temp, # 'this_iteration_hours': this_iteration_hours, # }) # print('-' * 50) if iteration_inside_temp < allowed_min_inside_temp: iteration_inside_temp = allowed_min_inside_temp # print('*' * 20) # print('-' * 10, 'start forecast', iteration_ts, iteration_inside_temp) for fc in filter(lambda x: x.ts <= iteration_ts, reversed_forecast): this_iteration_hours = Decimal((iteration_ts - fc.ts).total_seconds() / 3600.0) assert this_iteration_hours >= 0, this_iteration_hours outside_inside_diff = fc.temp - iteration_inside_temp temp_drop = config.COOLING_RATE_PER_HOUR_PER_TEMPERATURE_DIFF * outside_inside_diff * this_iteration_hours # if iteration_inside_temp - temp_drop > allowed_min_inside_temp: # iteration_inside_temp -= temp_drop # else: # break if fc.temp <= -17: # When outside temp is about -17 or colder, then the pump heating power will decrease a lot logger.debug('Forecast temp <= -17: %.1f' % fc.temp) temp_drop *= 2 iteration_inside_temp -= temp_drop iteration_ts = fc.ts # from pprint import pprint # pprint({ # 'fc': fc, # 'temp_drop': temp_drop, # 'iteration_inside_temp': iteration_inside_temp, # 'this_iteration_hours': this_iteration_hours, # }) # print('-' * 50) if iteration_inside_temp < allowed_min_inside_temp: iteration_inside_temp = allowed_min_inside_temp # print('!' * 20) # assert False, iteration_inside_temp # print('iteration_ts', iteration_ts) # print('target_inside_temperature', iteration_inside_temp) return {'target_inside_temp': max(iteration_inside_temp, minimum_inside_temp)}
def update( self, error: Optional[Decimal], error_without_hysteresis: Optional[Decimal] ) -> Tuple[Decimal, str]: if error is None: error = Decimal(0) else: self._update_past_errors(error_without_hysteresis) logger.debug('controller error %.4f', error) p_term = self.kp * error new_time = time.time() error_slope_per_second = self._past_error_slope_per_second() error_slope_per_hour = error_slope_per_second * Decimal(3600) error_slope_per_hour = min(error_slope_per_hour, Decimal('0.5')) error_slope_per_hour = max(error_slope_per_hour, Decimal('-0.5')) if self.current_time is not None: delta_time = Decimal(new_time - self.current_time) logger.debug('controller delta_time %.4f', delta_time) if error > 0 and error_slope_per_hour >= Decimal( '-0.05') or error < 0 and error_slope_per_hour <= 0: integral_update_value = self.ki * error * delta_time / Decimal( 3600) logger.info('Updating integral with %.4f', integral_update_value) self.integral += integral_update_value else: logger.info('Not updating integral') self.current_time = new_time if self.integral > self.i_high_limit: self.integral = self.i_high_limit logger.debug('controller integral high limit %.4f', self.i_high_limit) elif self.integral < self.i_low_limit: self.set_integral_to_lower_limit() i_term = self.integral d_term = self.kd * error_slope_per_hour logger.debug('controller p_term %.4f', p_term) logger.debug('controller i_term %.4f', i_term) logger.debug('controller d_term %.4f', d_term) past_errors_for_log = [(decimal_round(p[0]), decimal_round(p[1])) for p in self.past_errors] logger.debug('controller past errors %s', past_errors_for_log) output = p_term + i_term + d_term logger.debug('controller output %.4f', output) return output, self.log(error, p_term, i_term, d_term, error_slope_per_hour, self.i_low_limit, self.i_high_limit, output)