Newer
Older
smart-home-server / devices / sensor / bme280_sensor.cpp
#include "bme280_sensor.h"
#include <math.h>

Bme280Sensor::Bme280Sensor() {
}

Bme280Sensor::~Bme280Sensor() {
	if (_bme280 != nullptr) {
		delete _bme280;
		_bme280 = nullptr;
	}

	if (_bmp280 != nullptr) {
		delete _bmp280;
		_bmp280 = nullptr;
	}
}

bool Bme280Sensor::begin(TwoWire &wire, const Bme280Config &config) {
	_config = config;
	_wire   = &wire;

	/* Зажим параметров тренда */
	if (_config.temperature_trend_window_min < 2)            _config.temperature_trend_window_min = 2;
	if (_config.temperature_trend_window_min > TREND_MAX)    _config.temperature_trend_window_min = TREND_MAX;
	if (_config.pressure_trend_window_min < 2)               _config.pressure_trend_window_min = 2;
	if (_config.pressure_trend_window_min > TREND_MAX)       _config.pressure_trend_window_min = TREND_MAX;
	if (_config.humidity_trend_window_min < 2)               _config.humidity_trend_window_min = 2;
	if (_config.humidity_trend_window_min > TREND_MAX)       _config.humidity_trend_window_min = TREND_MAX;

	_wire->begin(_config.sda_pin, _config.scl_pin, 400000);

	/* На случай повторной инициализации */
	if (_bme280 != nullptr) { delete _bme280; _bme280 = nullptr; }
	if (_bmp280 != nullptr) { delete _bmp280; _bmp280 = nullptr; }

	_sensor_type           = BME_SENSOR_TYPE_UNKNOWN;
	_initialized           = false;
	_online                = false;
	_has_valid_data        = false;
	_last_read_attempt_ms  = 0;
	_last_success_read_ms  = 0;

	_temperature_c     = 0.0f;
	_pressure_hpa      = 0.0f;
	_humidity_percent  = 0.0f;

	_filtered_temperature_c    = 0.0f;
	_filtered_pressure_hpa     = 0.0f;
	_filtered_humidity_percent = 0.0f;

	/* Сброс трендов */
	_temp_trend_count = 0; _temp_trend_idx = 0; _temp_trend_last_ms = 0;
	_pres_trend_count = 0; _pres_trend_idx = 0; _pres_trend_last_ms = 0;
	_humi_trend_count = 0; _humi_trend_idx = 0; _humi_trend_last_ms = 0;
	_temp_trend_slope = 0.0f; _temp_trend_std_dev = 0.0f; _temp_trend_dir = 0;
	_pres_trend_slope = 0.0f; _pres_trend_std_dev = 0.0f; _pres_trend_dir = 0;
	_humi_trend_slope = 0.0f; _humi_trend_std_dev = 0.0f; _humi_trend_dir = 0;

	/* Сначала пробуем BME280 */
	if (init_bme280()) {
		_sensor_type = BME_SENSOR_TYPE_BME280;
		_initialized = true;
		_online      = true;
		return true;
	}

	/* Если не вышло — пробуем BMP280 */
	if (init_bmp280()) {
		_sensor_type = BME_SENSOR_TYPE_BMP280;
		_initialized = true;
		_online      = true;
		return true;
	}

	_sensor_type = BME_SENSOR_TYPE_UNKNOWN;
	return false;
}

/* ====================================================
   UPDATE
   ==================================================== */

void Bme280Sensor::update() {
	if (!_initialized) return;

	uint32_t now_ms = millis();

	if (now_ms - _last_read_attempt_ms < _config.read_interval_ms) return;

	_last_read_attempt_ms = now_ms;

	float temperature = 0.0f;
	float pressure    = 0.0f;
	float humidity    = 0.0f;

	if (_sensor_type == BME_SENSOR_TYPE_BME280) {
		if (_bme280 == nullptr) { _online = false; return; }

		temperature = _bme280->readTemperature();
		pressure    = _bme280->readPressure() / 100.0f;
		humidity    = _bme280->readHumidity();

		if (isnan(temperature) || isnan(pressure) || isnan(humidity)) {
			_online = false;
			return;
		}

		_temperature_c    = temperature;
		_pressure_hpa     = pressure;
		_humidity_percent = humidity;

		update_filtered_temperature(temperature);
		update_filtered_pressure(pressure);
		update_filtered_humidity(humidity);

	} else if (_sensor_type == BME_SENSOR_TYPE_BMP280) {
		if (_bmp280 == nullptr) { _online = false; return; }

		temperature = _bmp280->readTemperature();
		pressure    = _bmp280->readPressure() / 100.0f;

		if (isnan(temperature) || isnan(pressure)) {
			_online = false;
			return;
		}

		_temperature_c    = temperature;
		_pressure_hpa     = pressure;
		_humidity_percent = 0.0f;

		update_filtered_temperature(temperature);
		update_filtered_pressure(pressure);
	}

	_online         = true;
	_has_valid_data = true;
	_last_success_read_ms = now_ms;

	/* Обновляем тренды */
	_tick_temp_trend(now_ms);
	_tick_pres_trend(now_ms);
	if (_sensor_type == BME_SENSOR_TYPE_BME280) {
		_tick_humi_trend(now_ms);
	}
}

