Возникла необходимость установки циркуляционного насоса в полотенцесушителе, поскольку через некоторое время после ремонта горячая вода перестала самотёком заходить в него. Был куплен сам насос и смонтирован куда надо, с физикой всё в порядке. А вот с менеджментом и экономикой стало как-то грустно: даже заявленные 25 Вт мощности насоса выливаются в 219 кВт*ч потребления за год, или, если в деньгах - примерно в 800 рублей добавочных расходов. Уменьшить энергопотребление можно различными путями: 1. Заменить насос на менее производительный и маломощный; 2. запускать насос эпизодически, по мере надобности (остывания полотенцесушителей). С первым пунктом всё сложно: чем ниже энергопотребление, тем выше цены. Второй пункт можно реализовать при помощи механических, электронных таймеров (300-700 рублей) или термореле ( >2000 рублей). Бытовые таймеры страдают либо грубым квантованием времени (шаг 15 минут в суточных механических) или недостаточным количеством программ (порядка 20 в электронных), ну а готовые термореле дороговаты и требуют перенастройки при изменении температуры теплоносителя.

В связи с вышесказанным родилась хотелка самонастраиваемого устройства регулировки работы насоса. Предъявляемые требования: низкое энергопотребление, автоматическая адаптация к температуре теплоносителя, самонастройка длительности времени работы/простоя.

Для реализации использовал МК семейства Arduino (Пробовал как Mega, так и Nano), цифровой термодатчик Dallas DS18xxx, резистор на 4,7кОм, реле рабочим напряжением 5В и коммутируемым 220В (я брал твердотельное OMRON), и блок питания на 5В постоянного тока. Разумеется, нужны ещё провода, пара клеммников и коробка, куда будет уложено всё богатство.  Схема творения не сохранилась, но способы подключения реле и термодатчиков широко описаны в примерах на различных сайтах, ничего нового изобретено не было. Термодатчик был размещён на выходящей из полотенцесушителя трубе, ближе к магистрали.

Принцип работы.

При включении МК включается насос, начинает измеряться температура. Насос работает, пока растёт температура на выходной трубе. При прекращении роста температуры отсчитывается wait_before_pump_off секунд и насос отключается. Далее МК ожидает падения температуры на hysteresis градусов, после чего насос включается и цикл повторяется. Если по какой-то причине (изначально недостаточная температура теплоносителя, хорошая теплоизоляция полотенцесушителя, ошибочное значение параметров) температура не может снизиться на величину гистерезиса, контроллер отсчитывает max_state_time_counter секунд и включает насос. Эти три параметра влияют на соотношение и длительность интервалов вкл/выкл. На практике достигнуто соотношение работы/отдыха на уровне порядка 1/3,5 при данных значениях параметров.

Код скетча:

#include <OneWire.h>

struct TempRelay {
  int PumpAddress;        //Пин выхода на силовое реле
  bool Heating;           //Cостояние реле вкл/выкл
  byte addr[8];           //Адрес датчика Dallas
  byte type_s;            //Тип датичка Dallas
  int time_counter;       //Время работы насоса
  int state_time_counter; //Время нахождения в текущем состоянии
  int higher_temp;        //Максимально достигнутая температура * 16
};

int wait_before_pump_off = 60; // Время работы насоса после достижения максимальной температуры трубы
int hysteresis = 5*16; // Градусы Цельсия, умноженные на 16 (приведение к raw-формату dallas). Падение температуры для повторного включения насоса.
int max_state_time_counter = 3600; //Максимальное время нахождения в одном из состояний, в секундах.
int loop_delay_calculator_counter = 0;
int loop_timer = 0; // Время выполнения одного прохода функции loop. Для вычисления задержки.
int loop_first_time_check = 0;

TempRelay TR;
OneWire  ds(2);  // Работать с датчиками Dallas на пине 2 (нужен резистор на 4.7 кОм)


