Skip to content

Commit

Permalink
#26 #22 auto co2 implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
dentra committed Mar 22, 2024
1 parent 35a4b48 commit 8a63671
Show file tree
Hide file tree
Showing 66 changed files with 1,626 additions and 841 deletions.
5 changes: 4 additions & 1 deletion CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,15 +477,18 @@ climate:

Дополнительные параметры:
* `enable_heat_cool`, тип boolean - включает/выключает дополнительный режим HEAT_COOL,
позволяющий ключать бризер, например, через сервис turn_on, с восстановлением
позволяющий включать бризер, например, через сервис turn_on, с восстановлением
предыдущего режима обогрева. По умолчанию: False.
* `enable_fan_auto` - тип boolean - включает/выключает дополнительный режим вентиляции "auto",
позволяющий включать автоматический режим вентиляции. По умолчанию: False.

Пример использования:
```yaml
climate:
- platform: tion
name: Climate
enable_heat_cool: True
enable_fan_auto: True
```

## Домен [fan]
Expand Down
2 changes: 1 addition & 1 deletion README.md.j2
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ English version of this page is available via [google translate](https://github-
* Обогрев
* Целевая температуры нагрева
* Скорость притока воздуха
* Звуковое оповещение (кроме O2)
* Звуковое оповещение (для O2 только если физически включено на бризере)
* Световое оповещение (только для 4S и Lite)
* Режим приток/рециркуляция (только для 4S и 3S)
* Режим приток/рециркуляция/смешанный (только для 3S)
Expand Down
56 changes: 56 additions & 0 deletions components/tion-api/pi_controller.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#include "pi_controller.h"

namespace dentra {
namespace tion {
namespace auto_co2 {

void PIController::reset(float kp, float ti, int db) {
this->kp_ = kp;
this->ti_ = ti;
this->db_ = db;
this->reset();
}

void PIController::reset(float kp, float ti, int db, float min, float max) {
this->kp_ = kp;
this->ti_ = ti;
this->db_ = db;
this->v_oa_min_ = min;
this->v_oa_max_ = max;
this->reset();
}

float PIController::update(int setpoint, int current) {
// 7: error from setpoint [ppm]
const auto e = setpoint - current;

// 8: error from setpoint, including dead band [ppm]
const auto e_db = //
/**/ current < setpoint - this->db_ ? e - this->db_ :
/**/ current > setpoint + this->db_ ? e + this->db_
: 0;

// 9: integral error [min.ppm-CO2]
const float i = this->ib_ + (this->dt_() / 60.0f) * e_db;

// 10: candidate outdoor airflow rate [L/s]
const float v_oa_c = -this->kp_ * (e_db + (i / this->ti_));

// 11: integral error with anti-integral windup [min.ppm-CO2]
if (v_oa_c < this->v_oa_min_) {
this->ib_ = -this->ti_ * (e_db + (this->v_oa_min_ / this->kp_));
} else if (v_oa_c > this->v_oa_max_) {
this->ib_ = -this->ti_ * (e_db + (this->v_oa_max_ / this->kp_));
} else {
this->ib_ = i;
}

// 12: outdoor airflow rate [L/s]
const float v_oa = -this->kp_ * (e_db + (this->ib_ / this->ti_));

return v_oa;
}

} // namespace auto_co2
} // namespace tion
} // namespace dentra
65 changes: 65 additions & 0 deletions components/tion-api/pi_controller.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#pragma once

#include <cstdint>
#include <cmath>

#include "utils.h"

namespace dentra {
namespace tion {
namespace auto_co2 {

/// @brief PI Controller.
/// @link https://www.sciencedirect.com/science/article/pii/S0378778823009477
class PIController {
public:
/// @param kp proportional gain [L/s.ppm-CO2]
/// @param ti integral gain [min]
/// @param db dead band [ppm]
/// @param v_oa_min minimum outdoor airflow rate [L/s]
/// @param v_oa_max maximum outdoor airflow rate [L/s]
PIController(float kp, float ti, int db = 0, float min = NAN, float max = NAN)
: kp_(kp), ti_(ti), db_(db), v_oa_min_(min), v_oa_max_(max) {}

/// @param setpoint CO2 setpoint [ppm]
/// @param current CO2 concentration [ppm]
/// @return outdoor airflow rate [L/s]
float update(int setpoint, int current);

void set_min(float min) { this->v_oa_min_ = min; }
void set_max(float max) { this->v_oa_min_ = max; }

/// @brief Resets integral error.
void reset() { this->ib_ = 0; }
/// @brief Resets Kp, Ti, db without touching min and max
void reset(float kp, float ti, int db);
void reset(float kp, float ti, int db, float min, float max);

protected:
/// proportional gain [L/s.ppm-CO2]
float kp_;
/// integral gain [min]
float ti_;
/// dead band [ppm]
int db_;
/// minimum outdoor airflow rate [L/s]
float v_oa_min_;
/// maximum outdoor airflow rate [L/s]
float v_oa_max_;
/// integral error with anti-integral windup [min.ppm-CO2].
float ib_{};
/// last time used in dt_() calculation.
uint32_t last_time_{};

/// time step [s].
float dt_() {
const auto now = tion::millis();
const float dt = this->last_time_ == 0 ? 0.0f : (now - this->last_time_) * 0.001f;
this->last_time_ = now;
return dt;
}
};

} // namespace auto_co2
} // namespace tion
} // namespace dentra
52 changes: 30 additions & 22 deletions components/tion-api/tion-api-3s-internal.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#pragma once

#include <cstdint>
#include "tion-api.h"
#include "tion-api-internal.h"

namespace dentra {
namespace tion_3s {
Expand Down Expand Up @@ -124,7 +124,7 @@ struct tion3s_state_set_t {
// Байт 1. Целевая температура нагрева.
int8_t target_temperature;
// Байт 2. Состояние затворки.
uint8_t /*tion3s_state_t::GatePosition*/ gate_position;
tion3s_state_t::GatePosition gate_position;
// Байт 3-4. Флаги.
tion3s_state_t::Flags flags;
// Байт 5-7. Управление фильтрами.
Expand All @@ -143,26 +143,34 @@ struct tion3s_state_set_t {
// 0 1 2 3 4 5 6 7 8 9
// 3D:02 01 17 02 0A.01 02.00.00 00 00 00:00:00:00:00:00:00:5A
static tion3s_state_set_t create(const tion::TionState &state) {
tion3s_state_set_t st_set{};

st_set.flags.power_state = state.power_state;
st_set.flags.heater_state = state.heater_state;
st_set.flags.sound_state = state.sound_state;
st_set.flags.ma_auto = state.auto_state;

st_set.fan_speed = state.fan_speed;
st_set.target_temperature = state.target_temperature;
st_set.gate_position = //-//
state.gate_position == tion::TionGatePosition::INDOOR //-//
? tion3s_state_t::GATE_POSITION_INDOOR //-//
: state.gate_position == tion::TionGatePosition::MIXED //-//
? tion3s_state_t::GATE_POSITION_MIXED //-//
: tion3s_state_t::GATE_POSITION_OUTDOOR; //-//

// в tion remote всегда выставляется этот бит
st_set.flags.preset_state = true;

return st_set;
return tion3s_state_set_t{
.fan_speed = state.fan_speed,
.target_temperature = state.target_temperature,
.gate_position = state.gate_position == tion::TionGatePosition::INDOOR //-//
? tion3s_state_t::GATE_POSITION_INDOOR //-//
: state.gate_position == tion::TionGatePosition::MIXED //-//
? tion3s_state_t::GATE_POSITION_MIXED //-//
: tion3s_state_t::GATE_POSITION_OUTDOOR, //-//

.flags =
{
.heater_state = state.heater_state,
.power_state = state.power_state,
.timer_state = {},
.sound_state = state.sound_state,
.ma_auto = state.auto_state,
.ma_connected = {}, // state.auto_state
.save = {},
.ma_pairing = {},
// в tion remote всегда выставляется этот бит
.preset_state = true,
.presets_state = {},
.reserved = {},
},
.filter_time = {},
.factory_reset = {},
.service_mode = {},
};
}
};

Expand Down
59 changes: 28 additions & 31 deletions components/tion-api/tion-api-3s.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ using namespace tion_3s;

static const char *const TAG = "tion-api-3s";

static const uint8_t PROD[] = {0, TION_3S_AUTO_PROD};

struct Tion3sTimersResponse {
struct {
uint8_t hours;
Expand All @@ -43,19 +45,16 @@ void Tion3sApi::read_frame(uint16_t frame_type, const void *frame_data, size_t f
// TION_LOGW(TAG, "Incorrect state data size: %zu", frame_data_size);
// return;
// }

if (frame_type == FRAME_TYPE_RSP(FRAME_TYPE_STATE_GET)) {
TION_LOGD(TAG, "Response[] State Get");
auto *frame = static_cast<const tion3s_state_t *>(frame_data);
this->update_state_(*frame);
TION_LOGD(TAG, "Response State Get");
this->update_state_(*static_cast<const tion3s_state_t *>(frame_data));
this->notify_state_(0);
} else if (frame_type == FRAME_TYPE_RSP(FRAME_TYPE_STATE_SET)) {
TION_LOGD(TAG, "Response[] State Set");
auto *frame = static_cast<const tion3s_state_t *>(frame_data);
this->update_state_(*frame);
TION_LOGD(TAG, "Response State Set");
this->update_state_(*static_cast<const tion3s_state_t *>(frame_data));
this->notify_state_(0);
} else if (frame_type == FRAME_TYPE_RSP(FRAME_TYPE_TIMERS_GET)) {
TION_LOGD(TAG, "Response[] Timers: %s", hexencode(frame_data, frame_data_size).c_str());
TION_LOGD(TAG, "Response Timers: %s", hex_cstr(frame_data, frame_data_size));
// структура Tion3sTimersResponse
} else if (frame_type == FRAME_TYPE_RSP(FRAME_TYPE_SRV_MODE_SET)) {
// есть подозрение, что актуальными являеются первые два байта,
Expand All @@ -76,39 +75,39 @@ void Tion3sApi::read_frame(uint16_t frame_type, const void *frame_data, size_t f
// [17:38:16][V][vport:011]: VRX: B3.40.11.00.08.00.08.00.08.00.00.00.00.00.00.00.00.00.00 (19)
// [17:38:21][V][vport:015]: VTX: 3D.01
// [17:38:21][V][vport:011]: VRX: B3.10.21.19.02.00.19.19.17.68.01.0F.06.00.1E.00.00.3C.00 (19)
TION_LOGD(TAG, "Response[] Pair: %s", hexencode(frame_data, frame_data_size).c_str());
TION_LOGD(TAG, "Response Pair: %s", hex_cstr(frame_data, frame_data_size));
} else {
TION_LOGW(TAG, "Unsupported frame %04X: %s", frame_type, hexencode(frame_data, frame_data_size).c_str());
TION_LOGW(TAG, "Unsupported frame %04X: %s", frame_type, hex_cstr(frame_data, frame_data_size));
}
}

bool Tion3sApi::pair() const {
TION_LOGD(TAG, "Request[] Pair");
TION_LOGD(TAG, "Request Pair");
const struct {
uint8_t pair;
} PACKED pair{.pair = 1};
return this->write_frame(FRAME_TYPE_REQ(FRAME_TYPE_SRV_MODE_SET), pair);
}

bool Tion3sApi::request_state_() const {
TION_LOGD(TAG, "Request[] State Get");
TION_LOGD(TAG, "Request State Get");
return this->write_frame(FRAME_TYPE_REQ(FRAME_TYPE_STATE_GET));
}

bool Tion3sApi::write_state(const tion::TionState &state, uint32_t request_id __attribute__((unused))) const {
TION_LOGD(TAG, "Request[] State Set");
if (!state.is_initialized(this->traits_)) {
TION_LOGW(TAG, "State is not initialized");
bool Tion3sApi::write_state_(const tion::TionState &state) const {
TION_LOGD(TAG, "Request State Set");
if (!state.is_initialized()) {
TION_LOGW(TAG, "State was not initialized");
return false;
}
auto st_set = tion3s_state_set_t::create(state);
return this->write_frame(FRAME_TYPE_REQ(FRAME_TYPE_STATE_SET), st_set);
}

bool Tion3sApi::reset_filter(const tion::TionState &state) const {
TION_LOGD(TAG, "Request[] Filter Time Reset");
if (!state.is_initialized(this->traits_)) {
TION_LOGW(TAG, "State is not initialized");
bool Tion3sApi::reset_filter_(const tion::TionState &state) const {
TION_LOGD(TAG, "Request Filter Time Reset");
if (!state.is_initialized()) {
TION_LOGW(TAG, "State was not initialized");
return false;
}

Expand All @@ -132,31 +131,29 @@ bool Tion3sApi::reset_filter(const tion::TionState &state) const {
}

bool Tion3sApi::request_command4() const {
TION_LOGD(TAG, "Request[] Timers");
TION_LOGD(TAG, "Request Timers");
return this->write_frame(FRAME_TYPE_REQ(FRAME_TYPE_TIMERS_GET));
}

bool Tion3sApi::factory_reset(const tion::TionState &state) const {
TION_LOGD(TAG, "Request[] Factory Reset");
if (!state.is_initialized(this->traits_)) {
TION_LOGW(TAG, "State is not initialized");
bool Tion3sApi::factory_reset_(const tion::TionState &state) const {
TION_LOGD(TAG, "Request Factory Reset");
if (!state.is_initialized()) {
TION_LOGW(TAG, "State was not initialized");
return false;
}
auto st_set = tion3s_state_set_t::create(state);
st_set.factory_reset = true;
return this->write_frame(FRAME_TYPE_REQ(FRAME_TYPE_STATE_SET), st_set);
}

void Tion3sApi::request_state() { this->request_state_(); }

Tion3sApi::Tion3sApi() {
this->traits_.errors_decoder = tion3s_state_t::decode_errors;

this->traits_.supports_sound_state = true;
this->traits_.supports_gate_position_change = true;
this->traits_.supports_gate_position_change_mixed = true;
#ifdef TION_ENABLE_ANTIFRIZE
this->traits_.supports_antifrize = true;
this->traits_.supports_manual_antifrize = true;
#endif
this->traits_.supports_reset_filter = true;
this->traits_.max_heater_power = TION_3S_HEATER_POWER / 10;
Expand All @@ -171,11 +168,11 @@ Tion3sApi::Tion3sApi() {
this->traits_.max_fan_power[4] = TION_3S_MAX_FAN_POWER4;
this->traits_.max_fan_power[5] = TION_3S_MAX_FAN_POWER5;
this->traits_.max_fan_power[6] = TION_3S_MAX_FAN_POWER6;

this->traits_.auto_prod = PROD;
}

void Tion3sApi::update_state_(const tion_3s::tion3s_state_t &state) {
this->traits_.initialized = true;

this->state_.power_state = state.flags.power_state;
this->state_.heater_state = state.flags.heater_state;
this->state_.sound_state = state.flags.sound_state;
Expand All @@ -199,7 +196,7 @@ void Tion3sApi::update_state_(const tion_3s::tion3s_state_t &state) {
this->state_.target_temperature = state.target_temperature;
this->state_.productivity = state.productivity;
// this->state_.heater_var = state.heater_var;
this->state_.work_time = tion_millis() / 1000;
this->state_.work_time = tion::millis() / 1000;
// this->state_.fan_time = state.counters.fan_time;
this->state_.filter_time_left = uint32_t(state.filter_time > 360 ? 1 : state.filter_time) * (24 * 3600);
// this->state_.airflow_counter = state.counters.airflow_counter;
Expand Down
Loading

0 comments on commit 8a63671

Please sign in to comment.