mirror of
https://github.com/esphome/esphome.git
synced 2026-10-15 02:32:49 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f94859cce | ||
|
|
009f4973e1 | ||
|
|
c1fa73470e |
@@ -338,9 +338,8 @@ class ApplyField:
|
||||
Double a literal brace in a template. ``conf_key`` may be a path into nested sections.
|
||||
``type_`` may be a C++ type string using ``{parent}`` when the type is only known per
|
||||
instance. ``const_fn(config, value)`` renders a constant's argument text when ``cg.safe_exp``
|
||||
is not the right spelling, or the value depends on another entity's config, such as the unit
|
||||
a climate reports in; a lambda or an id bypasses it, so the target must also take a plain
|
||||
``type_``. An absent key emits nothing.
|
||||
is not the right spelling (unit conversion belongs in the validator); a lambda or an id
|
||||
bypasses it, so the target must also take a plain ``type_``. An absent key emits nothing.
|
||||
"""
|
||||
|
||||
conf_key: str | tuple[str, ...]
|
||||
|
||||
@@ -46,15 +46,12 @@ from esphome.const import (
|
||||
CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC,
|
||||
CONF_TARGET_TEMPERATURE_STATE_TOPIC,
|
||||
CONF_TEMPERATURE_STEP,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VISUAL,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
entity_temperature_unit,
|
||||
entity_temperature_unit_of_id,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -140,10 +137,13 @@ CONF_MIN_HUMIDITY = "min_humidity"
|
||||
CONF_MAX_HUMIDITY = "max_humidity"
|
||||
CONF_TARGET_HUMIDITY = "target_humidity"
|
||||
|
||||
visual_temperature = cv.float_with_unit("visual_temperature", "(°|(° ?)?[CKF])?")
|
||||
|
||||
|
||||
VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_TARGET_TEMPERATURE): cv.temperature_with_unit,
|
||||
cv.Required(CONF_CURRENT_TEMPERATURE): cv.temperature_with_unit,
|
||||
cv.Required(CONF_TARGET_TEMPERATURE): visual_temperature,
|
||||
cv.Required(CONF_CURRENT_TEMPERATURE): visual_temperature,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -154,30 +154,13 @@ def visual_temperature_step(value: Any) -> ConfigType:
|
||||
return VISUAL_TEMPERATURE_STEP_SCHEMA(value)
|
||||
|
||||
# Otherwise, use the single value for both properties
|
||||
value = cv.temperature_with_unit(value)
|
||||
return {CONF_TARGET_TEMPERATURE: value, CONF_CURRENT_TEMPERATURE: value}
|
||||
|
||||
|
||||
def temperature_in_entity_unit(
|
||||
config: ConfigType, value: tuple[float, str | None]
|
||||
) -> str:
|
||||
"""Render a static action temperature in the unit of the climate the action targets."""
|
||||
unit = entity_temperature_unit_of_id(config[CONF_ID])
|
||||
return str(cg.safe_exp(cv.convert_temperature(*value, unit)))
|
||||
|
||||
|
||||
def _visual_temperatures_in_entity_unit(config: ConfigType) -> ConfigType:
|
||||
"""Convert the visual temperatures into the entity's unit."""
|
||||
unit = entity_temperature_unit(config)
|
||||
visual = config[CONF_VISUAL]
|
||||
for key in (CONF_MIN_TEMPERATURE, CONF_MAX_TEMPERATURE):
|
||||
if isinstance(value := visual.get(key), tuple):
|
||||
visual[key] = cv.convert_temperature(*value, unit)
|
||||
if (step := visual.get(CONF_TEMPERATURE_STEP)) is not None:
|
||||
for key in (CONF_TARGET_TEMPERATURE, CONF_CURRENT_TEMPERATURE):
|
||||
if isinstance(value := step[key], tuple):
|
||||
step[key] = cv.convert_temperature(*value, unit, delta=True)
|
||||
return config
|
||||
value = visual_temperature(value)
|
||||
return VISUAL_TEMPERATURE_STEP_SCHEMA(
|
||||
{
|
||||
CONF_TARGET_TEMPERATURE: value,
|
||||
CONF_CURRENT_TEMPERATURE: value,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
_CLIMATE_SCHEMA = (
|
||||
@@ -188,8 +171,8 @@ _CLIMATE_SCHEMA = (
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent),
|
||||
cv.Optional(CONF_VISUAL, default={}): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature_with_unit,
|
||||
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature_with_unit,
|
||||
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature,
|
||||
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature,
|
||||
cv.Optional(CONF_TEMPERATURE_STEP): visual_temperature_step,
|
||||
cv.Optional(CONF_MIN_HUMIDITY): cv.percentage_int,
|
||||
cv.Optional(CONF_MAX_HUMIDITY): cv.percentage_int,
|
||||
@@ -266,7 +249,6 @@ _CLIMATE_SCHEMA = (
|
||||
|
||||
|
||||
_CLIMATE_SCHEMA.add_extra(entity_duplicate_validator("climate"))
|
||||
_CLIMATE_SCHEMA.add_extra(_visual_temperatures_in_entity_unit)
|
||||
|
||||
|
||||
def climate_schema(
|
||||
@@ -274,18 +256,10 @@ def climate_schema(
|
||||
*,
|
||||
entity_category: str = cv.UNDEFINED,
|
||||
icon: str = cv.UNDEFINED,
|
||||
temperature_unit: bool = False,
|
||||
) -> cv.Schema:
|
||||
"""Build the schema of a climate platform.
|
||||
|
||||
``temperature_unit`` adds ``unit_of_measurement`` for platforms whose temperatures are all
|
||||
in the unit the user picks; the class then needs ``set_temperature_unit(TemperatureUnit)``.
|
||||
"""
|
||||
schema = {
|
||||
cv.GenerateID(): cv.declare_id(class_),
|
||||
}
|
||||
if temperature_unit:
|
||||
schema[cv.Optional(CONF_UNIT_OF_MEASUREMENT)] = cv.temperature_unit
|
||||
|
||||
for key, default, validator in [
|
||||
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
|
||||
@@ -486,13 +460,9 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Climate),
|
||||
cv.Optional(CONF_MODE): cv.templatable(validate_climate_mode),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature_with_unit),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(
|
||||
cv.temperature_with_unit
|
||||
),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(
|
||||
cv.temperature_with_unit
|
||||
),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_TARGET_HUMIDITY): cv.templatable(cv.percentage_int),
|
||||
cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"),
|
||||
cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
|
||||
@@ -512,23 +482,12 @@ automation.register_apply_action(
|
||||
"climate.control",
|
||||
CLIMATE_CONTROL_ACTION_SCHEMA,
|
||||
automation.ApplyField(CONF_MODE, "set_mode", ClimateMode),
|
||||
automation.ApplyField(CONF_TARGET_TEMPERATURE, "set_target_temperature", cg.float_),
|
||||
automation.ApplyField(
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
"set_target_temperature",
|
||||
cg.float_,
|
||||
temperature_in_entity_unit,
|
||||
CONF_TARGET_TEMPERATURE_LOW, "set_target_temperature_low", cg.float_
|
||||
),
|
||||
automation.ApplyField(
|
||||
CONF_TARGET_TEMPERATURE_LOW,
|
||||
"set_target_temperature_low",
|
||||
cg.float_,
|
||||
temperature_in_entity_unit,
|
||||
),
|
||||
automation.ApplyField(
|
||||
CONF_TARGET_TEMPERATURE_HIGH,
|
||||
"set_target_temperature_high",
|
||||
cg.float_,
|
||||
temperature_in_entity_unit,
|
||||
CONF_TARGET_TEMPERATURE_HIGH, "set_target_temperature_high", cg.float_
|
||||
),
|
||||
automation.ApplyField(CONF_TARGET_HUMIDITY, "set_target_humidity", cg.float_),
|
||||
automation.ApplyField(CONF_FAN_MODE, "set_fan_mode", ClimateFanMode),
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "display_color_utils.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::display {
|
||||
@@ -52,40 +53,65 @@ void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_
|
||||
this->line(x1, y1, x2, y2, color);
|
||||
}
|
||||
|
||||
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
|
||||
uint32_t color_value;
|
||||
// flatten keeps to_color() inlined in the pixel loop; in PSRAM builds GCC at
|
||||
// -Os stops inlining it once there are two call sites, which costs a call and
|
||||
// spills per pixel. Without PSRAM there is one call site and it changes
|
||||
// nothing, and draw_pixel_at() stays a virtual call either way.
|
||||
template<ColorBitness BITNESS>
|
||||
static void __attribute__((flatten))
|
||||
draw_packed_pixels(Display *display, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
const size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
|
||||
// BITNESS is a constant here, so to_color()'s three per pixel divisions
|
||||
// become a multiply and shift and its bitness switch folds.
|
||||
const auto draw_source_pixel = [&](int x, int y, size_t source_idx) ESPHOME_ALWAYS_INLINE {
|
||||
display->draw_pixel_at(
|
||||
x + x_start, y + y_start,
|
||||
ColorUtil::to_color(ColorUtil::read_packed<BITNESS>(ptr, source_idx, big_endian), order, BITNESS));
|
||||
};
|
||||
// The source and frame buffers are row-major, so walking rows keeps the read
|
||||
// and the write sequential. When the display swaps the axes before writing,
|
||||
// walking columns keeps the write sequential instead; with the frame buffer
|
||||
// in PSRAM a strided write costs more than a strided read.
|
||||
#ifdef USE_PSRAM
|
||||
if (display->pixel_axes_swapped()) {
|
||||
size_t column_idx = y_offset * line_stride + x_offset;
|
||||
for (int x = 0; x != w; x++, column_idx++) {
|
||||
size_t source_idx = column_idx;
|
||||
for (int y = 0; y != h; y++, source_idx += line_stride) {
|
||||
draw_source_pixel(x, y, source_idx);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Row walk: builds without PSRAM, and PSRAM builds whose axes are not swapped.
|
||||
for (int y = 0; y != h; y++) {
|
||||
size_t source_idx = (y_offset + y) * line_stride + x_offset;
|
||||
size_t source_idx_mod;
|
||||
for (int x = 0; x != w; x++, source_idx++) {
|
||||
switch (bitness) {
|
||||
default:
|
||||
color_value = ptr[source_idx];
|
||||
break;
|
||||
case COLOR_BITNESS_565:
|
||||
source_idx_mod = source_idx * 2;
|
||||
if (big_endian) {
|
||||
color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1];
|
||||
} else {
|
||||
color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8);
|
||||
}
|
||||
break;
|
||||
case COLOR_BITNESS_888:
|
||||
source_idx_mod = source_idx * 3;
|
||||
if (big_endian) {
|
||||
color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2];
|
||||
} else {
|
||||
color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16);
|
||||
}
|
||||
break;
|
||||
}
|
||||
this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness));
|
||||
draw_source_pixel(x, y, source_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
switch (bitness) {
|
||||
case COLOR_BITNESS_565:
|
||||
draw_packed_pixels<COLOR_BITNESS_565>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
case COLOR_BITNESS_888:
|
||||
draw_packed_pixels<COLOR_BITNESS_888>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
default:
|
||||
draw_packed_pixels<COLOR_BITNESS_332>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HOT Display::horizontal_line(int x, int y, int width, Color color) {
|
||||
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
|
||||
for (int i = x; i < x + width; i++)
|
||||
|
||||
@@ -139,6 +139,11 @@ enum DisplayRotation {
|
||||
DISPLAY_ROTATION_270_DEGREES = 270,
|
||||
};
|
||||
|
||||
/// Whether a rotation swaps the x and y axes.
|
||||
constexpr bool rotation_swaps_axes(DisplayRotation rotation) {
|
||||
return rotation == DISPLAY_ROTATION_90_DEGREES || rotation == DISPLAY_ROTATION_270_DEGREES;
|
||||
}
|
||||
|
||||
const int EDGES_TRIGON = 3;
|
||||
const int EDGES_TRIANGLE = 3;
|
||||
const int EDGES_TETRAGON = 4;
|
||||
@@ -717,6 +722,12 @@ class Display : public PollingComponent {
|
||||
|
||||
virtual DisplayRotation get_rotation() const { return this->rotation_; }
|
||||
|
||||
/// True when draw_pixel_at() swaps x and y before writing, so pixels that are
|
||||
/// neighbours along x land a row apart in the frame buffer. Not the same as
|
||||
/// get_width()/get_height() swapping: a display that rotates in hardware
|
||||
/// reports swapped dimensions but writes unswapped pixels.
|
||||
virtual bool pixel_axes_swapped() const { return rotation_swaps_axes(this->rotation_); }
|
||||
|
||||
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
|
||||
* returns the type the display is currently configured to.
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::display {
|
||||
enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 };
|
||||
@@ -10,6 +11,27 @@ inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 25
|
||||
|
||||
class ColorUtil {
|
||||
public:
|
||||
/// Reads one packed pixel of the given bitness from a source buffer into the
|
||||
/// value to_color() takes. BITNESS is a template argument so the byte layout
|
||||
/// folds; pass it on to to_color() as a constant too, and its channel scaling
|
||||
/// divides by a constant, which the compiler turns into a multiply and shift.
|
||||
template<ColorBitness BITNESS>
|
||||
static ESPHOME_ALWAYS_INLINE uint32_t read_packed(const uint8_t *ptr, size_t index, bool big_endian) {
|
||||
if constexpr (BITNESS == COLOR_BITNESS_565) {
|
||||
const size_t idx = index * 2;
|
||||
if (big_endian)
|
||||
return (ptr[idx] << 8) + ptr[idx + 1];
|
||||
return ptr[idx] + (ptr[idx + 1] << 8);
|
||||
} else if constexpr (BITNESS == COLOR_BITNESS_888) {
|
||||
const size_t idx = index * 3;
|
||||
if (big_endian)
|
||||
return (ptr[idx + 0] << 16) + (ptr[idx + 1] << 8) + ptr[idx + 2];
|
||||
return ptr[idx + 0] + (ptr[idx + 1] << 8) + (ptr[idx + 2] << 16);
|
||||
} else {
|
||||
return ptr[index];
|
||||
}
|
||||
}
|
||||
|
||||
static Color to_color(uint32_t colorcode, ColorOrder color_order,
|
||||
ColorBitness color_bitness = ColorBitness::COLOR_BITNESS_888, bool right_bit_aligned = true) {
|
||||
uint8_t first_color, second_color, third_color;
|
||||
|
||||
@@ -106,6 +106,7 @@ class EPaperBase : public Display,
|
||||
|
||||
int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; }
|
||||
int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; }
|
||||
bool pixel_axes_swapped() const override { return this->effective_transform_ & SWAP_XY; }
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
|
||||
@@ -204,6 +204,7 @@ class IT8951Display : public Display,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
int get_width() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->height_ : this->width_; }
|
||||
int get_height() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->width_ : this->height_; }
|
||||
bool pixel_axes_swapped() const override { return this->effective_transform_ & TRANSFORM_SWAP_XY; }
|
||||
|
||||
protected:
|
||||
int get_height_internal() override { return this->height_; }
|
||||
|
||||
@@ -1,15 +1,42 @@
|
||||
#include "lsm6ds.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::lsm6ds {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "lsm6ds");
|
||||
|
||||
static const struct {
|
||||
uint8_t who_am_i;
|
||||
const char *const name;
|
||||
} CHIP_IDS[] = {{0x69, "LSMDSO"}, {0x6A, "LSM6DS3"}};
|
||||
// Indexed by the accel range field, which is not in ascending order (datasheet Table 3)
|
||||
PROGMEM_STRING_TABLE(AccelRangeStrings, "±2g", "±16g", "±4g", "±8g");
|
||||
|
||||
static const LogString *gyro_range_to_str(LSM6DSGyroRange range) {
|
||||
switch (range) {
|
||||
case LSM6DS_GYRO_RANGE_125:
|
||||
return LOG_STR("±125dps");
|
||||
case LSM6DS_GYRO_RANGE_250:
|
||||
return LOG_STR("±250dps");
|
||||
case LSM6DS_GYRO_RANGE_500:
|
||||
return LOG_STR("±500dps");
|
||||
case LSM6DS_GYRO_RANGE_1000:
|
||||
return LOG_STR("±1000dps");
|
||||
case LSM6DS_GYRO_RANGE_2000:
|
||||
return LOG_STR("±2000dps");
|
||||
default:
|
||||
return LOG_STR("unknown");
|
||||
}
|
||||
}
|
||||
|
||||
static const LogString *chip_name_from_who_am_i(uint8_t who_am_i) {
|
||||
switch (who_am_i) {
|
||||
case 0x69:
|
||||
return LOG_STR("LSMDSO");
|
||||
case 0x6A:
|
||||
return LOG_STR("LSM6DS3");
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void LSM6DSComponent::setup() {
|
||||
MotionComponent::setup();
|
||||
@@ -19,19 +46,13 @@ void LSM6DSComponent::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
const char *chip_name = nullptr;
|
||||
for (const auto &chip : CHIP_IDS) {
|
||||
if (chip.who_am_i == who_am_i) {
|
||||
chip_name = chip.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const LogString *chip_name = chip_name_from_who_am_i(who_am_i);
|
||||
if (chip_name == nullptr) {
|
||||
ESP_LOGE(TAG, "Unknown WHO_AM_I: 0x%02X", who_am_i);
|
||||
this->mark_failed(LOG_STR("Unknown WHO_AM_I value"));
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Found %s (WHO_AM_I = 0x%02X)", chip_name, who_am_i);
|
||||
ESP_LOGD(TAG, "Found %s (WHO_AM_I = 0x%02X)", LOG_STR_ARG(chip_name), who_am_i);
|
||||
this->chip_name_ = chip_name;
|
||||
|
||||
// 2. Software reset — clears all registers to defaults
|
||||
@@ -85,34 +106,10 @@ void LSM6DSComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"LSM6DS IMU:\n"
|
||||
" Chip type: %s\n",
|
||||
this->chip_name_);
|
||||
LOG_STR_ARG(this->chip_name_ != nullptr ? this->chip_name_ : LOG_STR("Unknown")));
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
// Accel range — index into the sensitivity table (datasheet Table 3)
|
||||
static const char *const ACCEL_RANGE_STR[] = {"±2g", "±16g", "±4g", "±8g"};
|
||||
|
||||
const char *gyro_str;
|
||||
switch (this->gyro_range_) {
|
||||
case LSM6DS_GYRO_RANGE_125:
|
||||
gyro_str = "±125dps";
|
||||
break;
|
||||
case LSM6DS_GYRO_RANGE_250:
|
||||
gyro_str = "±250dps";
|
||||
break;
|
||||
case LSM6DS_GYRO_RANGE_500:
|
||||
gyro_str = "±500dps";
|
||||
break;
|
||||
case LSM6DS_GYRO_RANGE_1000:
|
||||
gyro_str = "±1000dps";
|
||||
break;
|
||||
case LSM6DS_GYRO_RANGE_2000:
|
||||
gyro_str = "±2000dps";
|
||||
break;
|
||||
default:
|
||||
gyro_str = "unknown";
|
||||
break;
|
||||
}
|
||||
auto accel_odr = this->accel_odr_ == 0 ? 0 : 13 * (1 << (this->accel_odr_ - 1));
|
||||
auto gyro_odr = this->gyro_odr_ == 0 ? 0 : 13 * (1 << (this->gyro_odr_ - 1));
|
||||
ESP_LOGCONFIG(TAG,
|
||||
@@ -120,7 +117,8 @@ void LSM6DSComponent::dump_config() {
|
||||
" Accel data rate : %dHz\n"
|
||||
" Gyro range : %s\n"
|
||||
" Gyro data rate : %dHz",
|
||||
ACCEL_RANGE_STR[this->accel_range_], accel_odr, gyro_str, gyro_odr);
|
||||
LOG_STR_ARG(AccelRangeStrings::get_log_str(this->accel_range_, AccelRangeStrings::LAST_INDEX)),
|
||||
accel_odr, LOG_STR_ARG(gyro_range_to_str(this->gyro_range_)), gyro_odr);
|
||||
}
|
||||
|
||||
// update_data()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/motion/motion_component.h"
|
||||
|
||||
@@ -97,7 +98,7 @@ class LSM6DSComponent final : public motion::MotionComponent, public i2c::I2CDev
|
||||
template<typename F> void add_temperature_listener(F &&cb) { this->temperature_callback_.add(std::forward<F>(cb)); }
|
||||
|
||||
protected:
|
||||
const char *chip_name_{"Unknown"};
|
||||
const LogString *chip_name_{nullptr};
|
||||
bool update_data(motion::MotionData &data) override;
|
||||
|
||||
LSM6DSAccelRange accel_range_{LSM6DS_ACCEL_RANGE_4G};
|
||||
|
||||
@@ -130,19 +130,8 @@ class MipiSpi : public display::Display,
|
||||
}
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
|
||||
int get_width() override {
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||
return HEIGHT;
|
||||
return WIDTH;
|
||||
}
|
||||
|
||||
int get_height() override {
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||
return WIDTH;
|
||||
return HEIGHT;
|
||||
}
|
||||
int get_width() override { return display::rotation_swaps_axes(this->rotation_) ? HEIGHT : WIDTH; }
|
||||
int get_height() override { return display::rotation_swaps_axes(this->rotation_) ? WIDTH : HEIGHT; }
|
||||
|
||||
void set_init_sequence(const uint8_t *sequence, size_t len) {
|
||||
this->init_sequence_ = sequence;
|
||||
@@ -621,6 +610,9 @@ class MipiSpiBuffer
|
||||
#endif
|
||||
}
|
||||
|
||||
// With hardware rotation the buffer is written in drawn coordinates.
|
||||
bool pixel_axes_swapped() const override { return !HAS_HARDWARE_ROTATION && display::Display::pixel_axes_swapped(); }
|
||||
|
||||
// Draw a pixel at the given coordinates.
|
||||
void draw_pixel_at(int x, int y, Color color) override {
|
||||
if (this->is_point_clipped(x, y))
|
||||
|
||||
@@ -27,10 +27,7 @@ from esphome.const import (
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
CONF_TARGET_TEMPERATURE_HIGH,
|
||||
CONF_TARGET_TEMPERATURE_LOW,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
)
|
||||
from esphome.core.entity_helpers import entity_temperature_unit
|
||||
from esphome.cpp_types import TEMPERATURE_UNITS
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import template_ns
|
||||
@@ -165,13 +162,6 @@ def _resolve_supports(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
TEMPERATURE_KEYS = (
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
CONF_TARGET_TEMPERATURE_LOW,
|
||||
CONF_TARGET_TEMPERATURE_HIGH,
|
||||
)
|
||||
|
||||
|
||||
def _validate_initial_state(config: ConfigType) -> ConfigType:
|
||||
# Climate keeps target_temperature and target_temperature_low in a union, so writing the wrong
|
||||
# one of the pair corrupts the setpoint with no runtime complaint.
|
||||
@@ -201,10 +191,6 @@ def _validate_initial_state(config: ConfigType) -> ConfigType:
|
||||
f"'{CONF_TARGET_HUMIDITY}' requires '{CONF_SUPPORTS_TARGET_HUMIDITY}' to be enabled",
|
||||
path=[CONF_INITIAL_STATE, CONF_TARGET_HUMIDITY],
|
||||
)
|
||||
unit = entity_temperature_unit(config)
|
||||
for key in TEMPERATURE_KEYS:
|
||||
if isinstance(value := initial_state.get(key), tuple):
|
||||
initial_state[key] = cv.convert_temperature(*value, unit)
|
||||
return config
|
||||
|
||||
|
||||
@@ -214,9 +200,9 @@ INITIAL_STATE_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_MODE): climate.validate_climate_mode,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.temperature_with_unit,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.temperature_with_unit,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.temperature_with_unit,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.temperature,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.temperature,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.temperature,
|
||||
cv.Optional(CONF_TARGET_HUMIDITY): cv.percentage_int,
|
||||
cv.Exclusive(CONF_FAN_MODE, "fan_mode"): climate.validate_climate_fan_mode,
|
||||
cv.Exclusive(
|
||||
@@ -231,7 +217,7 @@ INITIAL_STATE_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
climate.climate_schema(TemplateClimate, temperature_unit=True)
|
||||
climate.climate_schema(TemplateClimate)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
|
||||
@@ -309,8 +295,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await climate.register_climate(var, config)
|
||||
if (unit := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None:
|
||||
cg.add(var.set_temperature_unit(TEMPERATURE_UNITS[unit]))
|
||||
|
||||
if (sens := config.get(CONF_SENSOR)) is not None:
|
||||
cg.add(var.set_sensor(await cg.get_variable(sens)))
|
||||
@@ -397,19 +381,11 @@ CLIMATE_TEMPLATE_PUBLISH_ACTION_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(TemplateClimate),
|
||||
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.templatable(
|
||||
cv.temperature_with_unit
|
||||
),
|
||||
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_CURRENT_HUMIDITY): cv.templatable(cv.percentage_int),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(
|
||||
cv.temperature_with_unit
|
||||
),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(
|
||||
cv.temperature_with_unit
|
||||
),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(
|
||||
cv.temperature_with_unit
|
||||
),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_TARGET_HUMIDITY): cv.templatable(cv.percentage_int),
|
||||
cv.Optional(CONF_MODE): cv.templatable(climate.validate_climate_mode),
|
||||
cv.Optional(CONF_ACTION): cv.templatable(climate.validate_climate_action),
|
||||
@@ -438,29 +414,15 @@ automation.register_apply_action(
|
||||
"climate.template.publish",
|
||||
CLIMATE_TEMPLATE_PUBLISH_ACTION_SCHEMA,
|
||||
automation.ApplyField(
|
||||
CONF_CURRENT_TEMPERATURE,
|
||||
"current_temperature = {}",
|
||||
cg.float_,
|
||||
climate.temperature_in_entity_unit,
|
||||
CONF_CURRENT_TEMPERATURE, "current_temperature = {}", cg.float_
|
||||
),
|
||||
automation.ApplyField(CONF_CURRENT_HUMIDITY, "current_humidity = {}", cg.float_),
|
||||
automation.ApplyField(CONF_TARGET_TEMPERATURE, "set_target_temperature", cg.float_),
|
||||
automation.ApplyField(
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
"set_target_temperature",
|
||||
cg.float_,
|
||||
climate.temperature_in_entity_unit,
|
||||
CONF_TARGET_TEMPERATURE_LOW, "set_target_temperature_low", cg.float_
|
||||
),
|
||||
automation.ApplyField(
|
||||
CONF_TARGET_TEMPERATURE_LOW,
|
||||
"set_target_temperature_low",
|
||||
cg.float_,
|
||||
climate.temperature_in_entity_unit,
|
||||
),
|
||||
automation.ApplyField(
|
||||
CONF_TARGET_TEMPERATURE_HIGH,
|
||||
"set_target_temperature_high",
|
||||
cg.float_,
|
||||
climate.temperature_in_entity_unit,
|
||||
CONF_TARGET_TEMPERATURE_HIGH, "set_target_temperature_high", cg.float_
|
||||
),
|
||||
automation.ApplyField(CONF_TARGET_HUMIDITY, "set_target_humidity", cg.float_),
|
||||
automation.ApplyField(CONF_MODE, "set_mode", climate.ClimateMode),
|
||||
|
||||
@@ -22,7 +22,6 @@ class TemplateClimate final : public climate::Climate, public Component {
|
||||
climate::ClimateTraits traits() override { return this->traits_; }
|
||||
|
||||
void add_feature_flags(uint32_t flags) { this->traits_.add_feature_flags(flags); }
|
||||
void set_temperature_unit(TemperatureUnit unit) { this->traits_.set_temperature_unit(unit); }
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// The matching feature flag is added from codegen, so the configuration alone decides it.
|
||||
|
||||
@@ -12,12 +12,9 @@ from esphome.const import (
|
||||
CONF_SET_ACTION,
|
||||
CONF_SUPPORTED_MODES,
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.core.entity_helpers import entity_temperature_unit_of_id
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.cpp_types import TEMPERATURE_UNITS
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import template_ns
|
||||
@@ -42,7 +39,7 @@ RESTORE_MODES = {
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
water_heater.water_heater_schema(TemplateWaterHeater, temperature_unit=True)
|
||||
water_heater.water_heater_schema(TemplateWaterHeater)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_OPTIMISTIC, default=True): cv.boolean,
|
||||
@@ -68,8 +65,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await water_heater.register_water_heater(var, config)
|
||||
if (unit := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None:
|
||||
cg.add(var.set_temperature_unit(TEMPERATURE_UNITS[unit]))
|
||||
|
||||
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
|
||||
|
||||
@@ -130,12 +125,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(TemplateWaterHeater),
|
||||
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.templatable(
|
||||
cv.temperature_with_unit
|
||||
),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(
|
||||
cv.temperature_with_unit
|
||||
),
|
||||
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_MODE): cv.templatable(
|
||||
water_heater.validate_water_heater_mode
|
||||
),
|
||||
@@ -154,17 +145,13 @@ async def water_heater_template_publish_to_code(
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
# A static temperature is in the unit of the water heater it is published to
|
||||
unit = entity_temperature_unit_of_id(config[CONF_ID])
|
||||
for key, setter in (
|
||||
(CONF_CURRENT_TEMPERATURE, var.set_current_temperature),
|
||||
(CONF_TARGET_TEMPERATURE, var.set_target_temperature),
|
||||
):
|
||||
if (value := config.get(key)) is None:
|
||||
continue
|
||||
if isinstance(value, tuple):
|
||||
value = cv.convert_temperature(*value, unit)
|
||||
cg.add(setter(await cg.templatable(value, args, cg.float_)))
|
||||
if current_temp := config.get(CONF_CURRENT_TEMPERATURE):
|
||||
template_ = await cg.templatable(current_temp, args, cg.float_)
|
||||
cg.add(var.set_current_temperature(template_))
|
||||
|
||||
if target_temp := config.get(CONF_TARGET_TEMPERATURE):
|
||||
template_ = await cg.templatable(target_temp, args, cg.float_)
|
||||
cg.add(var.set_target_temperature(template_))
|
||||
|
||||
if mode := config.get(CONF_MODE):
|
||||
template_ = await cg.templatable(mode, args, water_heater.WaterHeaterMode)
|
||||
|
||||
@@ -32,7 +32,6 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() {
|
||||
}
|
||||
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_temperature_unit(this->temperature_unit_);
|
||||
if (this->target_temperature_f_.has_value()) {
|
||||
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ class TemplateWaterHeater final : public Component, public water_heater::WaterHe
|
||||
|
||||
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||
void set_restore_mode(TemplateWaterHeaterRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||
void set_temperature_unit(TemperatureUnit unit) { this->temperature_unit_ = unit; }
|
||||
void set_supported_modes(const std::initializer_list<water_heater::WaterHeaterMode> &modes) {
|
||||
this->supported_modes_ = modes;
|
||||
}
|
||||
@@ -55,7 +54,6 @@ class TemplateWaterHeater final : public Component, public water_heater::WaterHe
|
||||
TemplateLambda<bool> away_f_;
|
||||
TemplateLambda<bool> is_on_f_;
|
||||
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
|
||||
TemperatureUnit temperature_unit_{TemperatureUnit::CELSIUS};
|
||||
water_heater::WaterHeaterModeMask supported_modes_;
|
||||
bool optimistic_{true};
|
||||
};
|
||||
|
||||
@@ -6,13 +6,11 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_MAX_TEMPERATURE,
|
||||
CONF_MIN_TEMPERATURE,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VISUAL,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
entity_temperature_unit,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -43,35 +41,19 @@ WATER_HEATER_MODES = {
|
||||
}
|
||||
validate_water_heater_mode = cv.enum(WATER_HEATER_MODES, upper=True)
|
||||
|
||||
|
||||
def _visual_temperatures_in_entity_unit(config: ConfigType) -> ConfigType:
|
||||
"""Convert the visual temperatures into the entity's unit."""
|
||||
unit = entity_temperature_unit(config)
|
||||
visual = config[CONF_VISUAL]
|
||||
for key in (CONF_MIN_TEMPERATURE, CONF_MAX_TEMPERATURE):
|
||||
if isinstance(value := visual.get(key), tuple):
|
||||
visual[key] = cv.convert_temperature(*value, unit)
|
||||
if isinstance(value := visual.get(CONF_TARGET_TEMPERATURE_STEP), tuple):
|
||||
visual[CONF_TARGET_TEMPERATURE_STEP] = cv.convert_temperature(
|
||||
*value, unit, delta=True
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
_WATER_HEATER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_VISUAL, default={}): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature_with_unit,
|
||||
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature_with_unit,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_STEP): cv.temperature_with_unit,
|
||||
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature,
|
||||
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_STEP): cv.float_,
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
_WATER_HEATER_SCHEMA.add_extra(entity_duplicate_validator("water_heater"))
|
||||
_WATER_HEATER_SCHEMA.add_extra(_visual_temperatures_in_entity_unit)
|
||||
|
||||
|
||||
def water_heater_schema(
|
||||
@@ -79,16 +61,8 @@ def water_heater_schema(
|
||||
*,
|
||||
icon: str = cv.UNDEFINED,
|
||||
entity_category: str = cv.UNDEFINED,
|
||||
temperature_unit: bool = False,
|
||||
) -> cv.Schema:
|
||||
"""Build the schema of a water heater platform.
|
||||
|
||||
``temperature_unit`` adds ``unit_of_measurement`` for platforms whose temperatures are all
|
||||
in the unit the user picks; the class then needs ``set_temperature_unit(TemperatureUnit)``.
|
||||
"""
|
||||
schema = {cv.GenerateID(): cv.declare_id(class_)}
|
||||
if temperature_unit:
|
||||
schema[cv.Optional(CONF_UNIT_OF_MEASUREMENT)] = cv.temperature_unit
|
||||
|
||||
for key, default, validator in [
|
||||
(CONF_ICON, icon, cv.icon),
|
||||
|
||||
@@ -77,9 +77,6 @@ from esphome.const import (
|
||||
SCHEDULER_DONT_RUN,
|
||||
TYPE_GIT,
|
||||
TYPE_LOCAL,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_FAHRENHEIT,
|
||||
UNIT_KELVIN,
|
||||
Framework,
|
||||
Toolchain,
|
||||
__version__ as ESPHOME_VERSION,
|
||||
@@ -1419,10 +1416,7 @@ def float_with_unit(quantity, regex_suffix, optional_unit=False):
|
||||
if match is None:
|
||||
raise Invalid(f"Expected {quantity} with unit, got {value}")
|
||||
|
||||
try:
|
||||
mantissa = float(match.group(1))
|
||||
except ValueError as e:
|
||||
raise Invalid(f"Expected {quantity} with unit, got {value}") from e
|
||||
mantissa = float(match.group(1))
|
||||
if match.group(2) not in METRIC_SUFFIXES:
|
||||
raise Invalid(f"Invalid {quantity} suffix {match.group(2)}")
|
||||
|
||||
@@ -1441,72 +1435,53 @@ distance = float_with_unit("distance", "(m)")
|
||||
framerate = float_with_unit("framerate", "(FPS|fps|Fps|FpS|Hz)")
|
||||
angle = float_with_unit("angle", "(°|deg)", optional_unit=True)
|
||||
_temperature_c = float_with_unit("temperature", "(°C|° C|C|°)?")
|
||||
_temperature_c_strict = float_with_unit("temperature", "(°C|° C|C)")
|
||||
_temperature_k = float_with_unit("temperature", "(°K|° K|K)")
|
||||
_temperature_f = float_with_unit("temperature", "(°F|° F|F)")
|
||||
_temperature_k = float_with_unit("temperature", "(°K|° K|K)?")
|
||||
_temperature_f = float_with_unit("temperature", "(°F|° F|F)?")
|
||||
decibel = float_with_unit("decibel", "(dB|dBm|db|dbm)", optional_unit=True)
|
||||
pressure = float_with_unit("pressure", "(bar|Bar)", optional_unit=True)
|
||||
|
||||
|
||||
def temperature_unit(value: Any) -> str:
|
||||
"""Validate the unit an entity reports its temperatures in."""
|
||||
return one_of(UNIT_CELSIUS, UNIT_FAHRENHEIT, UNIT_KELVIN)(value)
|
||||
def temperature(value):
|
||||
err = None
|
||||
try:
|
||||
return _temperature_c(value)
|
||||
except Invalid as orig_err:
|
||||
err = orig_err
|
||||
|
||||
try:
|
||||
kelvin = _temperature_k(value)
|
||||
return kelvin - 273.15
|
||||
except Invalid:
|
||||
pass
|
||||
|
||||
try:
|
||||
fahrenheit = _temperature_f(value)
|
||||
return (fahrenheit - 32) * (5 / 9)
|
||||
except Invalid:
|
||||
pass
|
||||
|
||||
raise err
|
||||
|
||||
|
||||
def temperature_with_unit(value: Any) -> tuple[float, str | None]:
|
||||
"""Parse a temperature and keep the unit written with it; the unit is None when there is none."""
|
||||
for validator, unit in (
|
||||
(_temperature_k, UNIT_KELVIN),
|
||||
(_temperature_f, UNIT_FAHRENHEIT),
|
||||
(_temperature_c_strict, UNIT_CELSIUS),
|
||||
):
|
||||
try:
|
||||
return validator(value), unit
|
||||
except Invalid:
|
||||
pass
|
||||
return _temperature_c(value), None
|
||||
def temperature_delta(value):
|
||||
err = None
|
||||
try:
|
||||
return _temperature_c(value)
|
||||
except Invalid as orig_err:
|
||||
err = orig_err
|
||||
|
||||
try:
|
||||
return _temperature_k(value)
|
||||
except Invalid:
|
||||
pass
|
||||
|
||||
def _temperature_to_celsius(value: float, unit: str, delta: bool) -> float:
|
||||
if unit == UNIT_FAHRENHEIT:
|
||||
return value * (5 / 9) if delta else (value - 32) * (5 / 9)
|
||||
if unit == UNIT_KELVIN:
|
||||
return value if delta else value - 273.15
|
||||
if unit != UNIT_CELSIUS:
|
||||
raise ValueError(f"Unknown temperature unit {unit!r}")
|
||||
return value
|
||||
try:
|
||||
fahrenheit = _temperature_f(value)
|
||||
return fahrenheit * (5 / 9)
|
||||
except Invalid:
|
||||
pass
|
||||
|
||||
|
||||
def _temperature_from_celsius(value: float, unit: str, delta: bool) -> float:
|
||||
if unit == UNIT_FAHRENHEIT:
|
||||
return value * (9 / 5) if delta else value * (9 / 5) + 32
|
||||
if unit == UNIT_KELVIN:
|
||||
return value if delta else value + 273.15
|
||||
return value
|
||||
|
||||
|
||||
def convert_temperature(
|
||||
value: float, unit: str | None, to_unit: str, *, delta: bool = False
|
||||
) -> float:
|
||||
"""Convert a temperature, or with ``delta`` a temperature difference, between units.
|
||||
|
||||
A value written without a unit is taken to be in ``to_unit`` already.
|
||||
"""
|
||||
if unit is None or unit == to_unit:
|
||||
return value
|
||||
return _temperature_from_celsius(
|
||||
_temperature_to_celsius(value, unit, delta), to_unit, delta
|
||||
)
|
||||
|
||||
|
||||
def temperature(value: Any) -> float:
|
||||
"""Parse a temperature in °C, K or °F into degrees Celsius."""
|
||||
return convert_temperature(*temperature_with_unit(value), UNIT_CELSIUS)
|
||||
|
||||
|
||||
def temperature_delta(value: Any) -> float:
|
||||
"""Parse a temperature difference in °C, K or °F into Celsius degrees."""
|
||||
return convert_temperature(*temperature_with_unit(value), UNIT_CELSIUS, delta=True)
|
||||
raise err
|
||||
|
||||
|
||||
_color_temperature_mireds = float_with_unit("Color Temperature", r"(mireds|Mireds)")
|
||||
|
||||
@@ -1260,7 +1260,6 @@ UNIT_DECIBEL_MILLIWATT = "dBm"
|
||||
UNIT_DEGREE_PER_SECOND = "°/s"
|
||||
UNIT_DEGREES = "°"
|
||||
UNIT_EMPTY = ""
|
||||
UNIT_FAHRENHEIT = "°F"
|
||||
UNIT_G = "G"
|
||||
UNIT_GRAMS_PER_CUBIC_METER = "g/m³"
|
||||
UNIT_HECTOPASCAL = "hPa"
|
||||
|
||||
@@ -16,7 +16,6 @@ from esphome.const import (
|
||||
CONF_INTERNAL,
|
||||
CONF_NAME,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.config import (
|
||||
@@ -25,7 +24,7 @@ from esphome.core.config import (
|
||||
UNIT_OF_MEASUREMENT_MAX_LENGTH,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj, RawStatement, add, get_variable
|
||||
from esphome.cpp_types import TEMPERATURE_UNITS, App
|
||||
from esphome.cpp_types import App
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import (
|
||||
cpp_string_escape,
|
||||
@@ -551,19 +550,6 @@ def inherit_property_from(property_to_inherit, parent_id_property, transform=Non
|
||||
return inherit_property
|
||||
|
||||
|
||||
def entity_temperature_unit(config: ConfigType) -> str:
|
||||
"""The unit a climate or water heater reports in: ``unit_of_measurement`` when the platform took
|
||||
the option through its schema factory, else Celsius (anova has an unrelated option of that name)."""
|
||||
unit = config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
return unit if unit in TEMPERATURE_UNITS else UNIT_CELSIUS
|
||||
|
||||
|
||||
def entity_temperature_unit_of_id(entity_id: ID) -> str:
|
||||
"""The unit of the entity with this id, for actions that take static temperatures."""
|
||||
path = CORE.config.get_path_for_id(entity_id)[:-1]
|
||||
return entity_temperature_unit(CORE.config.get_config_for_path(path))
|
||||
|
||||
|
||||
def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigType]:
|
||||
"""Create a validator function to check for duplicate entity names.
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from esphome.const import UNIT_CELSIUS, UNIT_FAHRENHEIT, UNIT_KELVIN
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
global_ns = MockObj("", "")
|
||||
@@ -43,12 +42,6 @@ InternalGPIOPin = esphome_ns.class_("InternalGPIOPin", GPIOPin)
|
||||
gpio_ns = esphome_ns.namespace("gpio")
|
||||
gpio_Flags = gpio_ns.enum("Flags", is_class=True)
|
||||
EntityCategory = esphome_ns.enum("EntityCategory")
|
||||
TemperatureUnit = esphome_ns.enum("TemperatureUnit", is_class=True)
|
||||
TEMPERATURE_UNITS = {
|
||||
UNIT_CELSIUS: TemperatureUnit.CELSIUS,
|
||||
UNIT_FAHRENHEIT: TemperatureUnit.FAHRENHEIT,
|
||||
UNIT_KELVIN: TemperatureUnit.KELVIN,
|
||||
}
|
||||
Parented = esphome_ns.class_("Parented")
|
||||
ESPTime = esphome_ns.struct("ESPTime")
|
||||
StringRef = esphome_ns.class_("StringRef")
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
"""Tests for the climate visual temperature conversion."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.climate import (
|
||||
_visual_temperatures_in_entity_unit,
|
||||
visual_temperature_step,
|
||||
)
|
||||
from esphome.const import (
|
||||
CONF_CURRENT_TEMPERATURE,
|
||||
CONF_MAX_TEMPERATURE,
|
||||
CONF_MIN_TEMPERATURE,
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
CONF_TEMPERATURE_STEP,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VISUAL,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_FAHRENHEIT,
|
||||
)
|
||||
from esphome.core.entity_helpers import entity_temperature_unit
|
||||
|
||||
|
||||
def test_visual_temperature_step_scalar_applies_to_both() -> None:
|
||||
step = visual_temperature_step("1°F")
|
||||
assert step == {
|
||||
CONF_TARGET_TEMPERATURE: (1.0, UNIT_FAHRENHEIT),
|
||||
CONF_CURRENT_TEMPERATURE: (1.0, UNIT_FAHRENHEIT),
|
||||
}
|
||||
|
||||
|
||||
def test_visual_temperatures_default_to_celsius() -> None:
|
||||
config = _visual_temperatures_in_entity_unit(
|
||||
{
|
||||
CONF_VISUAL: {
|
||||
CONF_MIN_TEMPERATURE: (50.0, UNIT_FAHRENHEIT),
|
||||
CONF_MAX_TEMPERATURE: (30.0, None),
|
||||
CONF_TEMPERATURE_STEP: visual_temperature_step("1.8°F"),
|
||||
}
|
||||
}
|
||||
)
|
||||
visual = config[CONF_VISUAL]
|
||||
assert visual[CONF_MIN_TEMPERATURE] == pytest.approx(10.0)
|
||||
assert visual[CONF_MAX_TEMPERATURE] == 30.0
|
||||
assert visual[CONF_TEMPERATURE_STEP][CONF_TARGET_TEMPERATURE] == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_visual_temperatures_follow_unit_of_measurement() -> None:
|
||||
config = _visual_temperatures_in_entity_unit(
|
||||
{
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_FAHRENHEIT,
|
||||
CONF_VISUAL: {
|
||||
CONF_MIN_TEMPERATURE: (60.0, None),
|
||||
CONF_MAX_TEMPERATURE: (30.0, UNIT_CELSIUS),
|
||||
CONF_TEMPERATURE_STEP: {
|
||||
CONF_TARGET_TEMPERATURE: (1.0, UNIT_FAHRENHEIT),
|
||||
CONF_CURRENT_TEMPERATURE: (0.5, UNIT_CELSIUS),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
visual = config[CONF_VISUAL]
|
||||
assert visual[CONF_MIN_TEMPERATURE] == 60.0
|
||||
assert visual[CONF_MAX_TEMPERATURE] == pytest.approx(86.0)
|
||||
assert visual[CONF_TEMPERATURE_STEP][CONF_TARGET_TEMPERATURE] == 1.0
|
||||
assert visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE] == pytest.approx(0.9)
|
||||
|
||||
|
||||
def test_platform_with_its_own_unit_option_keeps_celsius() -> None:
|
||||
# anova validates unit_of_measurement as c|f for the device, not for the entity
|
||||
assert entity_temperature_unit({CONF_UNIT_OF_MEASUREMENT: "f"}) == UNIT_CELSIUS
|
||||
assert (
|
||||
entity_temperature_unit({CONF_UNIT_OF_MEASUREMENT: UNIT_FAHRENHEIT})
|
||||
== UNIT_FAHRENHEIT
|
||||
)
|
||||
|
||||
|
||||
def test_visual_temperatures_converted_once() -> None:
|
||||
config = {CONF_VISUAL: {CONF_MIN_TEMPERATURE: (50.0, UNIT_FAHRENHEIT)}}
|
||||
_visual_temperatures_in_entity_unit(config)
|
||||
_visual_temperatures_in_entity_unit(config)
|
||||
assert config[CONF_VISUAL][CONF_MIN_TEMPERATURE] == pytest.approx(10.0)
|
||||
@@ -24,8 +24,6 @@ from esphome.const import (
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
CONF_TARGET_TEMPERATURE_HIGH,
|
||||
CONF_TARGET_TEMPERATURE_LOW,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
UNIT_FAHRENHEIT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -145,14 +143,3 @@ def test_initial_state_matching_two_point_accepted() -> None:
|
||||
},
|
||||
}
|
||||
assert _validate_initial_state(config) is config
|
||||
|
||||
|
||||
def test_initial_state_temperature_follows_unit_of_measurement() -> None:
|
||||
config: ConfigType = {
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_FAHRENHEIT,
|
||||
CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: False,
|
||||
CONF_SUPPORTS_TARGET_HUMIDITY: False,
|
||||
CONF_INITIAL_STATE: {CONF_TARGET_TEMPERATURE: (21.0, "°C")},
|
||||
}
|
||||
_validate_initial_state(config)
|
||||
assert config[CONF_INITIAL_STATE][CONF_TARGET_TEMPERATURE] == pytest.approx(69.8)
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
"""Tests for the water heater visual temperature conversion."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.water_heater import (
|
||||
CONF_TARGET_TEMPERATURE_STEP,
|
||||
_visual_temperatures_in_entity_unit,
|
||||
)
|
||||
from esphome.const import (
|
||||
CONF_MAX_TEMPERATURE,
|
||||
CONF_MIN_TEMPERATURE,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VISUAL,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_FAHRENHEIT,
|
||||
)
|
||||
|
||||
|
||||
def test_visual_temperatures_default_to_celsius() -> None:
|
||||
config = _visual_temperatures_in_entity_unit(
|
||||
{
|
||||
CONF_VISUAL: {
|
||||
CONF_MIN_TEMPERATURE: (100.0, UNIT_FAHRENHEIT),
|
||||
CONF_MAX_TEMPERATURE: (85.0, None),
|
||||
CONF_TARGET_TEMPERATURE_STEP: (1.8, UNIT_FAHRENHEIT),
|
||||
}
|
||||
}
|
||||
)
|
||||
visual = config[CONF_VISUAL]
|
||||
assert visual[CONF_MIN_TEMPERATURE] == pytest.approx(37.78, abs=0.01)
|
||||
assert visual[CONF_MAX_TEMPERATURE] == 85.0
|
||||
assert visual[CONF_TARGET_TEMPERATURE_STEP] == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_visual_temperatures_follow_unit_of_measurement() -> None:
|
||||
config = _visual_temperatures_in_entity_unit(
|
||||
{
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_FAHRENHEIT,
|
||||
CONF_VISUAL: {
|
||||
CONF_MIN_TEMPERATURE: (100.0, None),
|
||||
CONF_MAX_TEMPERATURE: (60.0, UNIT_CELSIUS),
|
||||
CONF_TARGET_TEMPERATURE_STEP: (1.0, UNIT_CELSIUS),
|
||||
},
|
||||
}
|
||||
)
|
||||
visual = config[CONF_VISUAL]
|
||||
assert visual[CONF_MIN_TEMPERATURE] == 100.0
|
||||
assert visual[CONF_MAX_TEMPERATURE] == pytest.approx(140.0)
|
||||
assert visual[CONF_TARGET_TEMPERATURE_STEP] == pytest.approx(1.8)
|
||||
@@ -0,0 +1,20 @@
|
||||
from pathlib import Path
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.const import CONF_ESPHOME, CONF_INCLUDES
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
TESTS_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# The column walk in Display::draw_pixels_at() is only built with PSRAM; define it
|
||||
# when display's own tests are built so both walk orders run, and leave other
|
||||
# components' test binaries on the plain build.
|
||||
async def to_code_testing(config: ConfigType) -> None:
|
||||
if any(Path(p) == TESTS_DIR for p in CORE.config[CONF_ESPHOME][CONF_INCLUDES]):
|
||||
cg.add_define("USE_PSRAM")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -0,0 +1,109 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <initializer_list>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
|
||||
namespace esphome::display::testing {
|
||||
|
||||
// Records every pixel write; pixel_axes_swapped() picks the walk order.
|
||||
class RecordingDisplay : public Display {
|
||||
public:
|
||||
void set_axes_swapped(bool swapped) { this->axes_swapped_ = swapped; }
|
||||
bool pixel_axes_swapped() const override { return this->axes_swapped_; }
|
||||
void draw_pixel_at(int x, int y, Color color) override { this->pixels_.emplace_back(x, y, color.raw_32); }
|
||||
void update() override {}
|
||||
DisplayType get_display_type() override { return DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
int get_width_internal() override { return 64; }
|
||||
int get_height_internal() override { return 64; }
|
||||
|
||||
std::vector<std::tuple<int, int, uint32_t>> sorted_pixels() const {
|
||||
auto sorted = this->pixels_;
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
return sorted;
|
||||
}
|
||||
size_t count() const { return this->pixels_.size(); }
|
||||
const std::vector<std::tuple<int, int, uint32_t>> &pixels() const { return this->pixels_; }
|
||||
|
||||
protected:
|
||||
bool axes_swapped_{false};
|
||||
std::vector<std::tuple<int, int, uint32_t>> pixels_;
|
||||
};
|
||||
|
||||
// A 7x5 region with a 2 pixel left offset, a 3 line top offset and 4 pixels of
|
||||
// right padding, so the source index arithmetic of both walks is exercised.
|
||||
static constexpr int W = 7, H = 5, X_OFFSET = 2, Y_OFFSET = 3, X_PAD = 4;
|
||||
static constexpr int LINE_STRIDE = X_OFFSET + W + X_PAD;
|
||||
static constexpr int LINES = Y_OFFSET + H;
|
||||
|
||||
static std::vector<uint8_t> make_source(int bytes_per_pixel) {
|
||||
std::vector<uint8_t> source(LINE_STRIDE * LINES * bytes_per_pixel);
|
||||
uint8_t value = 1;
|
||||
for (auto &byte : source)
|
||||
byte = value++;
|
||||
return source;
|
||||
}
|
||||
|
||||
static void expect_walks_match(ColorBitness bitness, int bytes_per_pixel, bool big_endian) {
|
||||
const auto source = make_source(bytes_per_pixel);
|
||||
RecordingDisplay rows;
|
||||
RecordingDisplay columns;
|
||||
columns.set_axes_swapped(true);
|
||||
rows.draw_pixels_at(10, 20, W, H, source.data(), COLOR_ORDER_RGB, bitness, big_endian, X_OFFSET, Y_OFFSET, X_PAD);
|
||||
columns.draw_pixels_at(10, 20, W, H, source.data(), COLOR_ORDER_RGB, bitness, big_endian, X_OFFSET, Y_OFFSET, X_PAD);
|
||||
EXPECT_EQ(rows.count(), static_cast<size_t>(W * H));
|
||||
EXPECT_EQ(columns.count(), static_cast<size_t>(W * H));
|
||||
EXPECT_EQ(rows.sorted_pixels(), columns.sorted_pixels());
|
||||
}
|
||||
|
||||
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk332) { expect_walks_match(COLOR_BITNESS_332, 1, true); }
|
||||
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk565BigEndian) { expect_walks_match(COLOR_BITNESS_565, 2, true); }
|
||||
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk565LittleEndian) { expect_walks_match(COLOR_BITNESS_565, 2, false); }
|
||||
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk888BigEndian) { expect_walks_match(COLOR_BITNESS_888, 3, true); }
|
||||
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk888LittleEndian) { expect_walks_match(COLOR_BITNESS_888, 3, false); }
|
||||
|
||||
// One known pixel per bitness and byte order, so a decode that swapped bytes or channels
|
||||
// could not pass the walk comparison above by being wrong the same way twice.
|
||||
static void expect_golden(ColorBitness bitness, std::initializer_list<uint8_t> bytes, bool big_endian, Color expected) {
|
||||
const std::vector<uint8_t> source(bytes);
|
||||
RecordingDisplay display;
|
||||
display.draw_pixels_at(3, 4, 1, 1, source.data(), COLOR_ORDER_RGB, bitness, big_endian, 0, 0, 0);
|
||||
ASSERT_EQ(display.count(), 1u);
|
||||
EXPECT_EQ(display.pixels()[0], std::make_tuple(3, 4, expected.raw_32));
|
||||
}
|
||||
|
||||
TEST(DrawPixelsAt, Golden565BigEndian) {
|
||||
expect_golden(COLOR_BITNESS_565, {0xE0, 0x07}, true, Color(230, 0, 57)); // 0xE007: r 28, g 0, b 7
|
||||
}
|
||||
TEST(DrawPixelsAt, Golden565LittleEndian) {
|
||||
expect_golden(COLOR_BITNESS_565, {0xE0, 0x07}, false, Color(0, 255, 0)); // 0x07E0: pure green
|
||||
}
|
||||
TEST(DrawPixelsAt, Golden888BigEndian) {
|
||||
expect_golden(COLOR_BITNESS_888, {0x10, 0x20, 0x30}, true, Color(0x10, 0x20, 0x30));
|
||||
}
|
||||
TEST(DrawPixelsAt, Golden888LittleEndian) {
|
||||
expect_golden(COLOR_BITNESS_888, {0x10, 0x20, 0x30}, false, Color(0x30, 0x20, 0x10));
|
||||
}
|
||||
TEST(DrawPixelsAt, Golden332) {
|
||||
expect_golden(COLOR_BITNESS_332, {0xE3}, true, Color(255, 0, 255)); // r 7 of 7, g 0, b 3 of 3
|
||||
}
|
||||
|
||||
// The column walk really is a different order, otherwise the tests above prove nothing.
|
||||
TEST(DrawPixelsAt, ColumnWalkChangesOrder) {
|
||||
const auto source = make_source(1);
|
||||
RecordingDisplay rows;
|
||||
RecordingDisplay columns;
|
||||
columns.set_axes_swapped(true);
|
||||
rows.draw_pixels_at(0, 0, W, H, source.data(), COLOR_ORDER_RGB, COLOR_BITNESS_332, true, X_OFFSET, Y_OFFSET, X_PAD);
|
||||
columns.draw_pixels_at(0, 0, W, H, source.data(), COLOR_ORDER_RGB, COLOR_BITNESS_332, true, X_OFFSET, Y_OFFSET,
|
||||
X_PAD);
|
||||
ASSERT_GT(rows.count(), 1u);
|
||||
ASSERT_GT(columns.count(), 1u);
|
||||
EXPECT_EQ(std::get<0>(rows.pixels()[1]), 1); // the row walk steps x first
|
||||
EXPECT_EQ(std::get<1>(columns.pixels()[1]), 1); // the column walk steps y first
|
||||
}
|
||||
|
||||
} // namespace esphome::display::testing
|
||||
@@ -41,17 +41,6 @@ esphome:
|
||||
mode: !lambda "return climate::CLIMATE_MODE_COOL;"
|
||||
target_temperature: !lambda "return 23.0f;"
|
||||
|
||||
- climate.template.publish:
|
||||
id: template_climate_fahrenheit
|
||||
current_temperature: 20°C
|
||||
target_temperature: 70
|
||||
- climate.control:
|
||||
id: template_climate_fahrenheit
|
||||
target_temperature: 22°C
|
||||
- water_heater.template.publish:
|
||||
id: template_water_heater_fahrenheit
|
||||
current_temperature: 45°C
|
||||
target_temperature: 120
|
||||
- climate.template.publish:
|
||||
id: template_climate_custom_modes
|
||||
custom_fan_mode: "turbo"
|
||||
@@ -603,25 +592,6 @@ alarm_control_panel:
|
||||
- "1234"
|
||||
|
||||
climate:
|
||||
# unit_of_measurement keeps the visual temperatures in that unit; a value written in
|
||||
# another unit is converted into it
|
||||
- platform: template
|
||||
id: template_climate_fahrenheit
|
||||
name: "Template Climate Fahrenheit"
|
||||
unit_of_measurement: °F
|
||||
optimistic: true
|
||||
supported_modes:
|
||||
- "OFF"
|
||||
- HEAT
|
||||
initial_state:
|
||||
mode: HEAT
|
||||
target_temperature: 21°C
|
||||
visual:
|
||||
min_temperature: 60
|
||||
max_temperature: 30°C
|
||||
temperature_step:
|
||||
target_temperature: 1°F
|
||||
current_temperature: 0.5°C
|
||||
- platform: template
|
||||
id: template_climate
|
||||
name: "Template Climate"
|
||||
@@ -714,17 +684,6 @@ climate:
|
||||
temperature_step: 0.5
|
||||
|
||||
water_heater:
|
||||
- platform: template
|
||||
id: template_water_heater_fahrenheit
|
||||
name: "Template Water Heater Fahrenheit"
|
||||
unit_of_measurement: °F
|
||||
optimistic: true
|
||||
current_temperature: !lambda "return 110.0f;"
|
||||
target_temperature: !lambda "return 120.0f;"
|
||||
visual:
|
||||
min_temperature: 100
|
||||
max_temperature: 60°C
|
||||
target_temperature_step: 1°C
|
||||
- platform: template
|
||||
id: template_water_heater
|
||||
name: "Template Water Heater"
|
||||
|
||||
@@ -2,11 +2,15 @@ water_heater:
|
||||
- platform: template
|
||||
id: my_boiler
|
||||
name: "Test Boiler"
|
||||
min_temperature: 10
|
||||
max_temperature: 85
|
||||
target_temperature_step: 0.5
|
||||
current_temperature_step: 0.1
|
||||
optimistic: true
|
||||
current_temperature: !lambda "return 45.0f;"
|
||||
target_temperature: !lambda "return 60.0f;"
|
||||
current_temperature: 45.0
|
||||
mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;"
|
||||
visual:
|
||||
min_temperature: 10
|
||||
max_temperature: 85
|
||||
target_temperature_step: 0.5
|
||||
current_temperature_step: 0.1
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
packages:
|
||||
water_heater: !include common.yaml
|
||||
@@ -1,2 +0,0 @@
|
||||
packages:
|
||||
water_heater: !include common.yaml
|
||||
@@ -51,9 +51,6 @@ from esphome.const import (
|
||||
SCHEDULER_DONT_RUN,
|
||||
TYPE_GIT,
|
||||
TYPE_LOCAL,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_FAHRENHEIT,
|
||||
UNIT_KELVIN,
|
||||
Framework,
|
||||
Toolchain,
|
||||
)
|
||||
@@ -2148,60 +2145,6 @@ def test_temperature_delta_invalid() -> None:
|
||||
cv.temperature_delta("5x")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("100°F", (100.0, UNIT_FAHRENHEIT)),
|
||||
("100 °F", (100.0, UNIT_FAHRENHEIT)),
|
||||
("100F", (100.0, UNIT_FAHRENHEIT)),
|
||||
("25°C", (25.0, UNIT_CELSIUS)),
|
||||
("25C", (25.0, UNIT_CELSIUS)),
|
||||
("300K", (300.0, UNIT_KELVIN)),
|
||||
("300°K", (300.0, UNIT_KELVIN)),
|
||||
("25", (25.0, None)),
|
||||
("25°", (25.0, None)),
|
||||
(25, (25.0, None)),
|
||||
],
|
||||
)
|
||||
def test_temperature_with_unit(
|
||||
value: str | int, expected: tuple[float, str | None]
|
||||
) -> None:
|
||||
assert cv.temperature_with_unit(value) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["5x", "abc°F", None])
|
||||
def test_temperature_with_unit__invalid(value: str | None) -> None:
|
||||
with pytest.raises(Invalid):
|
||||
cv.temperature_with_unit(value)
|
||||
|
||||
|
||||
def test_temperature_unit() -> None:
|
||||
assert cv.temperature_unit("°F") == UNIT_FAHRENHEIT
|
||||
with pytest.raises(Invalid):
|
||||
cv.temperature_unit("F")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "unit", "to_unit", "delta", "expected"),
|
||||
[
|
||||
(60.0, None, UNIT_FAHRENHEIT, False, 60.0),
|
||||
(30.0, UNIT_CELSIUS, UNIT_FAHRENHEIT, False, 86.0),
|
||||
(212.0, UNIT_FAHRENHEIT, UNIT_CELSIUS, False, 100.0),
|
||||
(300.0, UNIT_KELVIN, UNIT_FAHRENHEIT, False, 80.33),
|
||||
(86.0, UNIT_FAHRENHEIT, UNIT_KELVIN, False, 303.15),
|
||||
(0.5, UNIT_CELSIUS, UNIT_FAHRENHEIT, True, 0.9),
|
||||
(9.0, UNIT_FAHRENHEIT, UNIT_CELSIUS, True, 5.0),
|
||||
(1.0, UNIT_KELVIN, UNIT_CELSIUS, True, 1.0),
|
||||
],
|
||||
)
|
||||
def test_convert_temperature(
|
||||
value: float, unit: str | None, to_unit: str, delta: bool, expected: float
|
||||
) -> None:
|
||||
assert cv.convert_temperature(value, unit, to_unit, delta=delta) == pytest.approx(
|
||||
expected
|
||||
)
|
||||
|
||||
|
||||
def test_color_temperature_mireds() -> None:
|
||||
assert cv.color_temperature("153 mireds") == pytest.approx(153.0)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user