void setup(void) {
  int i = 0;
  Serial.println("Init...");
  Serial.begin(9600);
  TR = {4, false, {0,0,0,0,0,0,0,0},0, 0, 0, -55*16};
  pinMode(TR.PumpAddress, OUTPUT);
  
  ds.write(0x60);
  int sensors = 0;
  while (ds.search(TR.addr)){
   sensors++;
  };
  ds.reset_search();
  if (!sensors) {
    Serial.println("No DS18xxx sensors.");
  }
  else
  {
  Serial.print("Sensors on the wire: ");
  Serial.println(sensors);
 
  Serial.print("HW id:");
  for( i = 0; i < 8; i++) {
    Serial.write(' ');
    Serial.print(TR.addr[i], HEX);
  } 
    if (OneWire::crc8(TR.addr, 7) != TR.addr[7]) {
      Serial.println("CRC is not valid! Bad wire?");
      return;
  }
  Serial.println(); 
  }
  // the first ROM byte indicates which chip
  switch (TR.addr[0]) {
    case 0x10:
      Serial.println("  Chip = DS18S20");  // or old DS1820
      TR.type_s = 1;
      break;
    case 0x28:
      Serial.println("  Chip = DS18B20");
      TR.type_s = 0;
      break;
    case 0x22:
      Serial.println("  Chip = DS1822");
      TR.type_s = 0;
      break;
    default:
      Serial.println("Device is not a DS18x20 family device.");
      return;
  }
  int16_t raw = read_dallas(&ds, TR.addr);
  delay(1000); // Дать время на формирование результата
  raw = read_dallas(&ds, TR.addr); 
  pump_on(&TR);
  pump_diag(&TR, raw);
  Serial.println("Init complete...");    
}

 
void loop() {
//Засечка времени начала первого прохода
  if (loop_delay_calculator_counter == 0)
    loop_first_time_check = millis();
 
  loop_delay_calculator_counter++;

  int16_t raw = 0;
  raw = read_dallas(&ds, TR.addr);


  TR.state_time_counter++;
  if (TR.Heating) {
    if (TR.higher_temp < raw){
      TR.higher_temp = raw;
      TR.time_counter = 0;
    }
    else
      TR.time_counter++;
    if(TR.time_counter > wait_before_pump_off || TR.state_time_counter > max_state_time_counter){
      pump_diag(&TR, (float)raw);
      pump_off(&TR);
    }
  }
  else
  {
    if (raw + hysteresis < TR.higher_temp || TR.state_time_counter > max_state_time_counter){
      pump_diag(&TR, (float)raw);
      pump_on(&TR);
    }
  }

if (loop_delay_calculator_counter > 100) 
  {
  loop_delay_calculator_counter--;
  delay(1000-loop_timer); //Подгоняем продолжительность цикла к 1 секунде. Приблизительно. Особого практического смысла эта фича не имеет, можно просто поставить задержку в 1 сек.
  }
else
  {
  if (loop_delay_calculator_counter == 100) // Считаем среднее время выполнения одного цикла после прохождения 100 циклов
    loop_timer = (millis() - loop_first_time_check) / 100;
  }
  TR.state_time_counter--;
  TR.time_counter = 0;
}

int16_t read_dallas(OneWire *ds, byte *address){
  byte data[12];
  byte present = 0;
  ds->reset();
  ds->select(address);
// при паразитном питании нужна задержка.
  ds->write(0x44, 1);        // start conversion, with parasite power on at the end
//  delay(1000);     // maybe 750ms is enough, maybe not
  // we might do a ds.depower() here, but the reset will take care of it.
 
  present = ds->reset();
  ds->select(address);   
  ds->write(0xBE);

//  Serial.print("  Data = ");
//  Serial.print(present, HEX);
//  Serial.print(" ");
  for (byte i = 0; i < 9; i++) {           // we need 9 bytes
    data[i] = ds->read();
//    Serial.print(data[i], HEX);
//    Serial.print(" ");
  }

/*  float celsius;
  celsius = (float)raw_temp(TR.type_s, data) / 16.0;
  Serial.print("Temperature = ");
  Serial.print(celsius);
  Serial.println(" Celsius"); 

*/ 
//  Serial.print(" CRC=");
//  Serial.print(OneWire::crc8(data, 8), HEX);
//  Serial.println();
  return raw_temp(TR.type_s, data);
}

void pump_diag(TempRelay *no, float raw){
  float celsius = raw / 16.0;
  Serial.print("Reached temperature = ");
  Serial.print(celsius);
  Serial.println(" Celsius");
 
  Serial.print("Pump was ");
  Serial.print(no->state_time_counter);
  Serial.print(" sec. ");
  Serial.println(no->Heating?"on":"off");
}

void pump_off(TempRelay *no){
 digitalWrite(no->PumpAddress, HIGH); // почему в nano - HIGH, а в mega LOW?
 no->Heating = false;
 no->state_time_counter = 0;
}

void pump_on(TempRelay *no){
 digitalWrite(no->PumpAddress, LOW); // почему в nano - LOW, а в mega HIGH?
 no->Heating = true;
 no->time_counter = 0;
 no->higher_temp = 0;
 no->state_time_counter = 0;
}

int16_t raw_temp(int type_s, byte *data)
{
  // Convert the data to actual temperature
  // because the result is a 16 bit signed integer, it should
  // be stored to an "int16_t" type, which is always 16 bits
  // even when compiled on a 32 bit processor.
  int16_t raw = (data[1] << 8) | data[0];
  if (type_s) {
    raw = raw << 3; // 9 bit resolution default
    if (data[7] == 0x10) {
      // "count remain" gives full 12 bit resolution
      raw = (raw & 0xFFF0) + 12 - data[6];
    }
  } else {
    byte cfg = (data[4] & 0x60);
    // at lower res, the low bits are undefined, so let's zero them
    if (cfg == 0x00) raw = raw & ~7;  // 9 bit resolution, 93.75 ms
    else if (cfg == 0x20) raw = raw & ~3; // 10 bit res, 187.5 ms
    else if (cfg == 0x40) raw = raw & ~1; // 11 bit res, 375 ms
    //// default is 12 bit resolution, 750 ms conversion time
  }
  return raw; 
}