/* ====================================================
   СТАТУС
   ==================================================== */

bool Bme280Sensor::is_online()      const { return _online; }
bool Bme280Sensor::has_valid_data() const { return _has_valid_data; }

bool Bme280Sensor::is_stale() const {
	if (!_has_valid_data) return true;
	return (millis() - _last_success_read_ms) > _config.stale_after_ms;
}

BmeSensorType Bme280Sensor::get_sensor_type()        const { return _sensor_type; }
String        Bme280Sensor::get_sensor_type_string()  const {
	if (_sensor_type == BME_SENSOR_TYPE_BME280) return "bme280";
	if (_sensor_type == BME_SENSOR_TYPE_BMP280) return "bmp280";
	return "unknown";
}

float Bme280Sensor::get_temperature_c()    const { return _filtered_temperature_c; }
float Bme280Sensor::get_pressure_hpa()     const { return _filtered_pressure_hpa; }
float Bme280Sensor::get_humidity_percent() const { return _filtered_humidity_percent; }
bool  Bme280Sensor::has_humidity()         const { return _sensor_type == BME_SENSOR_TYPE_BME280; }

/* ====================================================
   ДИНАМИКА — публичные геттеры
   ==================================================== */

const char* Bme280Sensor::get_temperature_dynamics() const {
	return _dynamics_string(_temp_trend_dir, _temp_trend_std_dev,
	                        _config.temperature_variable_std, _temp_trend_count);
}

const char* Bme280Sensor::get_pressure_dynamics() const {
	return _dynamics_string(_pres_trend_dir, _pres_trend_std_dev,
	                        _config.pressure_variable_std, _pres_trend_count);
}

const char* Bme280Sensor::get_humidity_dynamics() const {
	return _dynamics_string(_humi_trend_dir, _humi_trend_std_dev,
	                        _config.humidity_variable_std, _humi_trend_count);
}

float Bme280Sensor::get_temperature_dynamics_val() const { return _temp_trend_slope; }
float Bme280Sensor::get_pressure_dynamics_val()    const { return _pres_trend_slope; }
float Bme280Sensor::get_humidity_dynamics_val()    const { return _humi_trend_slope; }

/* ====================================================
   JSON
   ==================================================== */

String Bme280Sensor::get_temperature_json() const {
	String json = "{";
	json += "\"online\":"        + String(_online ? "true" : "false") + ",";
	json += "\"current\":"       + String(_filtered_temperature_c, 2) + ",";
	json += "\"dynamics\":\""    + String(get_temperature_dynamics()) + "\",";
	json += "\"dynamics_val\":"  + String(get_temperature_dynamics_val(), 2);
	json += "}";
	return json;
}

String Bme280Sensor::get_pressure_json() const {
	String json = "{";
	json += "\"online\":"        + String(_online ? "true" : "false") + ",";
	json += "\"current\":"       + String(_filtered_pressure_hpa, 2) + ",";
	json += "\"dynamics\":\""    + String(get_pressure_dynamics()) + "\",";
	json += "\"dynamics_val\":"  + String(get_pressure_dynamics_val(), 2);
	json += "}";
	return json;
}

String Bme280Sensor::get_humidity_json() const {
	String json = "{";
	json += "\"online\":"        + String(_online ? "true" : "false") + ",";
	json += "\"current\":"       + String(_filtered_humidity_percent, 2) + ",";
	json += "\"dynamics\":\""    + String(get_humidity_dynamics()) + "\",";
	json += "\"dynamics_val\":"  + String(get_humidity_dynamics_val(), 2);
	json += "}";
	return json;
}

