mirror of
https://github.com/esphome/esphome.git
synced 2024-11-21 22:48:10 +01:00
Add Uponor Smatrix component (#5769)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
76a3ffc8a9
commit
58c0d8c267
21 changed files with 796 additions and 4 deletions
|
@ -365,6 +365,7 @@ esphome/components/uart/button/* @ssieb
|
|||
esphome/components/ufire_ec/* @pvizeli
|
||||
esphome/components/ufire_ise/* @pvizeli
|
||||
esphome/components/ultrasonic/* @OttoWinter
|
||||
esphome/components/uponor_smatrix/* @kroimon
|
||||
esphome/components/vbus/* @ssieb
|
||||
esphome/components/veml3235/* @kbx81
|
||||
esphome/components/version/* @esphome/core
|
||||
|
|
|
@ -4,6 +4,7 @@ from esphome.components import sensor, uart
|
|||
from esphome.const import (
|
||||
CONF_CURRENT,
|
||||
CONF_ENERGY,
|
||||
CONF_EXTERNAL_TEMPERATURE,
|
||||
CONF_ID,
|
||||
CONF_POWER,
|
||||
CONF_VOLTAGE,
|
||||
|
@ -24,7 +25,6 @@ from esphome.const import (
|
|||
DEPENDENCIES = ["uart"]
|
||||
|
||||
CONF_INTERNAL_TEMPERATURE = "internal_temperature"
|
||||
CONF_EXTERNAL_TEMPERATURE = "external_temperature"
|
||||
|
||||
bl0940_ns = cg.esphome_ns.namespace("bl0940")
|
||||
BL0940 = bl0940_ns.class_("BL0940", cg.PollingComponent, uart.UARTDevice)
|
||||
|
|
|
@ -2,6 +2,7 @@ import esphome.codegen as cg
|
|||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor
|
||||
from esphome.const import (
|
||||
CONF_EXTERNAL_TEMPERATURE,
|
||||
CONF_ID,
|
||||
CONF_SPEED,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
|
@ -16,7 +17,6 @@ from .. import EMC2101_COMPONENT_SCHEMA, CONF_EMC2101_ID, emc2101_ns
|
|||
DEPENDENCIES = ["emc2101"]
|
||||
|
||||
CONF_INTERNAL_TEMPERATURE = "internal_temperature"
|
||||
CONF_EXTERNAL_TEMPERATURE = "external_temperature"
|
||||
CONF_DUTY_CYCLE = "duty_cycle"
|
||||
|
||||
EMC2101Sensor = emc2101_ns.class_("EMC2101Sensor", cg.PollingComponent)
|
||||
|
|
|
@ -3,6 +3,7 @@ import esphome.config_validation as cv
|
|||
from esphome.components import sensor, esp32_ble_tracker
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_LEVEL,
|
||||
CONF_EXTERNAL_TEMPERATURE,
|
||||
CONF_HUMIDITY,
|
||||
CONF_MAC_ADDRESS,
|
||||
CONF_TEMPERATURE,
|
||||
|
@ -19,8 +20,6 @@ from esphome.const import (
|
|||
CODEOWNERS = ["@fkirill"]
|
||||
DEPENDENCIES = ["esp32_ble_tracker"]
|
||||
|
||||
CONF_EXTERNAL_TEMPERATURE = "external_temperature"
|
||||
|
||||
inkbird_ibsth1_mini_ns = cg.esphome_ns.namespace("inkbird_ibsth1_mini")
|
||||
InkbirdIbstH1Mini = inkbird_ibsth1_mini_ns.class_(
|
||||
"InkbirdIbstH1Mini", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
|
||||
|
|
78
esphome/components/uponor_smatrix/__init__.py
Normal file
78
esphome/components/uponor_smatrix/__init__.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import uart, time
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_ID,
|
||||
CONF_TIME_ID,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@kroimon"]
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
MULTI_CONF = True
|
||||
|
||||
uponor_smatrix_ns = cg.esphome_ns.namespace("uponor_smatrix")
|
||||
UponorSmatrixComponent = uponor_smatrix_ns.class_(
|
||||
"UponorSmatrixComponent", cg.Component, uart.UARTDevice
|
||||
)
|
||||
UponorSmatrixDevice = uponor_smatrix_ns.class_(
|
||||
"UponorSmatrixDevice", cg.Parented.template(UponorSmatrixComponent)
|
||||
)
|
||||
|
||||
CONF_UPONOR_SMATRIX_ID = "uponor_smatrix_id"
|
||||
CONF_TIME_DEVICE_ADDRESS = "time_device_address"
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(UponorSmatrixComponent),
|
||||
cv.Optional(CONF_ADDRESS): cv.hex_uint16_t,
|
||||
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
cv.Optional(CONF_TIME_DEVICE_ADDRESS): cv.hex_uint16_t,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"uponor_smatrix",
|
||||
baud_rate=19200,
|
||||
require_tx=True,
|
||||
require_rx=True,
|
||||
data_bits=8,
|
||||
parity=None,
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
# A schema to use for all Uponor Smatrix devices
|
||||
UPONOR_SMATRIX_DEVICE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_UPONOR_SMATRIX_ID): cv.use_id(UponorSmatrixComponent),
|
||||
cv.Required(CONF_ADDRESS): cv.hex_uint16_t,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
cg.add_global(uponor_smatrix_ns.using)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
if address := config.get(CONF_ADDRESS):
|
||||
cg.add(var.set_system_address(address))
|
||||
if time_id := config.get(CONF_TIME_ID):
|
||||
time_ = await cg.get_variable(time_id)
|
||||
cg.add(var.set_time_id(time_))
|
||||
if time_device_address := config.get(CONF_TIME_DEVICE_ADDRESS):
|
||||
cg.add(var.set_time_device_address(time_device_address))
|
||||
|
||||
|
||||
async def register_uponor_smatrix_device(var, config):
|
||||
parent = await cg.get_variable(config[CONF_UPONOR_SMATRIX_ID])
|
||||
cg.add(var.set_parent(parent))
|
||||
cg.add(var.set_device_address(config[CONF_ADDRESS]))
|
||||
cg.add(parent.register_device(var))
|
33
esphome/components/uponor_smatrix/climate/__init__.py
Normal file
33
esphome/components/uponor_smatrix/climate/__init__.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import climate
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
from .. import (
|
||||
uponor_smatrix_ns,
|
||||
UponorSmatrixDevice,
|
||||
UPONOR_SMATRIX_DEVICE_SCHEMA,
|
||||
register_uponor_smatrix_device,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["uponor_smatrix"]
|
||||
|
||||
UponorSmatrixClimate = uponor_smatrix_ns.class_(
|
||||
"UponorSmatrixClimate",
|
||||
climate.Climate,
|
||||
cg.Component,
|
||||
UponorSmatrixDevice,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = climate.CLIMATE_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(UponorSmatrixClimate),
|
||||
}
|
||||
).extend(UPONOR_SMATRIX_DEVICE_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await climate.register_climate(var, config)
|
||||
await register_uponor_smatrix_device(var, config)
|
|
@ -0,0 +1,101 @@
|
|||
#include "uponor_smatrix_climate.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace uponor_smatrix {
|
||||
|
||||
static const char *const TAG = "uponor_smatrix.climate";
|
||||
|
||||
void UponorSmatrixClimate::dump_config() {
|
||||
LOG_CLIMATE("", "Uponor Smatrix Climate", this);
|
||||
ESP_LOGCONFIG(TAG, " Device address: 0x%04X", this->address_);
|
||||
}
|
||||
|
||||
void UponorSmatrixClimate::loop() {
|
||||
const uint32_t now = millis();
|
||||
|
||||
// Publish state after all update packets are processed
|
||||
if (this->last_data_ != 0 && (now - this->last_data_ > 100) && this->target_temperature_raw_ != 0) {
|
||||
float temp = raw_to_celsius((this->preset == climate::CLIMATE_PRESET_ECO)
|
||||
? (this->target_temperature_raw_ - this->eco_setback_value_raw_)
|
||||
: this->target_temperature_raw_);
|
||||
float step = this->get_traits().get_visual_target_temperature_step();
|
||||
this->target_temperature = roundf(temp / step) * step;
|
||||
this->publish_state();
|
||||
this->last_data_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
climate::ClimateTraits UponorSmatrixClimate::traits() {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_supports_current_humidity(true);
|
||||
traits.set_supported_modes({climate::CLIMATE_MODE_HEAT});
|
||||
traits.set_supports_action(true);
|
||||
traits.set_supported_presets({climate::CLIMATE_PRESET_ECO});
|
||||
traits.set_visual_min_temperature(this->min_temperature_);
|
||||
traits.set_visual_max_temperature(this->max_temperature_);
|
||||
traits.set_visual_current_temperature_step(0.1f);
|
||||
traits.set_visual_target_temperature_step(0.5f);
|
||||
return traits;
|
||||
}
|
||||
|
||||
void UponorSmatrixClimate::control(const climate::ClimateCall &call) {
|
||||
if (call.get_target_temperature().has_value()) {
|
||||
uint16_t temp = celsius_to_raw(*call.get_target_temperature());
|
||||
if (this->preset == climate::CLIMATE_PRESET_ECO) {
|
||||
// During ECO mode, the thermostat automatically substracts the setback value from the setpoint,
|
||||
// so we need to add it here first
|
||||
temp += this->eco_setback_value_raw_;
|
||||
}
|
||||
|
||||
// For unknown reasons, we need to send a null setpoint first for the thermostat to react
|
||||
UponorSmatrixData data[] = {{UPONOR_ID_TARGET_TEMP, 0}, {UPONOR_ID_TARGET_TEMP, temp}};
|
||||
this->send(data, sizeof(data) / sizeof(data[0]));
|
||||
}
|
||||
}
|
||||
|
||||
void UponorSmatrixClimate::on_device_data(const UponorSmatrixData *data, size_t data_len) {
|
||||
for (int i = 0; i < data_len; i++) {
|
||||
switch (data[i].id) {
|
||||
case UPONOR_ID_TARGET_TEMP_MIN:
|
||||
this->min_temperature_ = raw_to_celsius(data[i].value);
|
||||
break;
|
||||
case UPONOR_ID_TARGET_TEMP_MAX:
|
||||
this->max_temperature_ = raw_to_celsius(data[i].value);
|
||||
break;
|
||||
case UPONOR_ID_TARGET_TEMP:
|
||||
// Ignore invalid values here as they are used by the controller to explicitely request the setpoint from a
|
||||
// thermostat
|
||||
if (data[i].value != UPONOR_INVALID_VALUE)
|
||||
this->target_temperature_raw_ = data[i].value;
|
||||
break;
|
||||
case UPONOR_ID_ECO_SETBACK:
|
||||
this->eco_setback_value_raw_ = data[i].value;
|
||||
break;
|
||||
case UPONOR_ID_DEMAND:
|
||||
if (data[i].value & 0x1000) {
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
this->action = (data[i].value & 0x0040) ? climate::CLIMATE_ACTION_COOLING : climate::CLIMATE_ACTION_IDLE;
|
||||
} else {
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
this->action = (data[i].value & 0x0040) ? climate::CLIMATE_ACTION_HEATING : climate::CLIMATE_ACTION_IDLE;
|
||||
}
|
||||
break;
|
||||
case UPONOR_ID_MODE1:
|
||||
this->set_preset_((data[i].value & 0x0008) ? climate::CLIMATE_PRESET_ECO : climate::CLIMATE_PRESET_NONE);
|
||||
break;
|
||||
case UPONOR_ID_ROOM_TEMP:
|
||||
this->current_temperature = raw_to_celsius(data[i].value);
|
||||
break;
|
||||
case UPONOR_ID_HUMIDITY:
|
||||
this->current_humidity = data[i].value & 0x00FF;
|
||||
}
|
||||
}
|
||||
|
||||
this->last_data_ = millis();
|
||||
}
|
||||
|
||||
} // namespace uponor_smatrix
|
||||
} // namespace esphome
|
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#include "esphome/components/uponor_smatrix/uponor_smatrix.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace uponor_smatrix {
|
||||
|
||||
class UponorSmatrixClimate : public climate::Climate, public Component, public UponorSmatrixDevice {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
protected:
|
||||
climate::ClimateTraits traits() override;
|
||||
void control(const climate::ClimateCall &call) override;
|
||||
void on_device_data(const UponorSmatrixData *data, size_t data_len) override;
|
||||
|
||||
uint32_t last_data_;
|
||||
float min_temperature_{5.0f};
|
||||
float max_temperature_{35.0f};
|
||||
uint16_t eco_setback_value_raw_{0x0048};
|
||||
uint16_t target_temperature_raw_;
|
||||
};
|
||||
|
||||
} // namespace uponor_smatrix
|
||||
} // namespace esphome
|
70
esphome/components/uponor_smatrix/sensor/__init__.py
Normal file
70
esphome/components/uponor_smatrix/sensor/__init__.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor
|
||||
from esphome.const import (
|
||||
CONF_EXTERNAL_TEMPERATURE,
|
||||
CONF_HUMIDITY,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
from .. import (
|
||||
uponor_smatrix_ns,
|
||||
UponorSmatrixDevice,
|
||||
UPONOR_SMATRIX_DEVICE_SCHEMA,
|
||||
register_uponor_smatrix_device,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["uponor_smatrix"]
|
||||
|
||||
UponorSmatrixSensor = uponor_smatrix_ns.class_(
|
||||
"UponorSmatrixSensor",
|
||||
sensor.Sensor,
|
||||
cg.Component,
|
||||
UponorSmatrixDevice,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(UponorSmatrixSensor),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
).extend(UPONOR_SMATRIX_DEVICE_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await register_uponor_smatrix_device(var, config)
|
||||
|
||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temperature_config)
|
||||
cg.add(var.set_temperature_sensor(sens))
|
||||
if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(external_temperature_config)
|
||||
cg.add(var.set_external_temperature_sensor(sens))
|
||||
if humidity_config := config.get(CONF_HUMIDITY):
|
||||
sens = await sensor.new_sensor(humidity_config)
|
||||
cg.add(var.set_humidity_sensor(sens))
|
|
@ -0,0 +1,37 @@
|
|||
#include "uponor_smatrix_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace uponor_smatrix {
|
||||
|
||||
static const char *const TAG = "uponor_smatrix.sensor";
|
||||
|
||||
void UponorSmatrixSensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Uponor Smatrix Sensor");
|
||||
ESP_LOGCONFIG(TAG, " Device address: 0x%04X", this->address_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "External Temperature", this->external_temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
void UponorSmatrixSensor::on_device_data(const UponorSmatrixData *data, size_t data_len) {
|
||||
for (int i = 0; i < data_len; i++) {
|
||||
switch (data[i].id) {
|
||||
case UPONOR_ID_ROOM_TEMP:
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(raw_to_celsius(data[i].value));
|
||||
break;
|
||||
case UPONOR_ID_EXTERNAL_TEMP:
|
||||
if (this->external_temperature_sensor_ != nullptr)
|
||||
this->external_temperature_sensor_->publish_state(raw_to_celsius(data[i].value));
|
||||
break;
|
||||
case UPONOR_ID_HUMIDITY:
|
||||
if (this->humidity_sensor_ != nullptr)
|
||||
this->humidity_sensor_->publish_state(data[i].value & 0x00FF);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace uponor_smatrix
|
||||
} // namespace esphome
|
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uponor_smatrix/uponor_smatrix.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace uponor_smatrix {
|
||||
|
||||
class UponorSmatrixSensor : public sensor::Sensor, public Component, public UponorSmatrixDevice {
|
||||
SUB_SENSOR(temperature)
|
||||
SUB_SENSOR(external_temperature)
|
||||
SUB_SENSOR(humidity)
|
||||
|
||||
public:
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void on_device_data(const UponorSmatrixData *data, size_t data_len) override;
|
||||
};
|
||||
|
||||
} // namespace uponor_smatrix
|
||||
} // namespace esphome
|
225
esphome/components/uponor_smatrix/uponor_smatrix.cpp
Normal file
225
esphome/components/uponor_smatrix/uponor_smatrix.cpp
Normal file
|
@ -0,0 +1,225 @@
|
|||
#include "uponor_smatrix.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace uponor_smatrix {
|
||||
|
||||
static const char *const TAG = "uponor_smatrix";
|
||||
|
||||
void UponorSmatrixComponent::setup() {
|
||||
#ifdef USE_TIME
|
||||
if (this->time_id_ != nullptr) {
|
||||
this->time_id_->add_on_time_sync_callback([this] { this->send_time(); });
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void UponorSmatrixComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Uponor Smatrix");
|
||||
ESP_LOGCONFIG(TAG, " System address: 0x%04X", this->address_);
|
||||
#ifdef USE_TIME
|
||||
if (this->time_id_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Time synchronization: YES");
|
||||
ESP_LOGCONFIG(TAG, " Time master device address: 0x%04X", this->time_device_address_);
|
||||
}
|
||||
#endif
|
||||
|
||||
this->check_uart_settings(19200);
|
||||
|
||||
if (!this->unknown_devices_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " Detected unknown device addresses:");
|
||||
for (auto device_address : this->unknown_devices_) {
|
||||
ESP_LOGCONFIG(TAG, " 0x%04X", device_address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UponorSmatrixComponent::loop() {
|
||||
const uint32_t now = millis();
|
||||
|
||||
// Discard stale data
|
||||
if (!this->rx_buffer_.empty() && (now - this->last_rx_ > 50)) {
|
||||
ESP_LOGD(TAG, "Discarding %d bytes of unparsed data", this->rx_buffer_.size());
|
||||
this->rx_buffer_.clear();
|
||||
}
|
||||
|
||||
// Read incoming data
|
||||
while (this->available()) {
|
||||
// The controller polls devices every 10 seconds, with around 200 ms between devices.
|
||||
// Remember timestamps so we can send our own packets when the bus is expected to be silent.
|
||||
if (now - this->last_rx_ > 500) {
|
||||
this->last_poll_start_ = now;
|
||||
}
|
||||
this->last_rx_ = now;
|
||||
|
||||
uint8_t byte;
|
||||
this->read_byte(&byte);
|
||||
if (this->parse_byte_(byte)) {
|
||||
this->rx_buffer_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Send packets during bus silence
|
||||
if ((now - this->last_rx_ > 300) && (now - this->last_poll_start_ < 9500) && (now - this->last_tx_ > 200)) {
|
||||
// Only build time packet when bus is silent and queue is empty to make sure we can send it right away
|
||||
if (this->send_time_requested_ && this->tx_queue_.empty() && this->do_send_time_())
|
||||
this->send_time_requested_ = false;
|
||||
// Send the next packet in the queue
|
||||
if (!this->tx_queue_.empty()) {
|
||||
auto packet = std::move(this->tx_queue_.front());
|
||||
this->tx_queue_.pop();
|
||||
|
||||
this->write_array(packet);
|
||||
this->flush();
|
||||
|
||||
this->last_tx_ = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
||||
this->rx_buffer_.push_back(byte);
|
||||
const uint8_t *packet = this->rx_buffer_.data();
|
||||
size_t packet_len = this->rx_buffer_.size();
|
||||
|
||||
if (packet_len < 7) {
|
||||
// Minimum packet size is 7 bytes, wait for more
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t system_address = encode_uint16(packet[0], packet[1]);
|
||||
uint16_t device_address = encode_uint16(packet[2], packet[3]);
|
||||
uint16_t crc = encode_uint16(packet[packet_len - 1], packet[packet_len - 2]);
|
||||
|
||||
uint16_t computed_crc = crc16(packet, packet_len - 2);
|
||||
if (crc != computed_crc) {
|
||||
// CRC did not match, more data might be coming
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Received packet: sys=%04X, dev=%04X, data=%s, crc=%04X", system_address, device_address,
|
||||
format_hex(&packet[4], packet_len - 6).c_str(), crc);
|
||||
|
||||
// Detect or check system address
|
||||
if (this->address_ == 0) {
|
||||
ESP_LOGI(TAG, "Using detected system address 0x%04X", system_address);
|
||||
this->address_ = system_address;
|
||||
} else if (this->address_ != system_address) {
|
||||
// This should never happen except if the system address was set or detected incorrectly, so warn the user.
|
||||
ESP_LOGW(TAG, "Received packet from unknown system address 0x%04X", system_address);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle packet
|
||||
size_t data_len = (packet_len - 6) / 3;
|
||||
if (data_len == 0) {
|
||||
if (packet[4] == UPONOR_ID_REQUEST)
|
||||
ESP_LOGVV(TAG, "Ignoring request packet for device 0x%04X", device_address);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Decode packet payload data for easy access
|
||||
UponorSmatrixData data[data_len];
|
||||
for (int i = 0; i < data_len; i++) {
|
||||
data[i].id = packet[(i * 3) + 4];
|
||||
data[i].value = encode_uint16(packet[(i * 3) + 5], packet[(i * 3) + 6]);
|
||||
}
|
||||
|
||||
#ifdef USE_TIME
|
||||
// Detect device that acts as time master if not set explicitely
|
||||
if (this->time_device_address_ == 0 && data_len >= 2) {
|
||||
// The first thermostat paired to the controller will act as the time master. Time can only be manually adjusted at
|
||||
// this first thermostat. To synchronize time, we need to know its address, so we search for packets coming from a
|
||||
// thermostat sending both room temperature and time information.
|
||||
bool found_temperature = false;
|
||||
bool found_time = false;
|
||||
for (int i = 0; i < data_len; i++) {
|
||||
if (data[i].id == UPONOR_ID_ROOM_TEMP)
|
||||
found_temperature = true;
|
||||
if (data[i].id == UPONOR_ID_DATETIME1)
|
||||
found_time = true;
|
||||
if (found_temperature && found_time) {
|
||||
ESP_LOGI(TAG, "Using detected time device address 0x%04X", device_address);
|
||||
this->time_device_address_ = device_address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Forward data to device components
|
||||
bool found = false;
|
||||
for (auto *device : this->devices_) {
|
||||
if (device->address_ == device_address) {
|
||||
found = true;
|
||||
device->on_device_data(data, data_len);
|
||||
}
|
||||
}
|
||||
|
||||
// Log unknown device addresses
|
||||
if (!found && !this->unknown_devices_.count(device_address)) {
|
||||
ESP_LOGI(TAG, "Received packet for unknown device address 0x%04X ", device_address);
|
||||
this->unknown_devices_.insert(device_address);
|
||||
}
|
||||
|
||||
// Return true to reset buffer
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UponorSmatrixComponent::send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len) {
|
||||
if (this->address_ == 0 || device_address == 0 || data == nullptr || data_len == 0)
|
||||
return false;
|
||||
|
||||
// Assemble packet for send queue. All fields are big-endian except for the little-endian checksum.
|
||||
std::vector<uint8_t> packet(6 + 3 * data_len);
|
||||
packet.push_back(this->address_ >> 8);
|
||||
packet.push_back(this->address_ >> 0);
|
||||
packet.push_back(device_address >> 8);
|
||||
packet.push_back(device_address >> 0);
|
||||
|
||||
for (int i = 0; i < data_len; i++) {
|
||||
packet.push_back(data[i].id);
|
||||
packet.push_back(data[i].value >> 8);
|
||||
packet.push_back(data[i].value >> 0);
|
||||
}
|
||||
|
||||
auto crc = crc16(packet.data(), packet.size());
|
||||
packet.push_back(crc >> 0);
|
||||
packet.push_back(crc >> 8);
|
||||
|
||||
this->tx_queue_.push(packet);
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_TIME
|
||||
bool UponorSmatrixComponent::do_send_time_() {
|
||||
if (this->time_device_address_ == 0 || this->time_id_ == nullptr)
|
||||
return false;
|
||||
|
||||
ESPTime now = this->time_id_->now();
|
||||
if (!now.is_valid())
|
||||
return false;
|
||||
|
||||
uint8_t year = now.year - 2000;
|
||||
uint8_t month = now.month;
|
||||
// ESPHome days are [1-7] starting with Sunday, Uponor days are [0-6] starting with Monday
|
||||
uint8_t day_of_week = (now.day_of_week == 1) ? 6 : (now.day_of_week - 2);
|
||||
uint8_t day_of_month = now.day_of_month;
|
||||
uint8_t hour = now.hour;
|
||||
uint8_t minute = now.minute;
|
||||
uint8_t second = now.second;
|
||||
|
||||
uint16_t time1 = (year & 0x7F) << 7 | (month & 0x0F) << 3 | (day_of_week & 0x07);
|
||||
uint16_t time2 = (day_of_month & 0x1F) << 11 | (hour & 0x1F) << 6 | (minute & 0x3F);
|
||||
uint16_t time3 = second;
|
||||
|
||||
ESP_LOGI(TAG, "Sending local time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour,
|
||||
now.minute, now.second);
|
||||
|
||||
UponorSmatrixData data[] = {{UPONOR_ID_DATETIME1, time1}, {UPONOR_ID_DATETIME2, time2}, {UPONOR_ID_DATETIME3, time3}};
|
||||
return this->send(this->time_device_address_, data, sizeof(data) / sizeof(data[0]));
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace uponor_smatrix
|
||||
} // namespace esphome
|
128
esphome/components/uponor_smatrix/uponor_smatrix.h
Normal file
128
esphome/components/uponor_smatrix/uponor_smatrix.h
Normal file
|
@ -0,0 +1,128 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_TIME
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
#include "esphome/core/time.h"
|
||||
#endif
|
||||
|
||||
#include <queue>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace uponor_smatrix {
|
||||
|
||||
/// Date/Time Part 1 (year, month, day of week)
|
||||
static const uint8_t UPONOR_ID_DATETIME1 = 0x08;
|
||||
/// Date/Time Part 2 (day of month, hour, minute)
|
||||
static const uint8_t UPONOR_ID_DATETIME2 = 0x09;
|
||||
/// Date/Time Part 3 (seconds)
|
||||
static const uint8_t UPONOR_ID_DATETIME3 = 0x0A;
|
||||
/// Unknown (observed values: 0x0342, 0x0024)
|
||||
static const uint8_t UPONOR_ID_UNKNOWN1 = 0x0C;
|
||||
/// Outdoor Temperature? (sent by controller)
|
||||
static const uint8_t UPONOR_ID_OUTDOOR_TEMP = 0x2D;
|
||||
/// Unknown (observed values: 0x8000)
|
||||
static const uint8_t UPONOR_ID_UNKNOWN2 = 0x35;
|
||||
/// Room Temperature Setpoint Minimum
|
||||
static const uint8_t UPONOR_ID_TARGET_TEMP_MIN = 0x37;
|
||||
/// Room Temperature Setpoint Maximum
|
||||
static const uint8_t UPONOR_ID_TARGET_TEMP_MAX = 0x38;
|
||||
/// Room Temperature Setpoint
|
||||
static const uint8_t UPONOR_ID_TARGET_TEMP = 0x3B;
|
||||
/// Room Temperature Setpoint Setback for ECO Mode
|
||||
static const uint8_t UPONOR_ID_ECO_SETBACK = 0x3C;
|
||||
/// Heating/Cooling Demand
|
||||
static const uint8_t UPONOR_ID_DEMAND = 0x3D;
|
||||
/// Thermostat Operating Mode 1 (ECO state, program schedule state)
|
||||
static const uint8_t UPONOR_ID_MODE1 = 0x3E;
|
||||
/// Thermostat Operating Mode 2 (sensor configuration, heating/cooling allowed)
|
||||
static const uint8_t UPONOR_ID_MODE2 = 0x3F;
|
||||
/// Current Room Temperature
|
||||
static const uint8_t UPONOR_ID_ROOM_TEMP = 0x40;
|
||||
/// Current External (Floor/Outdoor) Sensor Temperature
|
||||
static const uint8_t UPONOR_ID_EXTERNAL_TEMP = 0x41;
|
||||
/// Current Room Humidity
|
||||
static const uint8_t UPONOR_ID_HUMIDITY = 0x42;
|
||||
/// Data Request (sent by controller)
|
||||
static const uint8_t UPONOR_ID_REQUEST = 0xFF;
|
||||
|
||||
/// Indicating an invalid/missing value
|
||||
static const uint16_t UPONOR_INVALID_VALUE = 0x7FFF;
|
||||
|
||||
struct UponorSmatrixData {
|
||||
uint8_t id;
|
||||
uint16_t value;
|
||||
};
|
||||
|
||||
class UponorSmatrixDevice;
|
||||
|
||||
class UponorSmatrixComponent : public uart::UARTDevice, public Component {
|
||||
public:
|
||||
UponorSmatrixComponent() = default;
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
void set_system_address(uint16_t address) { this->address_ = address; }
|
||||
void register_device(UponorSmatrixDevice *device) { this->devices_.push_back(device); }
|
||||
|
||||
bool send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len);
|
||||
|
||||
#ifdef USE_TIME
|
||||
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
|
||||
void set_time_device_address(uint16_t address) { this->time_device_address_ = address; }
|
||||
void send_time() { this->send_time_requested_ = true; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool parse_byte_(uint8_t byte);
|
||||
|
||||
uint16_t address_;
|
||||
std::vector<UponorSmatrixDevice *> devices_;
|
||||
std::set<uint16_t> unknown_devices_;
|
||||
|
||||
std::vector<uint8_t> rx_buffer_;
|
||||
std::queue<std::vector<uint8_t>> tx_queue_;
|
||||
uint32_t last_rx_;
|
||||
uint32_t last_tx_;
|
||||
uint32_t last_poll_start_;
|
||||
|
||||
#ifdef USE_TIME
|
||||
time::RealTimeClock *time_id_{nullptr};
|
||||
uint16_t time_device_address_;
|
||||
bool send_time_requested_;
|
||||
bool do_send_time_();
|
||||
#endif
|
||||
};
|
||||
|
||||
class UponorSmatrixDevice : public Parented<UponorSmatrixComponent> {
|
||||
public:
|
||||
void set_device_address(uint16_t address) { this->address_ = address; }
|
||||
|
||||
virtual void on_device_data(const UponorSmatrixData *data, size_t data_len) = 0;
|
||||
bool send(const UponorSmatrixData *data, size_t data_len) {
|
||||
return this->parent_->send(this->address_, data, data_len);
|
||||
}
|
||||
|
||||
protected:
|
||||
friend UponorSmatrixComponent;
|
||||
uint16_t address_;
|
||||
};
|
||||
|
||||
inline float raw_to_celsius(uint16_t raw) {
|
||||
return (raw == UPONOR_INVALID_VALUE) ? NAN : fahrenheit_to_celsius(raw / 10.0f);
|
||||
}
|
||||
|
||||
inline uint16_t celsius_to_raw(float celsius) {
|
||||
return std::isnan(celsius) ? UPONOR_INVALID_VALUE
|
||||
: static_cast<uint16_t>(lroundf(celsius_to_fahrenheit(celsius) * 10.0f));
|
||||
}
|
||||
|
||||
} // namespace uponor_smatrix
|
||||
} // namespace esphome
|
|
@ -251,6 +251,7 @@ CONF_EXPORT_ACTIVE_ENERGY = "export_active_energy"
|
|||
CONF_EXPORT_REACTIVE_ENERGY = "export_reactive_energy"
|
||||
CONF_EXTERNAL_CLOCK_INPUT = "external_clock_input"
|
||||
CONF_EXTERNAL_COMPONENTS = "external_components"
|
||||
CONF_EXTERNAL_TEMPERATURE = "external_temperature"
|
||||
CONF_EXTERNAL_VCC = "external_vcc"
|
||||
CONF_FALLING_EDGE = "falling_edge"
|
||||
CONF_FAMILY = "family"
|
||||
|
|
38
tests/components/uponor_smatrix/common.yaml
Normal file
38
tests/components/uponor_smatrix/common.yaml
Normal file
|
@ -0,0 +1,38 @@
|
|||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
uart:
|
||||
- id: uponor_uart
|
||||
baud_rate: 19200
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
servers:
|
||||
- 0.pool.ntp.org
|
||||
- 1.pool.ntp.org
|
||||
- 192.168.178.1
|
||||
|
||||
uponor_smatrix:
|
||||
uart_id: uponor_uart
|
||||
address: 0x110B
|
||||
time_id: sntp_time
|
||||
time_device_address: 0xDE13
|
||||
|
||||
climate:
|
||||
- platform: uponor_smatrix
|
||||
address: 0xDE13
|
||||
name: Thermostat Living Room
|
||||
|
||||
sensor:
|
||||
- platform: uponor_smatrix
|
||||
address: 0xDE13
|
||||
humidity:
|
||||
name: Thermostat Humidity Living Room
|
||||
temperature:
|
||||
name: Thermostat Temperature Living Room
|
||||
external_temperature:
|
||||
name: Thermostat Floor Temperature Living Room
|
5
tests/components/uponor_smatrix/test.esp32-c3-idf.yaml
Normal file
5
tests/components/uponor_smatrix/test.esp32-c3-idf.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
<<: !include common.yaml
|
5
tests/components/uponor_smatrix/test.esp32-c3.yaml
Normal file
5
tests/components/uponor_smatrix/test.esp32-c3.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
<<: !include common.yaml
|
5
tests/components/uponor_smatrix/test.esp32-idf.yaml
Normal file
5
tests/components/uponor_smatrix/test.esp32-idf.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
tx_pin: GPIO17
|
||||
rx_pin: GPIO16
|
||||
|
||||
<<: !include common.yaml
|
5
tests/components/uponor_smatrix/test.esp32.yaml
Normal file
5
tests/components/uponor_smatrix/test.esp32.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
tx_pin: GPIO17
|
||||
rx_pin: GPIO16
|
||||
|
||||
<<: !include common.yaml
|
5
tests/components/uponor_smatrix/test.esp8266.yaml
Normal file
5
tests/components/uponor_smatrix/test.esp8266.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
<<: !include common.yaml
|
5
tests/components/uponor_smatrix/test.rp2040.yaml
Normal file
5
tests/components/uponor_smatrix/test.rp2040.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
<<: !include common.yaml
|
Loading…
Reference in a new issue