Skip to content

Commit

Permalink
Add Uponor Smatrix component (esphome#5769)
Browse files Browse the repository at this point in the history
Co-authored-by: Jesse Hills <[email protected]>
  • Loading branch information
kroimon and jesserockz authored Feb 22, 2024
1 parent 76a3ffc commit 58c0d8c
Show file tree
Hide file tree
Showing 21 changed files with 796 additions and 4 deletions.
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion esphome/components/bl0940/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from esphome.const import (
CONF_CURRENT,
CONF_ENERGY,
CONF_EXTERNAL_TEMPERATURE,
CONF_ID,
CONF_POWER,
CONF_VOLTAGE,
Expand All @@ -24,7 +25,6 @@
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)
Expand Down
2 changes: 1 addition & 1 deletion esphome/components/emc2101/sensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
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,
Expand All @@ -16,7 +17,6 @@
DEPENDENCIES = ["emc2101"]

CONF_INTERNAL_TEMPERATURE = "internal_temperature"
CONF_EXTERNAL_TEMPERATURE = "external_temperature"
CONF_DUTY_CYCLE = "duty_cycle"

EMC2101Sensor = emc2101_ns.class_("EMC2101Sensor", cg.PollingComponent)
Expand Down
3 changes: 1 addition & 2 deletions esphome/components/inkbird_ibsth1_mini/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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,
Expand All @@ -19,8 +20,6 @@
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
Expand Down
78 changes: 78 additions & 0 deletions esphome/components/uponor_smatrix/__init__.py
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions esphome/components/uponor_smatrix/climate/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
101 changes: 101 additions & 0 deletions esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.h
Original file line number Diff line number Diff line change
@@ -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 changes: 70 additions & 0 deletions esphome/components/uponor_smatrix/sensor/__init__.py
Original file line number Diff line number Diff line change
@@ -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))
Loading

0 comments on commit 58c0d8c

Please sign in to comment.