String Bme280Sensor::get_state_json() const {
	String json = "{";
	json += "\"sensor\":\""       + get_sensor_type_string() + "\",";
	json += "\"online\":"         + String(_online ? "true" : "false") + ",";
	json += "\"has_valid_data\":" + String(_has_valid_data ? "true" : "false") + ",";
	json += "\"stale\":"          + String(is_stale() ? "true" : "false") + ",";
	json += "\"i2c_address\":\"0x" + String(_config.i2c_address, HEX) + "\",";
	json += "\"temperature\":"    + get_temperature_json() + ",";
	json += "\"pressure\":"       + get_pressure_json();
	if (has_humidity()) {
		json += ",\"humidity\":"  + get_humidity_json();
	}
	json += "}";
	return json;
}

/* ====================================================
   ИНИЦИАЛИЗАЦИЯ ЧИПОВ
   ==================================================== */

bool Bme280Sensor::init_bme280() {
	if (_wire == nullptr) return false;

	_bme280 = new Adafruit_BME280();
	if (_bme280 == nullptr) return false;

	bool ok = _bme280->begin(_config.i2c_address, _wire);
	if (!ok) {
		delete _bme280;
		_bme280 = nullptr;
		return false;
	}

	_bme280->setSampling(
		Adafruit_BME280::MODE_NORMAL,
		Adafruit_BME280::SAMPLING_X2,
		Adafruit_BME280::SAMPLING_X16,
		Adafruit_BME280::SAMPLING_X1,
		Adafruit_BME280::FILTER_X4,
		Adafruit_BME280::STANDBY_MS_500
	);
	return true;
}

bool Bme280Sensor::init_bmp280() {
	if (_wire == nullptr) return false;

	_bmp280 = new Adafruit_BMP280(_wire);
	if (_bmp280 == nullptr) return false;

	bool ok = _bmp280->begin(_config.i2c_address);
	if (!ok) {
		delete _bmp280;
		_bmp280 = nullptr;
		return false;
	}

	_bmp280->setSampling(
		Adafruit_BMP280::MODE_NORMAL,
		Adafruit_BMP280::SAMPLING_X2,
		Adafruit_BMP280::SAMPLING_X16,
		Adafruit_BMP280::FILTER_X4,
		Adafruit_BMP280::STANDBY_MS_500
	);
	return true;
}

/* ====================================================
   EMA-ФИЛЬТРЫ
   ==================================================== */

void Bme280Sensor::update_filtered_temperature(float value) {
	if (!_has_valid_data || _filtered_temperature_c == 0.0f) {
		_filtered_temperature_c = value;
		return;
	}
	_filtered_temperature_c =
		(_filtered_temperature_c * (1.0f - _config.temperature_ema_alpha)) +
		(value                   *         _config.temperature_ema_alpha);
}

void Bme280Sensor::update_filtered_pressure(float value) {
	if (!_has_valid_data || _filtered_pressure_hpa == 0.0f) {
		_filtered_pressure_hpa = value;
		return;
	}
	_filtered_pressure_hpa =
		(_filtered_pressure_hpa * (1.0f - _config.pressure_ema_alpha)) +
		(value                  *         _config.pressure_ema_alpha);
}

void Bme280Sensor::update_filtered_humidity(float value) {
	if (!_has_valid_data || _filtered_humidity_percent == 0.0f) {
		_filtered_humidity_percent = value;
		return;
	}
	_filtered_humidity_percent =
		(_filtered_humidity_percent * (1.0f - _config.humidity_ema_alpha)) +
		(value                      *         _config.humidity_ema_alpha);
}

/* ====================================================
   ТИКИ ТРЕНДА (каждую минуту)
   ==================================================== */

void Bme280Sensor::_tick_temp_trend(uint32_t now_ms) {
	if (_temp_trend_last_ms == 0) { _temp_trend_last_ms = now_ms; return; }
	if (now_ms - _temp_trend_last_ms < 60000u) return;
	_temp_trend_last_ms = now_ms;

	uint8_t win = _config.temperature_trend_window_min;
	_temp_trend_buf[_temp_trend_idx] = _filtered_temperature_c;
	_temp_trend_idx = (_temp_trend_idx + 1) % win;
	if (_temp_trend_count < win) _temp_trend_count++;

	_compute_trend(
		_temp_trend_buf, win, _temp_trend_count, _temp_trend_idx,
		_config.temperature_trend_threshold, _config.temperature_variable_std,
		_temp_trend_slope, _temp_trend_std_dev, _temp_trend_dir);
}

