1. Котел отопления.
2. Накопительный бойлер для водоснабжения.
3. Насос в скважине.
Читал массу увлекательных статей на тему ХХ на Ардуино, читая которые четко фиксировал в голове мысль «хочу Ардуино». Прикинув стоимость компонентов и готовых решений, посчитал явную выгоду от внедрения Ардуино.
Итак, программа минимум:
1. 4 реле, часы (RTC), ЖК экран;
2. Режимы работы каждого реле: включено, выключено, суточный таймер, одноразовое включение;
3. Кнопки управления для настройки времени и режимов реле;
В доме установлен двухтарифный счетчик, поэтому бойлер нагревает воду с 23 до 7 утра. Аналогично отопление: два из трех тэнов, по моей задумке будут включаться ночью. Управление температурой пока остается родное на штатном пульте. Одноразовое включение в качестве резерва пойдет на насос, программируем включение, например, на набор емкости или прокачку скважины, после чего реле переходит в режим выключено. Основная особенность: изготовлено законченное устройство, управляемое кнопками, и не требующее подключения к ПК.
Конечно, хотелось в перспективе все повесить на контроллер, так как для отопления целесообразно сделать 3 режима работы: день с 7 до 23 в целях экономии, ночь, разогрев к утреннему отключению с 5..6 до 7. Но пока реализована программа минимум.
Аппаратная часть:
При изготовлении была задача получить как можно более дешевое изделие, поэтому максимально присутствует колхоз. На Али были заказаны стартовый комплект для arduino Uno R3, 4 релейный модуль, жк-экран I2C 20*4, часы RTC DS1307 I2C, цифровой датчик температуры и влажности Dht21.
Поскольку все это видел первый раз пришлось осваивать. Общие понятия почерпнул с помощью гугла из:
http://habrahabr.ru/company/masterkit/blog/257747/
http://arduino.ru/Reference
Красивую схему подключения сделать не могу, не в чем. Во Fritzing к примеру, из компонентов только сам микроконтроллер.
Подключение реле и кнопок проблем не вызвало, единственно включил подтягивающие резисторы. Это есть в руководстве. В подключении ЖК экрана помогла ссылка https://arduino-info.wikispaces.com/LCD-Blue-I2C#v3. Потребовалось регулировка подстроечным резистором, «из коробки» экран не горел совсем, чем вызвал у меня легкое замешательство.
Часы потребовали только батарейку, подключил по типовой схеме http://zelectro.cc/RTC_DS1307_arduino
Синхронизацию часов с компьютером делать не стал. При запуске производится проверка, если дата меньше 2000 года или больше 2100, выводится меню настройки часов.
Подключение нескольких кнопок к аналоговому входу описано по ссылке http://arduino.net.ua/Arduino_articles/Arduino_proekty/Podkljuchenie%20knopok%20k%20odnomu%20analogovomu%20vhodu/, там же в комментариях описано как включить подтягивающие резисторы «pinMode (A2, INPUT_PULLUP);»
Управление классическое, «мониторное»: кнопки «меню», "+","-", «set».
Поставил монтажные стойки под модули:
Прикрутил реле, часы, контроллер:
От принтера взял пару валиков и какую-то втулку. Втулка приклеена на двухсторонний скотч. На них будет крепиться еще одна плата, об этом ниже.
Блок питания взял от какого-то роутера Dlink, 5В 2А, ломать голову не стал запаял прямо на него USB провод:
Вырезал из пластика панель для крепления экрана:
Установил экран. Закрепил монтажными стойками, под клавиатурой — винт. Стойки подобраны по высоте с расчетом, что в них будет упираться крышка щитка, придавая жесткость конструкции. Втулка на блоке реле предотвращает продавливание платы вниз при нажатии кнопок.
Кнопки изначально планировал подключить к цифровым входам, но внезапно нашел модуль клавиатуры от монитора, который подошел как родной (схема кнопок от монитора).
Клавиатуру приклеил на двухсторонний скотч через прокладку, что бы поднять плату над правым нижним винтом. Кнопки нажимаю спичкой через отверстия. В идеале надо рассверлить отверстия и вставить туда нормальные толкатели. Может быть сделаю холодным зимним вечером, а сейчас потребовалось срочно внедрить реле на нагрев воды.
Фото готового устройства:
Мигающий светодиод также присутствует.
В данный момент реле висит «на соплях», управляет бойлером, окончательный монтаж будет произведен после установки проводки и контакторов для котла отопления. Фазу с колодки тоже надо убрать, конечно, при монтаже проводки. Сейчас некогда, надо делать наружные работы по дому. Затраты на детали составили около 2 тысяч рублей.
Программная часть:
Программная часть далась нелегко: 90% времени ушло на написание меню, годный код удалось осилить только с третьей версией прошивки.
Первый подход вырос из тестовых образцов на проверку деталей. Классический образец процедурного программирования, развития не получил. Пришлось вспоминать принципы написания хорошего, годного кода, который бы поддавался последующему чтению и правке.
Второй подход был переводом кода на принципы ООП. За основу был взят определенный класс TMenu, от которого наследовались непосредственно элементы меню.
Вкратце. Указателю CurrentMenu присваивается адрес текущего элемента меню Основные элементы класса это бит ItemIsValue, который определяет является ли текущий элемент подменю или изменяемым значением и функции OnKey(), Increment(), Decrement() и Print(). Также класс меню содержит указатель на родительское меню и массив указателей. В общем использование наследования позволяло сделать произвольное многоуровневое меню, в принципе можно сказать, что это динамическое меню, только в в данной реализации оно формируется один раз при инициализации. Во всяком случае код легко редактируется, добавляются элементы меню. Жестокая реальность поставила меня на место. В UNO R3 на всю эту роскошь не хватает памяти.
Третий подход — урезка второго. Главное отличие одно — конкретный объект класса меню содержит либо вложенные меню, либо переменные — редактируемые значения, тип которых задан классом.
Итак, Определен класс:
class TMenu
{
public:
byte _ItemsCount;
TMenu *Parent;
String *MenuName;
boolean ItemIsValue;
byte CurrentItem;
TMenu **Items;
String *ItemsName;
byte ItemsCount(void) ;
bool AddItem(TMenu *NewItem);
virtual void Print(void);
void OnKey(byte KeyNum);
void ChangeItem(byte value);
virtual void Increment(void);
virtual void Decrement(void);
virtual void OnSet(void);
DateTime CheckDateTime(DateTime OldDate, int Increment, byte DatePart);
};
Класс содержит:
— число элементов меню (подменю или переменная), указатель на родительское меню (если указатель равен 0, то достигнут верх);
— MenuName имя меню;
— ItemIsValue описан выше
— номер позиции курсора в меню (CurrentItem);
— указатель на массив указателей Items. Адреса подменю. Если меню содержит редактируемые элементы, это значение равно 0;
— функция Print() вызывается из цикла loop от имени текущего меню «CurrentMenu->Print();» таким образом отрисовывается экран с нужным текстом.
— функция OnKey(byte KeyNum) также вызывается из цикла loop в блоке подавления дребезга контактов, он же декодер клавиатуры от монитора.
— функции ChangeItem(byte value), virtual void Increment(void), virtual void Decrement(void) вызываются из OnKey() и обрабатывают кнопки "+" и "-". ChangeItem() — это переборка элементов меню, Increment() и Decrement() — полиморфные, переборка значений текущей переменной.
— функция CheckDateTime(DateTime OldDate, int Increment, byte DatePart) проверяет введенную дату и время. Распознается вискозный год и количество дней в месяце 28/29, 30, 31. Исходя из логики в функцию передается текущая дата, +1 или -1 и индекс части даты/времени (0 — год, 5 — секунды)
Навигация по меню реализована присвоением адреса объекта указателю CurrentMenu:
— CurrentMenu = CurrentMenu->Items[CurrentMenu->CurrentItem]; вход в выбранное меню
— CurrentMenu = CurrentMenu->Parent; переход в предыдущее меню
Логика работы:
Цикл loop непрерывно опрашивает клавиатуру, проверяет настройки реле и мигает светодиодом.
Клавиатура опрашивается в качестве рудимента и по цифровым входам 2-6 (menu,-,+,set), к этим кодам пересчитываются значения аналоговых портов.
— при нажатии на кнопку «menu» вне меню происходит вызов меню, в противном случае переход на меню вверх;
— при нажатии "+" или "-" происходит циклическая переборка элементов меню или циклическое изменение текущего параметра. При нажатии кнопки "'set" вход в выбранное меню либо сохранение значения переменной во флеш с одновременным выбором следующего значения.
Дребезг подавляется программно, каждой кнопке присваивается счетчик нажатия и отпускания, который увеличивается в случае нажатия или отпускания. Опрос проводится 3 раза с интервалом 15 мс. Счетчик нажатия или отпускания увеличивается на 1 либо сбрасывается. Таким образом распознается дребезг как нажатия так и отпускания. Состояние отпускания фиксируется для однократного срабатывания при удержании кнопки.
В настройках реле проверяется режим работы, в режиме «Daily» вводится и проверяется только время, с точностью до минут. Правильно распознается время включения больше времени выключения, например, включение в 23 и выключение в 7. В режиме «Оnce» задается дата и время. Для удобства настройки планирую подключить пятую кнопку и задать на нее функцию установки текущей даты и времени в режиме редактирования.
Это вкратце. Небольшие функции классов объявлены, как правило при объявлении класса, заголовочные файлы и библиотеки не используются. Код и так небольшой.
#include <DHT.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <RTClib.h>
#define LEFT 0
#define CENTER 1
#define RIGHT 2
#define RelayModesCount 4
#define KeyFirst 2
#define KeyLast 6
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);
RTC_DS1307 RTC; // RTC Modul
DHT dht(7, DHT21); // pin, type
volatile boolean Blinker = true;
volatile long BlinkerTime;
volatile byte ButtonPress[8];
const String RelayModeNames[] = {«OFF», «ON», «Once», «Daily»};
int aKey1 = 0;
int aKey2 = 0;
DateTime NowDate;
boolean DoBlink(void)
{
boolean Result = false;
long NBlinkerTime = millis();
if (Blinker)
{
if (NBlinkerTime — BlinkerTime > 200)
{
digitalWrite(8, HIGH);
BlinkerTime = NBlinkerTime;
Blinker = false;
Result = true;
}
}
else
{
if (NBlinkerTime — BlinkerTime > 300 )
{
digitalWrite(8, LOW);
BlinkerTime = NBlinkerTime;
Blinker = true;
}
}
return Result;
}
String BlinkString(String string, byte Cur, byte ItemsCount)
{
String result = string;
byte len = string.length();
if (!Blinker && Cur == ItemsCount)
{
for (byte i = 0; i < len; i++) result.setCharAt(i, ' ');
}
return result;
}
/********************************************************************************************************/
/********************************** Объявление классов *********************************************/
/********************************************************************************************************/
class TMenu
{
public:
byte _ItemsCount;
TMenu *Parent;
String *MenuName;
boolean ItemIsValue;
byte CurrentItem;
TMenu **Items;
String *ItemsName;
// byte ItemsCount(void);
byte ItemsCount(void) {
return _ItemsCount;
};
bool AddItem(TMenu *NewItem);
virtual void Print(void);
void OnKey(byte KeyNum);
void ChangeItem(byte value);
virtual void Increment(void);
virtual void Decrement(void);
virtual void OnSet(void);
DateTime CheckDateTime(DateTime OldDate, int Increment, byte DatePart);
};
class TNoMenu: public TMenu
{
public:
void Print(void);
TNoMenu(TMenu *ParentMenu) {
MenuName = 0;
CurrentItem = 0;
_ItemsCount = 0;
Parent = ParentMenu;
Items = 0;
ItemsName = 0;
ItemIsValue = false;
};
void Increment(void) {};
void Decrement(void) {};
void OnSet(void) {};
};
class TSelectMenu: public TMenu
{
public:
void Print(void);
TSelectMenu(TMenu *ParentMenu, String NewName) {
MenuName = new String(NewName);
CurrentItem = 0;
_ItemsCount = 0;
Parent = ParentMenu;
Items = 0;
ItemsName = 0;
ItemIsValue = false;
};
void Increment(void) {};
void Decrement(void) {};
void OnSet(void) {};
};
class TTimeMenu: public TMenu
{
public:
void Print(void);
DateTime *SetDateTime;
long OldDateTime;
TTimeMenu(TMenu *ParentMenu, String NewName, DateTime *ParamDate) {
MenuName = new String(NewName);
CurrentItem = 0; _ItemsCount = 6; Parent = ParentMenu; Items = 0; ItemsName = 0;
ItemIsValue = true; OldDateTime = millis();
SetDateTime = ParamDate;
};
void Increment(void) {
*SetDateTime = CheckDateTime (*SetDateTime, 1, CurrentItem);
};
void Decrement(void) {
*SetDateTime = CheckDateTime (*SetDateTime, -1, CurrentItem);
};
void OnSet(void) {
RTC.adjust(*SetDateTime);
};
void SecondTimer(void) {
long TmpDateTime = millis(); if (TmpDateTime — OldDateTime > 1000) {
OldDateTime = TmpDateTime;
*SetDateTime = *SetDateTime + 1;
};
};
};
class TRelayMenu: public TMenu
{
public:
byte RelayNumber;
byte RelayMode;
// byte Shedule=0;
boolean OnceBit;
DateTime RelayOn;
DateTime RelayOff;
TRelayMenu(TMenu *ParentMenu, byte NewNumber, String NewName) {
MenuName = new String(NewName);
CurrentItem = 0; _ItemsCount = 11; Parent = ParentMenu; Items = 0; ItemsName = 0; ItemIsValue = true, OnceBit = false;
RelayNumber = NewNumber;
RelayMode = 0;
RelayOn = DateTime(2015, 1, 1, 23, 00, 00);
RelayOff = DateTime(2015, 1, 1, 07, 00, 00);
};
void Print(void);
void Increment(void) {
if (!CurrentItem) {
RelayMode++;
if ( RelayMode >= RelayModesCount) RelayMode = 0;
}
else if (CurrentItem < 6) RelayOn = CheckDateTime (RelayOn, 1, CurrentItem — 1);
else RelayOff = CheckDateTime (RelayOff, 1, CurrentItem — 6);
};
void Decrement(void) {
if (!CurrentItem) {
RelayMode--;
if ( RelayMode > 127) RelayMode = RelayModesCount — 1;
}
else if (CurrentItem < 6) RelayOn = CheckDateTime (RelayOn, -1, CurrentItem — 1);
else RelayOff = CheckDateTime (RelayOff, -1, CurrentItem — 6);
};
boolean CheckDaily(void);
void OnSet(void) {
///// здесь надо записать реле в память
byte p_address = RelayNumber * 16;
EEPROM.write(p_address, RelayMode);
EEPROM.write(p_address + 1, byte(RelayOn.year() — 2000));
EEPROM.write(p_address + 2, byte(RelayOn.month() ));
EEPROM.write(p_address + 3, byte(RelayOn.day() ));
EEPROM.write(p_address + 4, byte(RelayOn.hour() ));
EEPROM.write(p_address + 5, byte(RelayOn.minute() ));
EEPROM.write(p_address + 6, byte(RelayOff.year() — 2000));
EEPROM.write(p_address + 7, byte(RelayOff.month() ));
EEPROM.write(p_address + 8, byte(RelayOff.day() ));
EEPROM.write(p_address + 9, byte(RelayOff.hour() ));
EEPROM.write(p_address + 10, byte(RelayOff.minute() ));
};
};
/********************************************************************************************************/
/******************************** Конец объявления классов ******************************************/
/********************************************************************************************************/
TMenu *CurrentMenu = 0;
TNoMenu *NoMenu = 0;
TSelectMenu *SelectMenu;
TTimeMenu *TimeMenu;
TRelayMenu *RelayMenu[4];
/********************************************************************************************************************************************/
/********************************************************************************************************************************************/
/********************************************************************************************************************************************/
void setup()
{
NoMenu = new TNoMenu(0);
SelectMenu = new TSelectMenu (NoMenu, «NoMenu»);
TimeMenu = new TTimeMenu(SelectMenu, «Time Setup», &NowDate);
SelectMenu->AddItem(TimeMenu);
byte p_address;
DateTime DTFlesh;
for (int i = 0; i < 4; i++)
{
// здесь надо добавить загрузку параметров из флеша
RelayMenu[i] = new TRelayMenu (SelectMenu, i, «Relay » + String(i + 1));
SelectMenu->AddItem(RelayMenu[i]);
p_address = i * 16;
RelayMenu[i]->RelayMode = EEPROM.read(p_address);
DTFlesh = DateTime(int(EEPROM.read(p_address + 1) + 2000), EEPROM.read(p_address + 2), EEPROM.read(p_address + 3), EEPROM.read(p_address + 4), EEPROM.read(p_address + 5), 0 );
RelayMenu[i]->RelayOn = RelayMenu[i]->CheckDateTime(DTFlesh, 0, 0);
DTFlesh = DateTime(int(EEPROM.read(p_address + 6) + 2000), EEPROM.read(p_address + 7), EEPROM.read(p_address + 8), EEPROM.read(p_address + 9), EEPROM.read(p_address + 10), 0 );
RelayMenu[i]->RelayOff = RelayMenu[i]->CheckDateTime(DTFlesh, 0, 0);
}
for (byte i = KeyFirst; i < KeyLast; i++)
{
pinMode(i, INPUT); //Keypad 2-«menu» 3-"-" 4-"+" 5-«SET»
digitalWrite(i, HIGH); //setup Resistor input2Vcc
ButtonPress[i] = true;
}
pinMode(8, OUTPUT); //LED
pinMode(9, OUTPUT);
for (byte i = 10; i < 14; i++)
{
pinMode(i, OUTPUT); // relay i
digitalWrite(i, HIGH);
}
pinMode (A2, INPUT_PULLUP);
pinMode (A3, INPUT_PULLUP);
Serial.begin(9600); // Used to type in characters
digitalWrite(8, LOW);
digitalWrite(9, HIGH);
lcd.begin(20, 4); // initialize the lcd for 20 chars 4 lines and turn on backlight
RTC.begin();
lcd.noBacklight();
delay(150);
lcd.backlight();
NowDate = RTC.now();
//проверка времени
if ( NowDate.year() > 2000 && NowDate.year() < 2114 &&
NowDate.month() > 0 && NowDate.month() < 13 &&
NowDate.day() > 0 && NowDate.day() < 32 &&
NowDate.hour() >= 0 && NowDate.hour() < 24 &&
NowDate.minute() >= 0 && NowDate.minute() < 60 &&
NowDate.second() >= 0 && NowDate.second() < 60 )
{
CurrentMenu = NoMenu;
}
else
{
lcd.setCursor(2, 1);
lcd.print(«Clock Failure!»);
delay(700);
RTC.adjust(DateTime(2015, 1, 1, 00, 00, 00));
CurrentMenu = TimeMenu;
}
}
void loop()
{
/********* KEYPAD BUNCLE, 5 keys, from 2 to 6 *********/
byte NButtonPress[8] = {0, 0, 0, 0, 0, 0, 0, 0};
byte NButtonRelease[8] = {0, 0, 0, 0, 0, 0, 0, 0};
const byte ButtonTry = 3;
aKey1 = analogRead (2);
aKey2 = analogRead (3);
byte aKeyNum=0;
/**************** check for key pressed or released ****************/
for (byte i = 0; i < 3; i++)
{
delay(15);
if (aKey1< 64) aKeyNum=2;//AnalogKey 1 = Dig2
else if (aKey1<128) aKeyNum=6;//Analog key 3 = D4
else if (aKey1<256) aKeyNum=4;//key 5=d6
else if (aKey2< 64) aKeyNum=1;//key 6 = menu
else if (aKey2<128) aKeyNum=3;//analogkey 2 = D3
else if (aKey2<256) aKeyNum=5;//key 4 =d5
else aKeyNum=0; // no key
for (byte j = KeyFirst; j < KeyLast; j++) // Read ports 2...6
{
if (digitalRead(j) == LOW || aKeyNum==j)
{
NButtonPress[j]++;
NButtonRelease[j] = 0;
}
else
{
NButtonPress[j] = 0;
NButtonRelease[j]++;
delay(5);
}
}
}
/*************** Do key process ******************/
// byte m;
for (byte j = KeyFirst; j < KeyLast; j++)
{
if (NButtonPress[j] >= ButtonTry && ButtonPress[j] == false)
{
ButtonPress[j] = true;
CurrentMenu->OnKey(j);
}
else
{
if (NButtonRelease[j] >= ButtonTry && ButtonPress[j] == true)
{
ButtonPress[j] = false;
}
}
}
/***************** Relay Check *********************/
CurrentMenu->Print();
DoBlink();
}
void LcdPrint(byte string, String str, byte Align)
{
byte StrTrim1;
byte StrTrim2;
lcd.setCursor(0, string); //Start at character 0 on line 0
switch (Align)
{
case RIGHT:
break;
case CENTER:
StrTrim1 = byte((20 — str.length()) / 2);
StrTrim2 = 20 — str.length() — StrTrim1;
for (byte k = 0; k < StrTrim1; k++) lcd.print(" ");
lcd.print(str);
for (byte k = 0; k < StrTrim2; k++) lcd.print(" ");
break;
default:
lcd.print(str);
StrTrim1 = 20 — str.length();
for (byte k = 0; k < StrTrim1; k++) lcd.print(" ");
}
}
void TNoMenu::Print(void)
{
NowDate = RTC.now();
String Ddate;
Ddate = " R1-" + RelayModeNames[RelayMenu[0]->RelayMode] + " R2-" + RelayModeNames[RelayMenu[1]->RelayMode];
LcdPrint(0, Ddate, CENTER);
Ddate = " R3-" + RelayModeNames[RelayMenu[2]->RelayMode] + " R4-" + RelayModeNames[RelayMenu[3]->RelayMode];
LcdPrint(1, Ddate, CENTER);
Ddate = String (NowDate.year()) + "/" + String(NowDate.month()) + "/" + String(NowDate.day()) + " " + String (NowDate.hour()) + ":" + String(NowDate.minute()) + ":" + String(NowDate.second());
LcdPrint(2, Ddate, CENTER);
Ddate = «Temp » + String (int(dht.readTemperature())) + «C, Hum » + String(int(dht.readHumidity())) + "%";
LcdPrint(3, Ddate, CENTER);
RelayCheck();
}
void TTimeMenu::Print(void)
{
SecondTimer();
String Ddate = BlinkString(String((*SetDateTime).year()), CurrentItem, 0) + "/" +
BlinkString(String( (*SetDateTime).month()), CurrentItem, 1) + "/" +
BlinkString(String((*SetDateTime).day()), CurrentItem, 2) + " ";
LcdPrint(1, Ddate, CENTER);
Ddate = BlinkString(String ((*SetDateTime).hour()), CurrentItem, 3) + ":" +
BlinkString(String((*SetDateTime).minute()), CurrentItem, 4) + ":" +
BlinkString(String((*SetDateTime).second()), CurrentItem, 5);
LcdPrint(2, Ddate, CENTER);
LcdPrint(3, " ", CENTER);
RelayCheck();
}
void TMenu::OnKey(byte KeyNum)
{
switch (KeyNum)
{
case 3: // — if (ItemIsValue) Decrement();
else ChangeItem(-1);
break;
case 4: // +
if (ItemIsValue) Increment();
else ChangeItem(1);
break;
case 5: // SET
if (ItemIsValue)
{
OnSet();
ChangeItem(+1);
}
else // вход в подменю
{
if (Items && ItemsCount())
{
if (CurrentMenu->ItemsCount())
{
CurrentMenu = CurrentMenu->Items[CurrentMenu->CurrentItem];
CurrentMenu->CurrentItem = 0;
}
}
}
break;
default: // 2 -menu
if (Parent) CurrentMenu = CurrentMenu->Parent; //(TMenu *) &NoMenu;
else
{
CurrentMenu = SelectMenu;
CurrentMenu->CurrentItem = 0;
}
}
}
void TMenu::ChangeItem(byte value)
{
CurrentItem += value;
if (CurrentItem > 128) CurrentItem = ItemsCount() — 1;
else if (CurrentItem > ItemsCount() — 1) CurrentItem = 0;
}
boolean TMenu::AddItem(TMenu *NewItem)
{
if (!Items) Items = new TMenu *[_ItemsCount = 1];
else Items = (TMenu **)realloc((void *)Items, (_ItemsCount = _ItemsCount + 1) * sizeof(void *));
Items[_ItemsCount — 1] = NewItem;
}
DateTime TMenu::CheckDateTime(DateTime OldDate, int Increment, byte DatePart)
{
int DTmin[6] = {2000, 1, 1, 0, 0, 0};
int DTmax[6] = {2199, 12, 31, 23, 59, 59};
int DT[6];
int diff;
DT[0] = OldDate.year();
DT[1] = OldDate.month();
DT[2] = OldDate.day();
DT[3] = OldDate.hour();
DT[4] = OldDate.minute();
DT[5] = OldDate.second();
DT[DatePart] = DT[DatePart] + Increment;
if (DT[1] == 1 || DT[1] == 3 || DT[1] == 5 || DT[1] == 7 || DT[1] == 8 || DT[1] == 10 || DT[1] == 12) DTmax[2] = 31;
else if (DT[1] == 2)
{
if ((DT[0] % 4 == 0 && DT[0] % 100 != 0) || (DT[0] % 400 == 0)) DTmax[2] = 29;
else DTmax[2] = 28;
}
else DTmax[2] = 30;
for (byte i = 0; i < 6; i++)
{
if (DT[i] > DTmax[i]) DT[i] = DTmin[i];
else if (DT[i] < DTmin[i]) DT[i] = DTmax[i];
}
return DateTime(DT[0], DT[1], DT[2], DT[3], DT[4], DT[5]);
}
void TSelectMenu::Print(void)
{
NowDate = RTC.now();
byte shift = 0;
if (CurrentItem > 3) shift = CurrentItem — 3;
for (byte i = 0; i < 4; i++)
{
if ((CurrentItem — shift) == i) //&&Blinker)
{
LcdPrint(i, ">> " + * (Items[i + shift]->MenuName) + " <<", CENTER);
}
else LcdPrint(i, *(Items[i + shift]->MenuName), CENTER);
}
RelayCheck();
}
void TRelayMenu::Print(void)
{
String DData;
NowDate = RTC.now();
LcdPrint(0, (*MenuName) + "[" + BlinkString(RelayModeNames[RelayMode], CurrentItem, 0) + "]", CENTER);
DData = «On:»;
switch (RelayMode)
{
case 3: //Daily
// DData = DData + " ";
if (CurrentItem > 0 && CurrentItem < 4) CurrentItem = 4;
break;
default:
DData = DData + BlinkString(String(RelayOn.year(), DEC), CurrentItem, 1) + "/" + BlinkString(String( RelayOn.month(), DEC), CurrentItem, 2) +
"/" + BlinkString(String( RelayOn.day(), DEC), CurrentItem, 3);
}
DData = DData + " " + BlinkString(String (RelayOn.hour(), DEC), CurrentItem, 4) + ":" + BlinkString(String(RelayOn.minute(), DEC), CurrentItem, 5);
LcdPrint(1, DData, CENTER);
DData = «Off:»;
switch (RelayMode)
{
case 3: //Daily
// DData = DData + " ";
if (CurrentItem > 5 && CurrentItem < 9) CurrentItem = 9;
break;
default:
DData = DData + BlinkString(String(RelayOff.year(), DEC), CurrentItem, 6) + "/" + BlinkString(String( RelayOff.month(), DEC), CurrentItem, 7) +
"/" + BlinkString(String( RelayOff.day(), DEC), CurrentItem, 8);
}
DData = DData + " " + BlinkString(String (RelayOff.hour(), DEC), CurrentItem, 9) + ":" + BlinkString(String(RelayOff.minute(), DEC), CurrentItem, 10);
LcdPrint(2, DData, CENTER);
LcdPrint(3, " ", CENTER);
}
boolean TRelayMenu::CheckDaily(void)
{
int TimeOn = 60 * int(RelayOn.hour()) + int(RelayOn.minute());
int TimeOff = 60 * int(RelayOff.hour()) + int(RelayOff.minute());
int NowTime = 60 * int(NowDate.hour()) + int(NowDate.minute());
boolean result; // true = время включения больше времени выключения
if ( TimeOn > TimeOff )
{
if (NowTime <= TimeOff || NowTime >= TimeOn ) result = true;
else result = false;
}
else
{
if (NowTime <= TimeOff && NowTime >= TimeOn ) result = true;
else result = false;
};
return result;
}
void RelayCheck (void)
{
boolean OnceBitCheck;
for (byte i = 0; i < 4; i++)
{
switch (RelayMenu[i]->RelayMode)
{
case 1: //relay 0n
digitalWrite(i + 10, LOW);
break;
case 2: //Once;
OnceBitCheck = (NowDate.unixtime() > RelayMenu[i]->RelayOn.unixtime() && NowDate.unixtime()<RelayMenu[i]->RelayOff.unixtime());
if (OnceBitCheck) RelayMenu[i]->OnceBit = true;
else if (RelayMenu[i]->OnceBit)
{
RelayMenu[i]->RelayMode = 0;
byte p_address = RelayMenu[i]->RelayNumber * 16;
EEPROM.write(p_address, RelayMenu[i]->RelayMode);
}
digitalWrite(i + 10, !OnceBitCheck);
break;
case 3: //Daily
digitalWrite(i + 10, !(RelayMenu[i]->CheckDaily()));
break;
default: //relay 0ff
digitalWrite(i + 10, HIGH);
}
}
}
Вот такая получилась поделка, избавившая меня от необходимости вставать в 7 утра и ложиться в 23, при этом не забывая щелкать тумблерами. На промышленные стандарты не претендую, инкапсуляцию данных в коде делать не стал, глобальные переменные также оставил.
Были опасения в точности хода часов, но пока существенного отклонения не заметил.
Благодарю за внимание.
Комментарии (71)
viacheslav77 Автор
23.07.2015 10:02можно сделать красивее, но задача была быстро сделать максимально дешевый колхоз. В большинстве случаев логика работы забита в код.
telnov
23.07.2015 10:29В доме установлен двухтарифный счетчик, поэтому бойлер нагревает воду с 23 до 7 утра. Аналогично отопление: два из трех тэнов, по моей задумке будут включаться ночью.
Вы отапливаете дом электричеством??? Да Вы батенька богатей! Последний раз (лет 7-8 назад), когда я видел дом, который топят электричеством — это была загородная дача какого то футболиста Российской сборной у которого куры денег не клюют. Счета там были космические. Ну или вы электричество воруете. У вас нормальная дровяная печь то там есть? В случае если газа в доме нет и невозможно провести, обычно применяют или автоматический твердотопливный котел. Котельную во дворе устраивают. В бункер закидывается топливо на 2-7 дней (в зависимости от температуры воздуха). Или делается скважина и для отопления и горячей воды применяется тепловой насос. Однажды беседовал с обладателями такого девайса. Подтапливаться дровами приходится только в лютые морозы (-25 и ниже), в остальное время он справлялся. Но в том случае, я думаю что, проблема скорее всего была в недостаточной мощности устройства или в его неправильном монтаже.Alexeyslav
23.07.2015 11:15Все это актуально для отдельного дома, в квартире многое из этих вещей не применишь а центральное отопление не всегда вовремя работает.
telnov
23.07.2015 11:30Для квартиры тоже есть несколько решений. Ставится кондиционер с функцией отопление и в межсезонье(пока нет отопления) он работает как тепловой насос. Ну а от электрического бойлера никуда в квартире не убежишь. Разве что только газовый котел можно поставить.
Yurich
23.07.2015 12:17+1Скажите, пожалуйста, а Вы графики выхода на точку равнозатратности рисовать не пытались?
Я вот это делал. Электричество далеко не самый плохой вариант, ТН сильно хуже (если, конечно, его не из списанных деталей от кондеев делать).
А рекомендуемый Вами пеллетник в моем юскейсе начинает быть дешевле солярочного только на девятый год эксплуатации, но тут вот ведь какая ерунда: пеллетники служат где-то лет 15, после чего их нужно менять (горелки и шнековые приводы так и еще чаще), Дизельные котлы тоже столько же служат, но экономия на смене котлов и расходки получается весьма существенной. И это я еще не моделировал ситуацию, когда планируемые к затратам на пеллетник деньги (точнее их дельта между ТТ и ДТ) кладется в банк и проценты учитываются в смете общих расходов.
Что же до пеллет, то кВтч электричества в ночном тарифе стоит дешевле кВтч пеллетного, поэтому экономные граждане на ночь глушат ТТ котел и запускают электрический.
Да, и про котельную в отдельном строении: Вы, я уверен, считали расходы на возведение строения, регистрацию его в БТИ, налоги со строения и теплопотери в подземной теплотрассе в смете общих расходов на хотя бы 10-летнем отрезке. А, да, ну и, разумеется, не забыть бы еще обустройство прохода теплотрассы в основное здание из-под земли. Эта задача, прямо скажем, не из дешевых, особенно, если нет цокольного этажа.telnov
24.07.2015 11:26Я не пытался. Я не занимаюсь отоплением. Я рассказал только о том, что видел сам. Тепловой насос лучше, если только его устанавливали не Равшан и Джамшут из соседней стройки. Люди с которыми я разговаривал сначала тоже себе все электричеством сделали, но т.к. они жили в доме постоянно, то зимой счета за электричество доходили до 80-90 т.руб. в месяц(это 6 лет назад). Извините но невозможно зимой при температуре -12 — 25 не топить днем дом. После установки теплового насоса они стали платить максимально по 15-20 т.р. в зимние месяцы. По итогам имеем 2-3 года срок окупаемости по сравнению с электроотоплением. Вы мне про расчеты, а я вам про практику. Я к этим людям вообще по другому вопросу приезжал, просто увидел девайс и спросил. По поводу остальных вопросов. Я хочу сказать следующее. Если дом возможно отопить 15 кВт электрической мощности, то в принципе все ОК. Можно не заморачиваться и топить (хотя суммы счетов будут космическими). Но чаще всего если задействуется электроотопление, то мощность ввода приходится увеличивать. В Подмосковье, например, легко и почти бесплатно получается стандартное подключение 15 кВт на участок. Однако мощность свыше этого лимита стоит таких денег, которых хватит Вам и на постройку котельной, и на котел, и на топливо для этого котла на ближайшие 2-3 года.
Kitsok
23.07.2015 11:36Скажите пожалуйста, что будет, если реле, которым Вы включаете насос, залипнет?
Yurich
23.07.2015 12:20не знаком с этой релейной сборкой, но разве там электромагнитные реле?
Kitsok
23.07.2015 12:35Да это и не важно. Что будет с водопроводом, если реле залипнет в замкнутом состоянии?
Yurich
23.07.2015 12:36Да ничего не будет. Реле давления отключит насос, вот и всё.
Kitsok
23.07.2015 12:39А где Вы увидели реле давления в схеме? И зачем оно нужно, если есть ардуино? И зачем нужно ардуино, если есть реле давления?
Yurich
23.07.2015 12:44Верну Вам Ваш вопрос: где Вы увидели обработку датчика давления ардуиной?
Ардуина может оказаться полезной, если есть желание глушить насос по датчику протечки, например.Kitsok
23.07.2015 12:52+1Я не увидел вообще никакой безопасности в этом проекте. Совсем и полностью. И, наблюдая за похожими проектами, я крайне редко вижу элементы «правильного» подхода к автоматизации, когда надо закладываться на отказ любого элемента системы.
«Умный дом в массы» — очень правильный тренд, но забывание о надежности и отказоустойчивости (в классическом понимании) обернется минимум — финансовыми потерями, максимум — трагедиями.Yurich
23.07.2015 13:34Реле давления к безопасности отношение конечно имеет, но не самое прямое. Оно ставится всегда — просто без него водопровод в автономной системе водоснабщения работать не будет.
Кстати вот думаю, не замутить ли обзор реле давления для насосов — я уже четыре разных типа у себя затестдрайвил, но там ни ардуины ни кода — ни фига нет…Kitsok
23.07.2015 13:43У моих соседей на чердаке стоит 2 кубовые емкости и выключатель по уровню, прекрасно обходятся без реле давления.
Обзор РД — хорошая идея. Я тоже оттестил, в общей сложности, одно безымянное РД и два «контроллера». Причем один из них натурально самозатопился (потекла прокладка мембранной полости), и продолжал при этом функционировать.Yurich
23.07.2015 14:23Я бы не рискнул ставить на чердак две тонны воды.
Обрушившееся перекрытие с последующим разливом двух тонн воды в помещении это всегда очень дорого.
Особенно зимой.Kitsok
24.07.2015 11:49Очень странный аргумент. 2 тонны воды Вас на чердаке смущает, а пять тонн кровли — нет?
Yurich
24.07.2015 12:10Не смущает, разумеется. Снег на кровле это распределенная нагрузка, бак с водой распределен куда меньше.
Кроме того, межэтажные перекрытия редко строят с учетом такой нагрузки, как две тонны воды локально (не, если у Ваших знакомых монолит, тогда все ок).
eta4ever
23.07.2015 13:18+1Да, там простейшие электромагнитные реле, которые могут залипнуть.
Yurich
23.07.2015 13:29я б не рискнул такое в ответственные цепи ставить
eta4ever
23.07.2015 13:32Да можно ставить, только какую-то обратную связь надо сделать, с отрубанием всего нафиг в случае залипания.
Yurich
23.07.2015 13:35+1А отрубать будете еще одним реле? :)
Твердотельное реле рулит, у него залипания вообще не предусмотрено конструкцией. И обойдется дешевле, чем контроль залипания ЭМ-реле.eta4ever
23.07.2015 13:38Ну, контроль — штука вообще хорошая. Поставить датчик тока, и заодно будем знать потребление нагрузки.
А отрубать — да, еще одним реле. Или, скорее, пускателем на общем входе всего этого дела. Но в целом — да, симисторы тут будут предпочтительнее.Yurich
23.07.2015 13:43+1Я бы задавался вопросом «а что, если?» при проектировании системы безопасности.
Предположим, релюха включает свет в туалете. Залипла — ну и фиг с ней, не критично. Увижу, что свет горит, когда не должен, разберусь — небо на землю не упадет.
А вот поверхностный насос, принудительно запитанный в отсутствие тока воды, может доставить неприятностей, ему так работать неполезно. Довольно скоро он закипятит в себе воду и начнет разрушаться, ибо кавитация, ибо прокладка корпуса и сальник не предназначены для кипятка и т.д.Kitsok
23.07.2015 13:47Эх, не смогу показать, потерял ссылку на форуме, я там разбирал убийственный глубинный насос «российского» производителя. Там тоже не очень думали о безопасности.
В итоге, из-за неправильно выбранного материала нижнего подшипника вала, на нем образовалась каверна. Каверна забилась — насос заклинило, заклинило насос — пусковой ток перегрел и взорвал пусковой конденсатор в корпусе насоса. Во время взрыва и расплавления, фазный провод прилёг на корпус насоса.
Когда я вынул это чудо на поверхность и стал разбираться, то из трусости решил УЗО поставить прямо рядом, на случай «если что». Не поставил — не писал бы сейчас тут.Yurich
23.07.2015 13:58Аналогичная история приключилась, когда я крутил потроха китайскому поверхностному насосу. Не удивлен, если честно.
Сосед решил, что он самый умный в мире Карлсон, купил что-то экстремально дешевое, типа насосная станция в сборе за 2500 рублей. Все как положено, ГА с пластиковым фланцем, насос с пластиковым корпусом, дребезжащая автоматика и т.п.
Ну и прекратила она у него работать почти сразу: подшипники там были из смеси соломы с пластилином, ротор был почти заклинен, он меня позвал посмотреть, и тут, видимо ротор заклинило совсем. Ну а через это дело точно так же подорвался конденсатор, а дальше КЗ, оплавление проводов — вместо автомата у дедушки был нормальный жучок. Тут я уж не стал дожидаться финала и обесточил это все нахрен, мой дом рядом стоит, его жалко.
Alexeyslav
23.07.2015 16:09+2Симисторы имеют плохую особенность, они в любой момент могут самопроизвольно открыться, например от помехи по питанию.
Неоднократно сталкивался с тем что симистор открывается при внезапном КЗ в нагрузке, когда он должен быть закрыт.
Во время грозы, если не предпринять специальных мер они будут периодически открываться на пол периода а то и на целый.
Kitsok
23.07.2015 13:38+1Да, именно так. Причем на отдельное, аварийное реле, должен быть свой контроллер, считывающий непосредственно информацию с датчиков (возможно, не со всех), и самостоятельно принимающий решение о том, что «все пропало».
Твердотельное реле, конечно, не залипает. Зато оно может пробиться.
Не бывает безотказных деталей.Yurich
23.07.2015 13:44А как Вы планируете контролировать исправность аварийного реле?
Мне действительно это интересно.eta4ever
23.07.2015 13:48Мне пока в голову приходит только периодическая «учебная тревога».
Yurich
23.07.2015 13:49Вы же, наверное, сами понимаете, что Вам это надоест за полгода максимум.
Вот скажите, как часто Вы нажимаете кнопку «Тест» на УЗО у себя дома? Раз в полгода, как того требует любая инструкция?Kitsok
23.07.2015 13:50Я раз в год жму. И, что удивительно, они-таки да, выходят из строя. За 14 лет эксплуатации половина УЗО поменяно.
Yurich
23.07.2015 14:04Я жму два раза в год, кроме того, купил тестер для УЗО, кроме самотеста еще и им гоняю, чтоб знать, на каком токе утечки он срубается.
Штуковина выглядит вот так:
Покупал тут: http://www.castorama.fr/store/Testeur-VT35-Terre-et-diff-Multimetrix-prod4620001.html
Одно УЗО из трех установленных таки выбраковал.Aclz
24.07.2015 01:05+1Хм…
Вилка разборная Евро — 30 р.
Цементный резистор 6.8 кОм 10 Вт — ну ещё 5 р.
Соединяем резистором L- и PE-контакты внутри вилки и получаем тот же девайс за 35 рублей.
Имеем в щитке ещё и 10 мА УЗО? Ну тогда выкладываем ещё 35 рублей)Yurich
24.07.2015 08:28Ок, ок, ок. Вы себе сделали такую штуку? Я так понимаю, что вряд ли.
А у меня вот есть и я ей пользуюсь.
Не все проблемы надо решать паяльником. Некоторые можно и баблом — тупо быстрее.
Это уж я молчу о том, что если мы всерьез возьмемся бюджетировать полный аналог этого прибора, то по деньгам выйдет как минимум столько же.
Aclz
24.07.2015 11:50Судя по вашей забугорной ссылке в данном случае паяльником будет намного быстрее (отсилы пара часов). Было б надо было — сделал бы. Только надо ли? Были прецеденты? (не берем во внимание щиток, набитый ИЕКами и их родственниками или бюджетными сериями, типа шнайдеровского одномодульного двухполюсника, или купленными на развале — всё это надо не тестировать, а сразу менять).
Yurich
24.07.2015 12:13У меня весь щит на ИЭКах собран, разве что УЗО и вводной автомат — АВВ.
Все живет, все работает. Слухи про ненадежность ИЭК изрядно преувеличены. Могу Вам показать фотку выгоревшего дотла легранда, если желаете, сгорел вообще в номинальном режиме работы. Это не автомат, конечно, был, а устройство защиты от молний, но таки вот.
Тестер УЗО — ну я просто жил долгое время в той стране, мне до магазина было ближе, чем до метро. В РФ аналогичные приборы тоже продаются, кстати.Aclz
24.07.2015 13:02Ну вообще в инетернете есть тесты, разборы и т.д. А чисто на обывательском уровне: неделю назад случилось так, что в одной руке оказалась пачка из пяти С16-х Легранов DX3-E, в другой столько же аналогичных IEK-ов. Так вот, был немало удивлен, что последние по ощущениям оказались в 2 (!) раза легче, что как бы намекает о количестве металла внутри и исполнении в целом.
Kitsok
23.07.2015 13:50Никак не планирую. Эшелонированность защиты должна иметь предел.
Если совсем все правильно делать, то я бы выбил из производителей деталей исследования по надежности, прикинул бы потребную мне, посчитал бы общую надежность, и сделал бы столько эшелонов, сколько нужно, чтобы выйти на необходимый показатель.
По практике — коммутация одной цепи двумя реле + аварийное реле на весь модуль с контролем выходных цепей (после «исполнительных» реле). Стоит — копейки.
Alexeyslav
23.07.2015 16:05+1Есть там залипания, называется пробой.
Второе реле, которое будет всё это обесточивать к моменту залипания основного реле будет иметь гораздо меньшую наработку на количество переключений контактов, а значит его залипание в нужный момент времени маловероятно.
dkukushkin
23.07.2015 12:23+3Сделано добротно, однако электромагнитные реле лучше заменить на тиристорные, контакты которых не залипают даже в условиях сильных нагрузок. Типа такого:
eta4ever
23.07.2015 13:19Симисторные, но не суть. Или собрать самому, из оптосимистора MOC3063 и какого-нибудь мощного выходного.
Alexeyslav
23.07.2015 16:16Собственно, этот модуль и есть…
eta4ever
23.07.2015 16:18Ну, да, только с радиатором и клеммами. В принципе, они есть недорогие, иногда проще купить, чем городить.
dkukushkin
24.07.2015 10:49Симисторные, но не суть.
Симистop (симметричный триодный тиристор) — полупроводниковый прибор, являющийся разновидностью тиристоров… (Wiki)eta4ever
24.07.2015 13:15Строго говоря — да. И динистор туда же. Но они достаточно отличаются, чтобы иметь собственное название.
nochkin
23.07.2015 17:28На них надо ставить соответствующее охлаждение.
А поводу незалипания это не совсем так. Там могут быть пробои как и в любой электронике.dkukushkin
24.07.2015 10:54На них надо ставить соответствующее охлаждение.
Смотря какая нагрузка. У нас теплый пол работает через такую сборку + ATTiny, включается каждые 5 секунд на определенное время. Тиристорная сборка как на фото, написано 40 А, но при более слабой нагрузке радиатор добавлять не пришлось.
Недавно чел. описывал опыт включения обогревателя через реле. Реле сининькие (как в статье) работают несколько недель а то и меньше. Выход был найден — именно тиристорные сборки.eta4ever
24.07.2015 13:16Ну, на этих реле написано 10 или 16А, при этом они долго и хорошо работают на 1-2А, не больше. Проверено.
Alexeyslav
24.07.2015 15:49Потому что они не силовые. Если внимательно посмотреть в даташит, то эти 10-16А это максимальный рабочий ток который реле может выдержать, но ток коммутации гораздо меньше.
Есть такие же реле но которые нормально коммутируют токи до 5...7А, надо внимательно смотреть на указанную модель реле, силовые они еще и стоят дороже.eta4ever
24.07.2015 15:54Логично. Вообще, кстати, когда габариты позволяют, я стараюсь использовать реле в колодках на рейку (недавно узнал, что их кличут «промежуточными»).
nochkin
24.07.2015 18:54Реле, как на картинке выше, часто подделывают и поэтому они могут не тянуть нагрузку, которая указана на наклейке.
Можно найти много интересных картинок и историй по ключевым словам типа «fotek fake ssr». Поэтому надо быть аккуратнее в этом плане.
eta4ever
23.07.2015 13:22+2Раз уж поднята тема безопасности. Для того, чтобы система была хоть немного более правильной, не хватает следующего:
1) предохранителей;
2) темопредохранителей;
3) обратной связи по всему — по выходам, по напряжению питания.
То есть, сделано все аккуратно, но о некоторых вещах просто не подумали.
Aclz
23.07.2015 22:24Если в бойлере вдруг закончится горячая вода (ну захочет два человека помыться в ванной или ещё что) — то нагрев так и не начнётся до ночи? Если да, то польза от такой автоматизации сомнительна…
viacheslav77 Автор
24.07.2015 05:02+2Отвечаю по порядку.
1. Отопление комбинированное. Я не хоккеист с зарплатой 1 млн рублей, у меня дом 50 кв. м. За глаза хватает 6 кВт, из них 4 кВт включается на ночь. 4 кВт*8часов*30 дней* 0,69 руб/кВтч = 662 рубля в месяц. Я дурак? Альтернатива — дрова.
2. Это реле времени, а не умный дом. В его задачи входит только переключать режимы. Предохранители установлены в щитке, т.е. на входе. Термопредохранители и прочие защитные элементы установлены в штатной заводской системе управления бойлером и котлом т.е. на выходе. Реле включается в разрыв электроцепи. В случае залипания реле нагреватели будут работать в штатном режиме — будут греть воду или отапливать дом по установленным на штатной панели настройкам. В случае несрабатывания — из крана будут течь холодная вода, а дома будет немного прохладно.
3. Да реле те самые, залипающие. Тиристорные ставить не буду. Буду перемонтировать котел — поставлю контакторы в щиток.
4. Вода в бойлере закончится не может, изучите устройство бойлера, прежде чем давать советы. Забор идет из верхней точки. Выкипеть вода не может, так как выход бойлера — это кран, который в нормальном состоянии закрыт.
5. Обратную связь по всему делать не буду, я критерии устойчивости по Найквисту считать не умею.viacheslav77 Автор
24.07.2015 05:20прошу прощения, по диагонали прочитал, если закончится горячая вода — будет холодная. В штатном режиме хватает на две помывки тела и одну-две помывки посуды. По тех заданию такой расклад устраивает.
Yurich
24.07.2015 11:30Я полностью согласен со всеми Вашими тезисами, кроме одного: вода в бойлере таки может доставить неприятностей.
Но для этого надо, чтобы заклинило не только Ваше реле, надо, чтоб еще заклинил терморегулятор, а группа безопасности также заклинила или вовсе не была установлена.
Впечатляющий видос от разрушителей:
eta4ever
24.07.2015 13:18По 2-му пункту об этом стоило бы сказать в статье. У меня (и не только) сложилось впечатление, что на самоделку возлагается больше, чем на самом деле.
neochapay
Вот если коробочку не открывать то прям промышленный образец. Редко такие увидишь с ардуино…
TomashUA
Так то обычный электрический щиток с прозрачной дверцей.
neochapay
Ну у большинства всё остаётся в лапше торчащей из стены :)
Aclz
Это турецкий щиток Makel. Весьма стрёмный причем, т.к. для автоматов неудобный, а для колхозинга на Ардуине неоправдано глубокий, как и все щитки с DIN-рейкой внутри.