void Bme280Sensor::_tick_pres_trend(uint32_t now_ms) {
	if (_pres_trend_last_ms == 0) { _pres_trend_last_ms = now_ms; return; }
	if (now_ms - _pres_trend_last_ms < 60000u) return;
	_pres_trend_last_ms = now_ms;

	uint8_t win = _config.pressure_trend_window_min;
	_pres_trend_buf[_pres_trend_idx] = _filtered_pressure_hpa;
	_pres_trend_idx = (_pres_trend_idx + 1) % win;
	if (_pres_trend_count < win) _pres_trend_count++;

	_compute_trend(
		_pres_trend_buf, win, _pres_trend_count, _pres_trend_idx,
		_config.pressure_trend_threshold, _config.pressure_variable_std,
		_pres_trend_slope, _pres_trend_std_dev, _pres_trend_dir);
}

void Bme280Sensor::_tick_humi_trend(uint32_t now_ms) {
	if (_humi_trend_last_ms == 0) { _humi_trend_last_ms = now_ms; return; }
	if (now_ms - _humi_trend_last_ms < 60000u) return;
	_humi_trend_last_ms = now_ms;

	uint8_t win = _config.humidity_trend_window_min;
	_humi_trend_buf[_humi_trend_idx] = _filtered_humidity_percent;
	_humi_trend_idx = (_humi_trend_idx + 1) % win;
	if (_humi_trend_count < win) _humi_trend_count++;

	_compute_trend(
		_humi_trend_buf, win, _humi_trend_count, _humi_trend_idx,
		_config.humidity_trend_threshold, _config.humidity_variable_std,
		_humi_trend_slope, _humi_trend_std_dev, _humi_trend_dir);
}

/* ====================================================
   ЛИНЕЙНАЯ РЕГРЕССИЯ (статическая)
   ==================================================== */

void Bme280Sensor::_compute_trend(
	const float* buf, uint8_t win, uint8_t count, uint8_t idx,
	float threshold, float variable_std,
	float &out_slope, float &out_std_dev, int8_t &out_dir)
{
	if (count < 2) {
		out_slope   = 0.0f;
		out_std_dev = 0.0f;
		out_dir     = 0;
		return;
	}

	uint8_t n = count;
	float sx = 0.0f, sy = 0.0f, sxx = 0.0f, sxy = 0.0f, mean_y = 0.0f;

	for (uint8_t i = 0; i < n; i++) {
		/*
			Элементы в кольцевом буфере идут от самого старого к новому.
			idx указывает на следующую позицию записи, т.е. самый старый
			элемент находится по смещению (idx - count + win) % win.
		*/
		uint8_t bi = (uint8_t)((idx - n + i + win) % win);
		float x = (float)i;
		float y = buf[bi];
		sx     += x;
		sy     += y;
		sxx    += x * x;
		sxy    += x * y;
		mean_y += y;
	}

	float fn    = (float)n;
	float denom = fn * sxx - sx * sx;

	if (fabsf(denom) < 1e-9f) {
		out_slope = 0.0f;
		out_dir   = 0;
	} else {
		float slope = (fn * sxy - sx * sy) / denom;
		out_slope = slope;
		out_dir   = (slope >  threshold) ?  1 :
		            (slope < -threshold) ? -1 : 0;
	}

	/* Стандартное отклонение от среднего */
	mean_y /= fn;
	float var = 0.0f;
	for (uint8_t i = 0; i < n; i++) {
		uint8_t bi = (uint8_t)((idx - n + i + win) % win);
		float d = buf[bi] - mean_y;
		var += d * d;
	}
	out_std_dev = sqrtf(var / fn);
}

/* ====================================================
   СТРОКА ДИНАМИКИ (статическая)
   ==================================================== */

const char* Bme280Sensor::_dynamics_string(
	int8_t dir, float std_dev, float variable_std, uint8_t count)
{
	if (count < 2)                            return "constant";
	if (dir == 0 && std_dev > variable_std)   return "variable";
	if (dir > 0)                              return "increasing";
	if (dir < 0)                              return "decreasing";
	return "constant";
}