mirror of
https://github.com/esphome/esphome.git
synced 2025-03-01 00:08:15 +00:00
Merge branch 'component_climate_ir_samsung' of https://github.com/jorofi/esphome into component_climate_ir_samsung
This commit is contained in:
commit
f68f261031
4
.github/actions/build-image/action.yaml
vendored
4
.github/actions/build-image/action.yaml
vendored
@ -46,7 +46,7 @@ runs:
|
||||
|
||||
- name: Build and push to ghcr by digest
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@v6.10.0
|
||||
uses: docker/build-push-action@v6.11.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
@ -72,7 +72,7 @@ runs:
|
||||
|
||||
- name: Build and push to dockerhub by digest
|
||||
id: build-dockerhub
|
||||
uses: docker/build-push-action@v6.10.0
|
||||
uses: docker/build-push-action@v6.11.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
2
.github/workflows/ci-docker.yml
vendored
2
.github/workflows/ci-docker.yml
vendored
@ -48,7 +48,7 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
|
||||
- name: Set TAG
|
||||
run: |
|
||||
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@ -93,7 +93,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
- name: Set up QEMU
|
||||
if: matrix.platform != 'linux/amd64'
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@v3.3.0
|
||||
@ -141,7 +141,7 @@ jobs:
|
||||
echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
with:
|
||||
name: digests-${{ steps.sanitize.outputs.name }}
|
||||
path: /tmp/digests
|
||||
|
2
.github/workflows/sync-device-classes.yml
vendored
2
.github/workflows/sync-device-classes.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
||||
python ./script/sync-device_class.py
|
||||
|
||||
- name: Commit changes
|
||||
uses: peter-evans/create-pull-request@v7.0.5
|
||||
uses: peter-evans/create-pull-request@v7.0.6
|
||||
with:
|
||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||
committer: esphomebot <esphome@nabucasa.com>
|
||||
|
@ -302,7 +302,7 @@ esphome/components/noblex/* @AGalfra
|
||||
esphome/components/npi19/* @bakerkj
|
||||
esphome/components/number/* @esphome/core
|
||||
esphome/components/one_wire/* @ssieb
|
||||
esphome/components/online_image/* @guillempages
|
||||
esphome/components/online_image/* @clydebarrow @guillempages
|
||||
esphome/components/opentherm/* @olegtarasov
|
||||
esphome/components/ota/* @esphome/core
|
||||
esphome/components/output/* @esphome/core
|
||||
|
@ -1,6 +1,11 @@
|
||||
# ESPHome [](https://discord.gg/KhAMKrd) [](https://GitHub.com/esphome/esphome/releases/)
|
||||
|
||||
[](https://esphome.io/)
|
||||
<a href="https://esphome.io/">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://esphome.io/_static/logo-text-on-dark.svg", alt="ESPHome Logo">
|
||||
<img src="https://esphome.io/_static/logo-text-on-light.svg" alt="ESPHome Logo">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
**Documentation:** https://esphome.io/
|
||||
|
||||
|
@ -29,7 +29,7 @@ RUN \
|
||||
# Use pinned versions so that we get updates with build caching
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
python3-pip=23.0.1+dfsg-1 \
|
||||
python3-setuptools=66.1.1-1 \
|
||||
python3-setuptools=66.1.1-1+deb12u1 \
|
||||
python3-venv=3.11.2-1+b1 \
|
||||
python3-wheel=0.38.4-2 \
|
||||
iputils-ping=3:20221126-1+deb12u1 \
|
||||
|
@ -758,6 +758,14 @@ def parse_args(argv):
|
||||
options_parser.add_argument(
|
||||
"-q", "--quiet", help="Disable all ESPHome logs.", action="store_true"
|
||||
)
|
||||
options_parser.add_argument(
|
||||
"-l",
|
||||
"--log-level",
|
||||
help="Set the log level.",
|
||||
default=os.getenv("ESPHOME_LOG_LEVEL", "INFO"),
|
||||
action="store",
|
||||
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||
)
|
||||
options_parser.add_argument(
|
||||
"--dashboard", help=argparse.SUPPRESS, action="store_true"
|
||||
)
|
||||
@ -987,11 +995,16 @@ def run_esphome(argv):
|
||||
args = parse_args(argv)
|
||||
CORE.dashboard = args.dashboard
|
||||
|
||||
# Override log level if verbose is set
|
||||
if args.verbose:
|
||||
args.log_level = "DEBUG"
|
||||
elif args.quiet:
|
||||
args.log_level = "CRITICAL"
|
||||
|
||||
setup_log(
|
||||
args.verbose,
|
||||
args.quiet,
|
||||
log_level=args.log_level,
|
||||
# Show timestamp for dashboard access logs
|
||||
args.command == "dashboard",
|
||||
include_timestamp=args.command == "dashboard",
|
||||
)
|
||||
|
||||
if args.command in PRE_CONFIG_ACTIONS:
|
||||
|
@ -1,28 +1,10 @@
|
||||
import logging
|
||||
|
||||
from esphome import automation, core
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.components.image as espImage
|
||||
from esphome.components.image import (
|
||||
CONF_USE_TRANSPARENCY,
|
||||
LOCAL_SCHEMA,
|
||||
SOURCE_LOCAL,
|
||||
SOURCE_WEB,
|
||||
WEB_SCHEMA,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_FILE,
|
||||
CONF_ID,
|
||||
CONF_PATH,
|
||||
CONF_RAW_DATA_ID,
|
||||
CONF_REPEAT,
|
||||
CONF_RESIZE,
|
||||
CONF_SOURCE,
|
||||
CONF_TYPE,
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.const import CONF_ID, CONF_REPEAT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -30,6 +12,7 @@ AUTO_LOAD = ["image"]
|
||||
CODEOWNERS = ["@syndlex"]
|
||||
DEPENDENCIES = ["display"]
|
||||
MULTI_CONF = True
|
||||
MULTI_CONF_NO_DEFAULT = True
|
||||
|
||||
CONF_LOOP = "loop"
|
||||
CONF_START_FRAME = "start_frame"
|
||||
@ -51,72 +34,9 @@ SetFrameAction = animation_ns.class_(
|
||||
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
|
||||
TYPED_FILE_SCHEMA = cv.typed_schema(
|
||||
{
|
||||
SOURCE_LOCAL: LOCAL_SCHEMA,
|
||||
SOURCE_WEB: WEB_SCHEMA,
|
||||
},
|
||||
key=CONF_SOURCE,
|
||||
)
|
||||
|
||||
|
||||
def _file_schema(value):
|
||||
if isinstance(value, str):
|
||||
return validate_file_shorthand(value)
|
||||
return TYPED_FILE_SCHEMA(value)
|
||||
|
||||
|
||||
FILE_SCHEMA = cv.Schema(_file_schema)
|
||||
|
||||
|
||||
def validate_file_shorthand(value):
|
||||
value = cv.string_strict(value)
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_SOURCE: SOURCE_WEB,
|
||||
CONF_URL: value,
|
||||
}
|
||||
)
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_SOURCE: SOURCE_LOCAL,
|
||||
CONF_PATH: value,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def validate_cross_dependencies(config):
|
||||
"""
|
||||
Validate fields whose possible values depend on other fields.
|
||||
For example, validate that explicitly transparent image types
|
||||
have "use_transparency" set to True.
|
||||
Also set the default value for those kind of dependent fields.
|
||||
"""
|
||||
image_type = config[CONF_TYPE]
|
||||
is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"]
|
||||
# If the use_transparency option was not specified, set the default depending on the image type
|
||||
if CONF_USE_TRANSPARENCY not in config:
|
||||
config[CONF_USE_TRANSPARENCY] = is_transparent_type
|
||||
|
||||
if is_transparent_type and not config[CONF_USE_TRANSPARENCY]:
|
||||
raise cv.Invalid(f"Image type {image_type} must always be transparent.")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
ANIMATION_SCHEMA = cv.Schema(
|
||||
cv.All(
|
||||
CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
||||
cv.Required(CONF_FILE): FILE_SCHEMA,
|
||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
|
||||
espImage.IMAGE_TYPE, upper=True
|
||||
),
|
||||
# Not setting default here on purpose; the default depends on the image type,
|
||||
# and thus will be set in the "validate_cross_dependencies" validator.
|
||||
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
|
||||
cv.Optional(CONF_LOOP): cv.All(
|
||||
{
|
||||
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
|
||||
@ -124,13 +44,9 @@ ANIMATION_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_REPEAT): cv.positive_int,
|
||||
}
|
||||
),
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
},
|
||||
validate_cross_dependencies,
|
||||
)
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = ANIMATION_SCHEMA
|
||||
|
||||
NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
@ -164,180 +80,26 @@ async def animation_action_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
from PIL import Image
|
||||
(
|
||||
prog_arr,
|
||||
width,
|
||||
height,
|
||||
image_type,
|
||||
trans_value,
|
||||
frame_count,
|
||||
) = await espImage.write_image(config, all_frames=True)
|
||||
|
||||
conf_file = config[CONF_FILE]
|
||||
if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
|
||||
path = CORE.relative_config_path(conf_file[CONF_PATH])
|
||||
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
|
||||
path = espImage.compute_local_image_path(conf_file).as_posix()
|
||||
else:
|
||||
raise core.EsphomeError(f"Unknown animation source: {conf_file[CONF_SOURCE]}")
|
||||
|
||||
try:
|
||||
image = Image.open(path)
|
||||
except Exception as e:
|
||||
raise core.EsphomeError(f"Could not load image file {path}: {e}")
|
||||
|
||||
width, height = image.size
|
||||
frames = image.n_frames
|
||||
if CONF_RESIZE in config:
|
||||
new_width_max, new_height_max = config[CONF_RESIZE]
|
||||
ratio = min(new_width_max / width, new_height_max / height)
|
||||
width, height = int(width * ratio), int(height * ratio)
|
||||
elif width > 500 or height > 500:
|
||||
_LOGGER.warning(
|
||||
'The image "%s" you requested is very big. Please consider'
|
||||
" using the resize parameter.",
|
||||
path,
|
||||
)
|
||||
|
||||
transparent = config[CONF_USE_TRANSPARENCY]
|
||||
|
||||
if config[CONF_TYPE] == "GRAYSCALE":
|
||||
data = [0 for _ in range(height * width * frames)]
|
||||
pos = 0
|
||||
for frameIndex in range(frames):
|
||||
image.seek(frameIndex)
|
||||
frame = image.convert("LA", dither=Image.Dither.NONE)
|
||||
if CONF_RESIZE in config:
|
||||
frame = frame.resize([width, height])
|
||||
pixels = list(frame.getdata())
|
||||
if len(pixels) != height * width:
|
||||
raise core.EsphomeError(
|
||||
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
|
||||
)
|
||||
for pix, a in pixels:
|
||||
if transparent:
|
||||
if pix == 1:
|
||||
pix = 0
|
||||
if a < 0x80:
|
||||
pix = 1
|
||||
|
||||
data[pos] = pix
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] == "RGBA":
|
||||
data = [0 for _ in range(height * width * 4 * frames)]
|
||||
pos = 0
|
||||
for frameIndex in range(frames):
|
||||
image.seek(frameIndex)
|
||||
frame = image.convert("RGBA")
|
||||
if CONF_RESIZE in config:
|
||||
frame = frame.resize([width, height])
|
||||
pixels = list(frame.getdata())
|
||||
if len(pixels) != height * width:
|
||||
raise core.EsphomeError(
|
||||
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
|
||||
)
|
||||
for pix in pixels:
|
||||
data[pos] = pix[0]
|
||||
pos += 1
|
||||
data[pos] = pix[1]
|
||||
pos += 1
|
||||
data[pos] = pix[2]
|
||||
pos += 1
|
||||
data[pos] = pix[3]
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] == "RGB24":
|
||||
data = [0 for _ in range(height * width * 3 * frames)]
|
||||
pos = 0
|
||||
for frameIndex in range(frames):
|
||||
image.seek(frameIndex)
|
||||
frame = image.convert("RGBA")
|
||||
if CONF_RESIZE in config:
|
||||
frame = frame.resize([width, height])
|
||||
pixels = list(frame.getdata())
|
||||
if len(pixels) != height * width:
|
||||
raise core.EsphomeError(
|
||||
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
|
||||
)
|
||||
for r, g, b, a in pixels:
|
||||
if transparent:
|
||||
if r == 0 and g == 0 and b == 1:
|
||||
b = 0
|
||||
if a < 0x80:
|
||||
r = 0
|
||||
g = 0
|
||||
b = 1
|
||||
|
||||
data[pos] = r
|
||||
pos += 1
|
||||
data[pos] = g
|
||||
pos += 1
|
||||
data[pos] = b
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]:
|
||||
bytes_per_pixel = 3 if transparent else 2
|
||||
data = [0 for _ in range(height * width * bytes_per_pixel * frames)]
|
||||
pos = 0
|
||||
for frameIndex in range(frames):
|
||||
image.seek(frameIndex)
|
||||
frame = image.convert("RGBA")
|
||||
if CONF_RESIZE in config:
|
||||
frame = frame.resize([width, height])
|
||||
pixels = list(frame.getdata())
|
||||
if len(pixels) != height * width:
|
||||
raise core.EsphomeError(
|
||||
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
|
||||
)
|
||||
for r, g, b, a in pixels:
|
||||
R = r >> 3
|
||||
G = g >> 2
|
||||
B = b >> 3
|
||||
rgb = (R << 11) | (G << 5) | B
|
||||
data[pos] = rgb >> 8
|
||||
pos += 1
|
||||
data[pos] = rgb & 0xFF
|
||||
pos += 1
|
||||
if transparent:
|
||||
data[pos] = a
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
|
||||
width8 = ((width + 7) // 8) * 8
|
||||
data = [0 for _ in range((height * width8 // 8) * frames)]
|
||||
for frameIndex in range(frames):
|
||||
image.seek(frameIndex)
|
||||
if transparent:
|
||||
alpha = image.split()[-1]
|
||||
has_alpha = alpha.getextrema()[0] < 0xFF
|
||||
else:
|
||||
has_alpha = False
|
||||
frame = image.convert("1", dither=Image.Dither.NONE)
|
||||
if CONF_RESIZE in config:
|
||||
frame = frame.resize([width, height])
|
||||
if transparent:
|
||||
alpha = alpha.resize([width, height])
|
||||
for x, y in [(i, j) for i in range(width) for j in range(height)]:
|
||||
if transparent and has_alpha:
|
||||
if not alpha.getpixel((x, y)):
|
||||
continue
|
||||
elif frame.getpixel((x, y)):
|
||||
continue
|
||||
|
||||
pos = x + y * width8 + (height * width8 * frameIndex)
|
||||
data[pos // 8] |= 0x80 >> (pos % 8)
|
||||
else:
|
||||
raise core.EsphomeError(
|
||||
f"Animation f{config[CONF_ID]} has not supported type {config[CONF_TYPE]}."
|
||||
)
|
||||
|
||||
rhs = [HexInt(x) for x in data]
|
||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
prog_arr,
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
espImage.IMAGE_TYPE[config[CONF_TYPE]],
|
||||
frame_count,
|
||||
image_type,
|
||||
trans_value,
|
||||
)
|
||||
cg.add(var.set_transparency(transparent))
|
||||
if loop_config := config.get(CONF_LOOP):
|
||||
start = loop_config[CONF_START_FRAME]
|
||||
end = loop_config.get(CONF_END_FRAME, frames)
|
||||
end = loop_config.get(CONF_END_FRAME, frame_count)
|
||||
count = loop_config.get(CONF_REPEAT, -1)
|
||||
cg.add(var.set_loop(start, end, count))
|
||||
|
@ -6,8 +6,8 @@ namespace esphome {
|
||||
namespace animation {
|
||||
|
||||
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
|
||||
image::ImageType type)
|
||||
: Image(data_start, width, height, type),
|
||||
image::ImageType type, image::Transparency transparent)
|
||||
: Image(data_start, width, height, type, transparent),
|
||||
animation_data_start_(data_start),
|
||||
current_frame_(0),
|
||||
animation_frame_count_(animation_frame_count),
|
||||
|
@ -8,7 +8,8 @@ namespace animation {
|
||||
|
||||
class Animation : public image::Image {
|
||||
public:
|
||||
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type);
|
||||
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type,
|
||||
image::Transparency transparent);
|
||||
|
||||
uint32_t get_animation_frame_count() const;
|
||||
int get_current_frame() const;
|
||||
|
@ -37,8 +37,9 @@ void ClimateIR::setup() {
|
||||
this->publish_state();
|
||||
});
|
||||
this->current_temperature = this->sensor_->state;
|
||||
} else
|
||||
} else {
|
||||
this->current_temperature = NAN;
|
||||
}
|
||||
// restore set points
|
||||
auto restore = this->restore_state_();
|
||||
if (restore.has_value()) {
|
||||
|
@ -131,8 +131,9 @@ bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteRecei
|
||||
} else {
|
||||
parent->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
}
|
||||
} else
|
||||
} else {
|
||||
parent->mode = climate::CLIMATE_MODE_COOL;
|
||||
}
|
||||
|
||||
// Fan Speed
|
||||
if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || parent->mode == climate::CLIMATE_MODE_HEAT_COOL ||
|
||||
|
@ -50,6 +50,10 @@ void DebugComponent::dump_config() {
|
||||
this->reset_reason_->publish_state(get_reset_reason_());
|
||||
}
|
||||
#endif // USE_TEXT_SENSOR
|
||||
|
||||
#ifdef USE_ESP32
|
||||
this->log_partition_info_(); // Log partition information for ESP32
|
||||
#endif // USE_ESP32
|
||||
}
|
||||
|
||||
void DebugComponent::loop() {
|
||||
|
@ -55,6 +55,20 @@ class DebugComponent : public PollingComponent {
|
||||
#endif // USE_ESP32
|
||||
#endif // USE_SENSOR
|
||||
|
||||
#ifdef USE_ESP32
|
||||
/**
|
||||
* @brief Logs information about the device's partition table.
|
||||
*
|
||||
* This function iterates through the ESP32's partition table and logs details
|
||||
* about each partition, including its name, type, subtype, starting address,
|
||||
* and size. The information is useful for diagnosing issues related to flash
|
||||
* memory or verifying the partition configuration dynamically at runtime.
|
||||
*
|
||||
* Only available when compiled for ESP32 platforms.
|
||||
*/
|
||||
void log_partition_info_();
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
text_sensor::TextSensor *device_info_{nullptr};
|
||||
text_sensor::TextSensor *reset_reason_{nullptr};
|
||||
|
@ -5,6 +5,7 @@
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_system.h>
|
||||
#include <esp_chip_info.h>
|
||||
#include <esp_partition.h>
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
#include <esp32/rom/rtc.h>
|
||||
@ -28,6 +29,19 @@ namespace debug {
|
||||
|
||||
static const char *const TAG = "debug";
|
||||
|
||||
void DebugComponent::log_partition_info_() {
|
||||
ESP_LOGCONFIG(TAG, "Partition table:");
|
||||
ESP_LOGCONFIG(TAG, " %-12s %-4s %-8s %-10s %-10s", "Name", "Type", "Subtype", "Address", "Size");
|
||||
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);
|
||||
while (it != NULL) {
|
||||
const esp_partition_t *partition = esp_partition_get(it);
|
||||
ESP_LOGCONFIG(TAG, " %-12s %-4d %-8d 0x%08X 0x%08X", partition->label, partition->type, partition->subtype,
|
||||
partition->address, partition->size);
|
||||
it = esp_partition_next(it);
|
||||
}
|
||||
esp_partition_iterator_release(it);
|
||||
}
|
||||
|
||||
std::string DebugComponent::get_reset_reason_() {
|
||||
std::string reset_reason;
|
||||
switch (esp_reset_reason()) {
|
||||
@ -276,6 +290,19 @@ void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
device_info += " Cores:" + to_string(info.cores);
|
||||
device_info += " Revision:" + to_string(info.revision);
|
||||
|
||||
// Framework detection
|
||||
device_info += "|Framework: ";
|
||||
#ifdef USE_ARDUINO
|
||||
ESP_LOGD(TAG, "Framework: Arduino");
|
||||
device_info += "Arduino";
|
||||
#elif defined(USE_ESP_IDF)
|
||||
ESP_LOGD(TAG, "Framework: ESP-IDF");
|
||||
device_info += "ESP-IDF";
|
||||
#else
|
||||
ESP_LOGW(TAG, "Framework: UNKNOWN");
|
||||
device_info += "UNKNOWN";
|
||||
#endif
|
||||
|
||||
ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version());
|
||||
device_info += "|ESP-IDF: ";
|
||||
device_info += esp_get_idf_version();
|
||||
|
@ -159,6 +159,15 @@ void DFPlayer::loop() {
|
||||
}
|
||||
break;
|
||||
case 9: // End byte
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
char byte_sequence[100];
|
||||
byte_sequence[0] = '\0';
|
||||
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
|
||||
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
|
||||
this->read_buffer_[i]);
|
||||
}
|
||||
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
|
||||
#endif
|
||||
if (byte != 0xEF) {
|
||||
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
@ -238,13 +247,17 @@ void DFPlayer::loop() {
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
break;
|
||||
case 0x3C:
|
||||
ESP_LOGV(TAG, "Playback finished (USB drive)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
case 0x3D:
|
||||
ESP_LOGV(TAG, "Playback finished");
|
||||
ESP_LOGV(TAG, "Playback finished (SD card)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
break;
|
||||
default:
|
||||
ESP_LOGV(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
|
||||
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
|
||||
}
|
||||
this->sent_cmd_ = 0;
|
||||
this->read_pos_ = 0;
|
||||
|
@ -118,8 +118,9 @@ std::unique_ptr<Command> CircularCommandQueue::dequeue() {
|
||||
if (front_ == rear_) {
|
||||
front_ = -1;
|
||||
rear_ = -1;
|
||||
} else
|
||||
} else {
|
||||
front_ = (front_ + 1) % COMMAND_QUEUE_SIZE;
|
||||
}
|
||||
|
||||
return dequeued_cmd;
|
||||
}
|
||||
|
@ -157,10 +157,11 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
|
||||
if (bit == 0) {
|
||||
bit = 7;
|
||||
byte++;
|
||||
} else
|
||||
} else {
|
||||
bit--;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!report_errors && error_code != 0)
|
||||
return false;
|
||||
|
||||
|
@ -39,6 +39,7 @@ DisplayOnPageChangeTrigger = display_ns.class_(
|
||||
|
||||
CONF_ON_PAGE_CHANGE = "on_page_change"
|
||||
CONF_SHOW_TEST_CARD = "show_test_card"
|
||||
CONF_UNSPECIFIED = "unspecified"
|
||||
|
||||
DISPLAY_ROTATIONS = {
|
||||
0: display_ns.DISPLAY_ROTATION_0_DEGREES,
|
||||
@ -55,16 +56,22 @@ def validate_rotation(value):
|
||||
return cv.enum(DISPLAY_ROTATIONS, int=True)(value)
|
||||
|
||||
|
||||
def validate_auto_clear(value):
|
||||
if value == CONF_UNSPECIFIED:
|
||||
return value
|
||||
return cv.boolean(value)
|
||||
|
||||
|
||||
BASIC_DISPLAY_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_LAMBDA): cv.lambda_,
|
||||
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
).extend(cv.polling_component_schema("1s"))
|
||||
|
||||
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ROTATION): validate_rotation,
|
||||
cv.Optional(CONF_PAGES): cv.All(
|
||||
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
|
||||
cv.ensure_list(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DisplayPage),
|
||||
@ -82,7 +89,9 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
|
||||
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_AUTO_CLEAR_ENABLED, default=True): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
|
||||
): validate_auto_clear,
|
||||
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
|
||||
}
|
||||
)
|
||||
@ -92,8 +101,12 @@ async def setup_display_core_(var, config):
|
||||
if CONF_ROTATION in config:
|
||||
cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]]))
|
||||
|
||||
if CONF_AUTO_CLEAR_ENABLED in config:
|
||||
cg.add(var.set_auto_clear(config[CONF_AUTO_CLEAR_ENABLED]))
|
||||
if auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED):
|
||||
# Default to true if pages or lambda is specified. Ideally this would be done during validation, but
|
||||
# the possible schemas are too complex to do this easily.
|
||||
if auto_clear == CONF_UNSPECIFIED:
|
||||
auto_clear = CONF_LAMBDA in config or CONF_PAGES in config
|
||||
cg.add(var.set_auto_clear(auto_clear))
|
||||
|
||||
if CONF_PAGES in config:
|
||||
pages = []
|
||||
|
@ -266,9 +266,10 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
|
||||
if (dymax < float(-dxmax) * tan_a) {
|
||||
upd_dxmax = ceil(float(dymax) / tan_a);
|
||||
hline_width = -dxmax - upd_dxmax + 1;
|
||||
} else
|
||||
} else {
|
||||
hline_width = 0;
|
||||
}
|
||||
}
|
||||
if (hline_width > 0)
|
||||
this->horizontal_line(center_x + dxmax, center_y - dymax, hline_width, color);
|
||||
}
|
||||
|
@ -90,9 +90,10 @@ void Rect::info(const std::string &prefix) {
|
||||
if (this->is_set()) {
|
||||
ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(),
|
||||
this->y2());
|
||||
} else
|
||||
} else {
|
||||
ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace display
|
||||
} // namespace esphome
|
||||
|
@ -58,7 +58,11 @@ uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
|
||||
#else
|
||||
uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); }
|
||||
#endif
|
||||
uint32_t arch_get_cpu_freq_hz() { return rtc_clk_apb_freq_get(); }
|
||||
uint32_t arch_get_cpu_freq_hz() {
|
||||
rtc_cpu_freq_config_t config;
|
||||
rtc_clk_cpu_freq_get_config(&config);
|
||||
return config.freq_mhz * 1000000U;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
@ -112,7 +112,7 @@ void ESP32ImprovComponent::loop() {
|
||||
this->set_state_(improv::STATE_AUTHORIZED);
|
||||
} else
|
||||
#else
|
||||
this->set_state_(improv::STATE_AUTHORIZED);
|
||||
{ this->set_state_(improv::STATE_AUTHORIZED); }
|
||||
#endif
|
||||
{
|
||||
if (!this->check_identify_())
|
||||
|
@ -33,6 +33,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
memset(this->buf_, 0, buffer_size);
|
||||
|
||||
this->effect_data_ = allocator.allocate(this->num_leds_);
|
||||
if (this->effect_data_ == nullptr) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
@ -15,6 +16,9 @@ from esphome.const import (
|
||||
CONF_RMT_CHANNEL,
|
||||
CONF_RMT_SYMBOLS,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
@ -64,13 +68,53 @@ CONF_RESET_HIGH = "reset_high"
|
||||
CONF_RESET_LOW = "reset_low"
|
||||
|
||||
|
||||
class OptionalForIDF5(cv.SplitDefault):
|
||||
@property
|
||||
def default(self):
|
||||
if not esp32_rmt.use_new_rmt_driver():
|
||||
return cv.UNDEFINED
|
||||
return super().default
|
||||
|
||||
@default.setter
|
||||
def default(self, value):
|
||||
# Ignore default set from vol.Optional
|
||||
pass
|
||||
|
||||
|
||||
def only_with_new_rmt_driver(obj):
|
||||
if not esp32_rmt.use_new_rmt_driver():
|
||||
raise cv.Invalid(
|
||||
"This feature is only available for the IDF framework version 5."
|
||||
)
|
||||
return obj
|
||||
|
||||
|
||||
def not_with_new_rmt_driver(obj):
|
||||
if esp32_rmt.use_new_rmt_driver():
|
||||
raise cv.Invalid(
|
||||
"This feature is not available for the IDF framework version 5."
|
||||
)
|
||||
return obj
|
||||
|
||||
|
||||
def final_validation(config):
|
||||
if not esp32_rmt.use_new_rmt_driver() and CONF_RMT_CHANNEL not in config:
|
||||
raise cv.Invalid("rmt_channel is a required option.")
|
||||
if not esp32_rmt.use_new_rmt_driver():
|
||||
if CONF_RMT_CHANNEL not in config:
|
||||
if CORE.using_esp_idf:
|
||||
raise cv.Invalid(
|
||||
"rmt_channel is a required option for IDF version < 5."
|
||||
)
|
||||
raise cv.Invalid(
|
||||
"rmt_channel is a required option for the Arduino framework."
|
||||
)
|
||||
_LOGGER.warning(
|
||||
"RMT_LED_STRIP support for IDF version < 5 is deprecated and will be removed soon."
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validation
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
light.ADDRESSABLE_LIGHT_SCHEMA.extend(
|
||||
{
|
||||
@ -79,9 +123,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
|
||||
cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
|
||||
cv.Optional(CONF_RMT_CHANNEL): cv.All(
|
||||
cv.only_with_arduino, esp32_rmt.validate_rmt_channel(tx=True)
|
||||
not_with_new_rmt_driver, esp32_rmt.validate_rmt_channel(tx=True)
|
||||
),
|
||||
cv.SplitDefault(
|
||||
OptionalForIDF5(
|
||||
CONF_RMT_SYMBOLS,
|
||||
esp32_idf=64,
|
||||
esp32_s2_idf=64,
|
||||
@ -89,7 +133,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
esp32_c3_idf=48,
|
||||
esp32_c6_idf=48,
|
||||
esp32_h2_idf=48,
|
||||
): cv.All(cv.only_with_esp_idf, cv.int_range(min=2)),
|
||||
): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)),
|
||||
cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
|
||||
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
|
||||
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
|
||||
|
@ -94,11 +94,11 @@ CLK_MODES = {
|
||||
|
||||
MANUAL_IP_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_STATIC_IP): cv.ipv4,
|
||||
cv.Required(CONF_GATEWAY): cv.ipv4,
|
||||
cv.Required(CONF_SUBNET): cv.ipv4,
|
||||
cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4,
|
||||
cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4,
|
||||
cv.Required(CONF_STATIC_IP): cv.ipv4address,
|
||||
cv.Required(CONF_GATEWAY): cv.ipv4address,
|
||||
cv.Required(CONF_SUBNET): cv.ipv4address,
|
||||
cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4address,
|
||||
cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4address,
|
||||
}
|
||||
)
|
||||
|
||||
@ -255,11 +255,11 @@ FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
def manual_ip(config):
|
||||
return cg.StructInitializer(
|
||||
ManualIP,
|
||||
("static_ip", IPAddress(*config[CONF_STATIC_IP].args)),
|
||||
("gateway", IPAddress(*config[CONF_GATEWAY].args)),
|
||||
("subnet", IPAddress(*config[CONF_SUBNET].args)),
|
||||
("dns1", IPAddress(*config[CONF_DNS1].args)),
|
||||
("dns2", IPAddress(*config[CONF_DNS2].args)),
|
||||
("static_ip", IPAddress(str(config[CONF_STATIC_IP]))),
|
||||
("gateway", IPAddress(str(config[CONF_GATEWAY]))),
|
||||
("subnet", IPAddress(str(config[CONF_SUBNET]))),
|
||||
("dns1", IPAddress(str(config[CONF_DNS1]))),
|
||||
("dns2", IPAddress(str(config[CONF_DNS2]))),
|
||||
)
|
||||
|
||||
|
||||
|
@ -97,8 +97,9 @@ void GCJA5Component::parse_data_() {
|
||||
if (this->rx_message_[0] != 0x02 || this->rx_message_[31] != 0x03 || !this->calculate_checksum_()) {
|
||||
ESP_LOGVV(TAG, "Discarding bad packet - failed checks.");
|
||||
return;
|
||||
} else
|
||||
} else {
|
||||
ESP_LOGVV(TAG, "Good packet found.");
|
||||
}
|
||||
|
||||
this->have_good_data_ = true;
|
||||
uint8_t status = this->rx_message_[29];
|
||||
|
@ -342,9 +342,10 @@ bool HaierClimateBase::prepare_pending_action() {
|
||||
this->action_request_.reset();
|
||||
return false;
|
||||
}
|
||||
} else
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ClimateTraits HaierClimateBase::traits() { return traits_; }
|
||||
|
||||
|
@ -710,9 +710,10 @@ void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, boo
|
||||
alarm_code++;
|
||||
}
|
||||
active_alarms_[i] = packet[2 + i];
|
||||
} else
|
||||
} else {
|
||||
alarm_code += 8;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
float alarm_count = 0.0f;
|
||||
static uint8_t nibble_bits_count[] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
|
||||
|
@ -87,9 +87,10 @@ void HeatpumpIRClimate::setup() {
|
||||
this->publish_state();
|
||||
});
|
||||
this->current_temperature = this->sensor_->state;
|
||||
} else
|
||||
} else {
|
||||
this->current_temperature = NAN;
|
||||
}
|
||||
}
|
||||
|
||||
void HeatpumpIRClimate::transmit_state() {
|
||||
uint8_t power_mode_cmd;
|
||||
|
@ -25,6 +25,7 @@ void I2SAudioMicrophone::setup() {
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
if (this->pdm_) {
|
||||
if (this->parent_->get_port() != I2S_NUM_0) {
|
||||
ESP_LOGE(TAG, "PDM only works on I2S0!");
|
||||
@ -33,6 +34,7 @@ void I2SAudioMicrophone::setup() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void I2SAudioMicrophone::start() {
|
||||
if (this->is_failed())
|
||||
|
@ -1,9 +1,12 @@
|
||||
import logging
|
||||
|
||||
from esphome import core, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, spi
|
||||
from esphome.components.display import validate_rotation
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_COLOR_ORDER,
|
||||
CONF_COLOR_PALETTE,
|
||||
CONF_DC_PIN,
|
||||
@ -27,17 +30,12 @@ from esphome.const import (
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.final_validate import full_config
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
|
||||
def AUTO_LOAD():
|
||||
if CORE.is_esp32:
|
||||
return ["psram"]
|
||||
return []
|
||||
|
||||
|
||||
CODEOWNERS = ["@nielsnl68", "@clydebarrow"]
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ili9xxx_ns = cg.esphome_ns.namespace("ili9xxx")
|
||||
ILI9XXXDisplay = ili9xxx_ns.class_(
|
||||
@ -84,7 +82,7 @@ COLOR_ORDERS = {
|
||||
"BGR": ColorOrder.COLOR_ORDER_BGR,
|
||||
}
|
||||
|
||||
COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE")
|
||||
COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE", "8BIT", upper=True)
|
||||
|
||||
CONF_LED_PIN = "led_pin"
|
||||
CONF_COLOR_PALETTE_IMAGES = "color_palette_images"
|
||||
@ -195,11 +193,29 @@ CONFIG_SCHEMA = cv.All(
|
||||
_validate,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
|
||||
def final_validate(config):
|
||||
global_config = full_config.get()
|
||||
# Ideally would calculate buffer size here, but that info is not available on the Python side
|
||||
needs_buffer = (
|
||||
CONF_LAMBDA in config or CONF_PAGES in config or config[CONF_AUTO_CLEAR_ENABLED]
|
||||
)
|
||||
if (
|
||||
CORE.is_esp32
|
||||
and config[CONF_COLOR_PALETTE] == "NONE"
|
||||
and "psram" not in global_config
|
||||
and needs_buffer
|
||||
):
|
||||
LOGGER.info("Consider enabling PSRAM if available for the display buffer")
|
||||
|
||||
return spi.final_validate_device_schema(
|
||||
"ili9xxx", require_miso=False, require_mosi=True
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
rhs = MODELS[config[CONF_MODEL]].new()
|
||||
var = cg.Pvariable(config[CONF_ID], rhs)
|
||||
@ -283,6 +299,8 @@ async def to_code(config):
|
||||
palette = converted.getpalette()
|
||||
assert len(palette) == 256 * 3
|
||||
rhs = palette
|
||||
elif config[CONF_COLOR_PALETTE] == "8BIT":
|
||||
cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_8))
|
||||
else:
|
||||
cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_16))
|
||||
|
||||
|
@ -66,12 +66,9 @@ void ILI9XXXDisplay::setup() {
|
||||
void ILI9XXXDisplay::alloc_buffer_() {
|
||||
if (this->buffer_color_mode_ == BITS_16) {
|
||||
this->init_internal_(this->get_buffer_length_() * 2);
|
||||
if (this->buffer_ != nullptr) {
|
||||
return;
|
||||
}
|
||||
this->buffer_color_mode_ = BITS_8;
|
||||
}
|
||||
} else {
|
||||
this->init_internal_(this->get_buffer_length_());
|
||||
}
|
||||
if (this->buffer_ == nullptr) {
|
||||
this->mark_failed();
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
|
||||
protected:
|
||||
inline bool check_buffer_() {
|
||||
if (this->buffer_ == nullptr) {
|
||||
if (!this->is_failed())
|
||||
this->alloc_buffer_();
|
||||
return !this->is_failed();
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import puremagic
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
from esphome import core, external_files
|
||||
import esphome.codegen as cg
|
||||
@ -29,21 +29,259 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "image"
|
||||
DEPENDENCIES = ["display"]
|
||||
MULTI_CONF = True
|
||||
MULTI_CONF_NO_DEFAULT = True
|
||||
|
||||
image_ns = cg.esphome_ns.namespace("image")
|
||||
|
||||
ImageType = image_ns.enum("ImageType")
|
||||
|
||||
CONF_OPAQUE = "opaque"
|
||||
CONF_CHROMA_KEY = "chroma_key"
|
||||
CONF_ALPHA_CHANNEL = "alpha_channel"
|
||||
CONF_INVERT_ALPHA = "invert_alpha"
|
||||
|
||||
TRANSPARENCY_TYPES = (
|
||||
CONF_OPAQUE,
|
||||
CONF_CHROMA_KEY,
|
||||
CONF_ALPHA_CHANNEL,
|
||||
)
|
||||
|
||||
|
||||
def get_image_type_enum(type):
|
||||
return getattr(ImageType, f"IMAGE_TYPE_{type.upper()}")
|
||||
|
||||
|
||||
def get_transparency_enum(transparency):
|
||||
return getattr(TransparencyType, f"TRANSPARENCY_{transparency.upper()}")
|
||||
|
||||
|
||||
class ImageEncoder:
|
||||
"""
|
||||
Superclass of image type encoders
|
||||
"""
|
||||
|
||||
# Control which transparency options are available for a given type
|
||||
allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_OPAQUE}
|
||||
|
||||
# All imageencoder types are valid
|
||||
@staticmethod
|
||||
def validate(value):
|
||||
return value
|
||||
|
||||
def __init__(self, width, height, transparency, dither, invert_alpha):
|
||||
"""
|
||||
:param width: The image width in pixels
|
||||
:param height: The image height in pixels
|
||||
:param transparency: Transparency type
|
||||
:param dither: Dither method
|
||||
:param invert_alpha: True if the alpha channel should be inverted; for monochrome formats inverts the colours.
|
||||
"""
|
||||
self.transparency = transparency
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.data = [0 for _ in range(width * height)]
|
||||
self.dither = dither
|
||||
self.index = 0
|
||||
self.invert_alpha = invert_alpha
|
||||
self.path = ""
|
||||
|
||||
def convert(self, image, path):
|
||||
"""
|
||||
Convert the image format
|
||||
:param image: Input image
|
||||
:param path: Path to the image file
|
||||
:return: converted image
|
||||
"""
|
||||
return image
|
||||
|
||||
def encode(self, pixel):
|
||||
"""
|
||||
Encode a single pixel
|
||||
"""
|
||||
|
||||
def end_row(self):
|
||||
"""
|
||||
Marks the end of a pixel row
|
||||
:return:
|
||||
"""
|
||||
|
||||
|
||||
def is_alpha_only(image: Image):
|
||||
"""
|
||||
Check if an image (assumed to be RGBA) is only alpha
|
||||
"""
|
||||
# Any alpha data?
|
||||
if image.split()[-1].getextrema()[0] == 0xFF:
|
||||
return False
|
||||
return all(b.getextrema()[1] == 0 for b in image.split()[:-1])
|
||||
|
||||
|
||||
class ImageBinary(ImageEncoder):
|
||||
allow_config = {CONF_OPAQUE, CONF_INVERT_ALPHA, CONF_CHROMA_KEY}
|
||||
|
||||
def __init__(self, width, height, transparency, dither, invert_alpha):
|
||||
self.width8 = (width + 7) // 8
|
||||
super().__init__(self.width8, height, transparency, dither, invert_alpha)
|
||||
self.bitno = 0
|
||||
|
||||
def convert(self, image, path):
|
||||
if is_alpha_only(image):
|
||||
image = image.split()[-1]
|
||||
return image.convert("1", dither=self.dither)
|
||||
|
||||
def encode(self, pixel):
|
||||
if self.invert_alpha:
|
||||
pixel = not pixel
|
||||
if pixel:
|
||||
self.data[self.index] |= 0x80 >> (self.bitno % 8)
|
||||
self.bitno += 1
|
||||
if self.bitno == 8:
|
||||
self.bitno = 0
|
||||
self.index += 1
|
||||
|
||||
def end_row(self):
|
||||
"""
|
||||
Pad rows to a byte boundary
|
||||
"""
|
||||
if self.bitno != 0:
|
||||
self.bitno = 0
|
||||
self.index += 1
|
||||
|
||||
|
||||
class ImageGrayscale(ImageEncoder):
|
||||
allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_INVERT_ALPHA, CONF_OPAQUE}
|
||||
|
||||
def convert(self, image, path):
|
||||
if is_alpha_only(image):
|
||||
if self.transparency != CONF_ALPHA_CHANNEL:
|
||||
_LOGGER.warning(
|
||||
"Grayscale image %s is alpha only, but transparency is set to %s",
|
||||
path,
|
||||
self.transparency,
|
||||
)
|
||||
self.transparency = CONF_ALPHA_CHANNEL
|
||||
image = image.split()[-1]
|
||||
return image.convert("LA")
|
||||
|
||||
def encode(self, pixel):
|
||||
b, a = pixel
|
||||
if self.transparency == CONF_CHROMA_KEY:
|
||||
if b == 1:
|
||||
b = 0
|
||||
if a != 0xFF:
|
||||
b = 1
|
||||
if self.invert_alpha:
|
||||
b ^= 0xFF
|
||||
if self.transparency == CONF_ALPHA_CHANNEL:
|
||||
if a != 0xFF:
|
||||
b = a
|
||||
self.data[self.index] = b
|
||||
self.index += 1
|
||||
|
||||
|
||||
class ImageRGB565(ImageEncoder):
|
||||
def __init__(self, width, height, transparency, dither, invert_alpha):
|
||||
stride = 3 if transparency == CONF_ALPHA_CHANNEL else 2
|
||||
super().__init__(
|
||||
width * stride,
|
||||
height,
|
||||
transparency,
|
||||
dither,
|
||||
invert_alpha,
|
||||
)
|
||||
|
||||
def convert(self, image, path):
|
||||
return image.convert("RGBA")
|
||||
|
||||
def encode(self, pixel):
|
||||
r, g, b, a = pixel
|
||||
r = r >> 3
|
||||
g = g >> 2
|
||||
b = b >> 3
|
||||
if self.transparency == CONF_CHROMA_KEY:
|
||||
if r == 0 and g == 1 and b == 0:
|
||||
g = 0
|
||||
elif a < 128:
|
||||
r = 0
|
||||
g = 1
|
||||
b = 0
|
||||
rgb = (r << 11) | (g << 5) | b
|
||||
self.data[self.index] = rgb >> 8
|
||||
self.index += 1
|
||||
self.data[self.index] = rgb & 0xFF
|
||||
self.index += 1
|
||||
if self.transparency == CONF_ALPHA_CHANNEL:
|
||||
if self.invert_alpha:
|
||||
a ^= 0xFF
|
||||
self.data[self.index] = a
|
||||
self.index += 1
|
||||
|
||||
|
||||
class ImageRGB(ImageEncoder):
|
||||
def __init__(self, width, height, transparency, dither, invert_alpha):
|
||||
stride = 4 if transparency == CONF_ALPHA_CHANNEL else 3
|
||||
super().__init__(
|
||||
width * stride,
|
||||
height,
|
||||
transparency,
|
||||
dither,
|
||||
invert_alpha,
|
||||
)
|
||||
|
||||
def convert(self, image, path):
|
||||
return image.convert("RGBA")
|
||||
|
||||
def encode(self, pixel):
|
||||
r, g, b, a = pixel
|
||||
if self.transparency == CONF_CHROMA_KEY:
|
||||
if r == 0 and g == 1 and b == 0:
|
||||
g = 0
|
||||
elif a < 128:
|
||||
r = 0
|
||||
g = 1
|
||||
b = 0
|
||||
self.data[self.index] = r
|
||||
self.index += 1
|
||||
self.data[self.index] = g
|
||||
self.index += 1
|
||||
self.data[self.index] = b
|
||||
self.index += 1
|
||||
if self.transparency == CONF_ALPHA_CHANNEL:
|
||||
if self.invert_alpha:
|
||||
a ^= 0xFF
|
||||
self.data[self.index] = a
|
||||
self.index += 1
|
||||
|
||||
|
||||
class ReplaceWith:
|
||||
"""
|
||||
Placeholder class to provide feedback on deprecated features
|
||||
"""
|
||||
|
||||
allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_OPAQUE}
|
||||
|
||||
def __init__(self, replace_with):
|
||||
self.replace_with = replace_with
|
||||
|
||||
def validate(self, value):
|
||||
raise cv.Invalid(
|
||||
f"Image type {value} is removed; replace with {self.replace_with}"
|
||||
)
|
||||
|
||||
|
||||
IMAGE_TYPE = {
|
||||
"BINARY": ImageType.IMAGE_TYPE_BINARY,
|
||||
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY,
|
||||
"GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE,
|
||||
"RGB565": ImageType.IMAGE_TYPE_RGB565,
|
||||
"RGB24": ImageType.IMAGE_TYPE_RGB24,
|
||||
"RGBA": ImageType.IMAGE_TYPE_RGBA,
|
||||
"BINARY": ImageBinary,
|
||||
"GRAYSCALE": ImageGrayscale,
|
||||
"RGB565": ImageRGB565,
|
||||
"RGB": ImageRGB,
|
||||
"TRANSPARENT_BINARY": ReplaceWith(
|
||||
"'type: BINARY' and 'use_transparency: chroma_key'"
|
||||
),
|
||||
"RGB24": ReplaceWith("'type: RGB'"),
|
||||
"RGBA": ReplaceWith("'type: RGB' and 'use_transparency: alpha_channel'"),
|
||||
}
|
||||
|
||||
TransparencyType = image_ns.enum("TransparencyType")
|
||||
|
||||
CONF_USE_TRANSPARENCY = "use_transparency"
|
||||
|
||||
# If the MDI file cannot be downloaded within this time, abort.
|
||||
@ -53,17 +291,11 @@ SOURCE_LOCAL = "local"
|
||||
SOURCE_MDI = "mdi"
|
||||
SOURCE_WEB = "web"
|
||||
|
||||
|
||||
Image_ = image_ns.class_("Image")
|
||||
|
||||
|
||||
def _compute_local_icon_path(value: dict) -> Path:
|
||||
base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi"
|
||||
return base_dir / f"{value[CONF_ICON]}.svg"
|
||||
|
||||
|
||||
def compute_local_image_path(value: dict) -> Path:
|
||||
url = value[CONF_URL]
|
||||
def compute_local_image_path(value) -> Path:
|
||||
url = value[CONF_URL] if isinstance(value, dict) else value
|
||||
h = hashlib.new("sha256")
|
||||
h.update(url.encode())
|
||||
key = h.hexdigest()[:8]
|
||||
@ -71,30 +303,38 @@ def compute_local_image_path(value: dict) -> Path:
|
||||
return base_dir / key
|
||||
|
||||
|
||||
def download_mdi(value):
|
||||
validate_cairosvg_installed(value)
|
||||
def local_path(value):
|
||||
value = value[CONF_PATH] if isinstance(value, dict) else value
|
||||
return str(CORE.relative_config_path(value))
|
||||
|
||||
mdi_id = value[CONF_ICON]
|
||||
path = _compute_local_icon_path(value)
|
||||
|
||||
def download_file(url, path):
|
||||
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
|
||||
return str(path)
|
||||
|
||||
|
||||
def download_mdi(value):
|
||||
mdi_id = value[CONF_ICON] if isinstance(value, dict) else value
|
||||
base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi"
|
||||
path = base_dir / f"{mdi_id}.svg"
|
||||
|
||||
url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg"
|
||||
|
||||
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
|
||||
|
||||
return value
|
||||
return download_file(url, path)
|
||||
|
||||
|
||||
def download_image(value):
|
||||
url = value[CONF_URL]
|
||||
path = compute_local_image_path(value)
|
||||
|
||||
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
|
||||
|
||||
return value
|
||||
value = value[CONF_URL] if isinstance(value, dict) else value
|
||||
return download_file(value, compute_local_image_path(value))
|
||||
|
||||
|
||||
def validate_cairosvg_installed(value):
|
||||
"""Validate that cairosvg is installed"""
|
||||
def is_svg_file(file):
|
||||
if not file:
|
||||
return False
|
||||
with open(file, "rb") as f:
|
||||
return "<svg" in str(f.read(1024))
|
||||
|
||||
|
||||
def validate_cairosvg_installed():
|
||||
try:
|
||||
import cairosvg
|
||||
except ImportError as err:
|
||||
@ -110,73 +350,28 @@ def validate_cairosvg_installed(value):
|
||||
"(pip install -U cairosvg)"
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def validate_cross_dependencies(config):
|
||||
"""
|
||||
Validate fields whose possible values depend on other fields.
|
||||
For example, validate that explicitly transparent image types
|
||||
have "use_transparency" set to True.
|
||||
Also set the default value for those kind of dependent fields.
|
||||
"""
|
||||
is_mdi = CONF_FILE in config and config[CONF_FILE][CONF_SOURCE] == SOURCE_MDI
|
||||
if CONF_TYPE not in config:
|
||||
if is_mdi:
|
||||
config[CONF_TYPE] = "TRANSPARENT_BINARY"
|
||||
else:
|
||||
config[CONF_TYPE] = "BINARY"
|
||||
|
||||
image_type = config[CONF_TYPE]
|
||||
is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"]
|
||||
|
||||
# If the use_transparency option was not specified, set the default depending on the image type
|
||||
if CONF_USE_TRANSPARENCY not in config:
|
||||
config[CONF_USE_TRANSPARENCY] = is_transparent_type
|
||||
|
||||
if is_transparent_type and not config[CONF_USE_TRANSPARENCY]:
|
||||
raise cv.Invalid(f"Image type {image_type} must always be transparent.")
|
||||
|
||||
if is_mdi and config[CONF_TYPE] not in ["BINARY", "TRANSPARENT_BINARY"]:
|
||||
raise cv.Invalid("MDI images must be binary images.")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def validate_file_shorthand(value):
|
||||
value = cv.string_strict(value)
|
||||
if value.startswith("mdi:"):
|
||||
validate_cairosvg_installed(value)
|
||||
|
||||
match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value)
|
||||
if match is None:
|
||||
raise cv.Invalid("Could not parse mdi icon name.")
|
||||
icon = match.group(1)
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_SOURCE: SOURCE_MDI,
|
||||
CONF_ICON: icon,
|
||||
}
|
||||
)
|
||||
return download_mdi(icon)
|
||||
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_SOURCE: SOURCE_WEB,
|
||||
CONF_URL: value,
|
||||
}
|
||||
)
|
||||
return FILE_SCHEMA(
|
||||
{
|
||||
CONF_SOURCE: SOURCE_LOCAL,
|
||||
CONF_PATH: value,
|
||||
}
|
||||
)
|
||||
return download_image(value)
|
||||
|
||||
value = cv.file_(value)
|
||||
return local_path(value)
|
||||
|
||||
|
||||
LOCAL_SCHEMA = cv.Schema(
|
||||
LOCAL_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_PATH): cv.file_,
|
||||
}
|
||||
},
|
||||
local_path,
|
||||
)
|
||||
|
||||
MDI_SCHEMA = cv.All(
|
||||
@ -203,205 +398,202 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
|
||||
)
|
||||
|
||||
|
||||
def _file_schema(value):
|
||||
if isinstance(value, str):
|
||||
return validate_file_shorthand(value)
|
||||
return TYPED_FILE_SCHEMA(value)
|
||||
def validate_transparency(choices=TRANSPARENCY_TYPES):
|
||||
def validate(value):
|
||||
if isinstance(value, bool):
|
||||
value = str(value)
|
||||
return cv.one_of(*choices, lower=True)(value)
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
FILE_SCHEMA = cv.Schema(_file_schema)
|
||||
def validate_type(image_types):
|
||||
def validate(value):
|
||||
value = cv.one_of(*image_types, upper=True)(value)
|
||||
return IMAGE_TYPE[value].validate(value)
|
||||
|
||||
IMAGE_SCHEMA = cv.Schema(
|
||||
cv.All(
|
||||
return validate
|
||||
|
||||
|
||||
def validate_settings(value):
|
||||
type = value[CONF_TYPE]
|
||||
transparency = value[CONF_USE_TRANSPARENCY].lower()
|
||||
allow_config = IMAGE_TYPE[type].allow_config
|
||||
if transparency not in allow_config:
|
||||
raise cv.Invalid(
|
||||
f"Image format '{type}' cannot have transparency: {transparency}"
|
||||
)
|
||||
invert_alpha = value.get(CONF_INVERT_ALPHA, False)
|
||||
if (
|
||||
invert_alpha
|
||||
and transparency != CONF_ALPHA_CHANNEL
|
||||
and CONF_INVERT_ALPHA not in allow_config
|
||||
):
|
||||
raise cv.Invalid("No alpha channel to invert")
|
||||
if file := value.get(CONF_FILE):
|
||||
file = Path(file)
|
||||
if is_svg_file(file):
|
||||
validate_cairosvg_installed()
|
||||
else:
|
||||
try:
|
||||
Image.open(file)
|
||||
except UnidentifiedImageError as exc:
|
||||
raise cv.Invalid(f"File can't be opened as image: {file}") from exc
|
||||
return value
|
||||
|
||||
|
||||
BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(Image_),
|
||||
cv.Required(CONF_FILE): FILE_SCHEMA,
|
||||
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
|
||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||
# Not setting default here on purpose; the default depends on the source type
|
||||
# (file or mdi), and will be set in the "validate_cross_dependencies" validator.
|
||||
cv.Optional(CONF_TYPE): cv.enum(IMAGE_TYPE, upper=True),
|
||||
# Not setting default here on purpose; the default depends on the image type,
|
||||
# and thus will be set in the "validate_cross_dependencies" validator.
|
||||
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
|
||||
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
|
||||
"NONE", "FLOYDSTEINBERG", upper=True
|
||||
),
|
||||
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
},
|
||||
validate_cross_dependencies,
|
||||
)
|
||||
}
|
||||
).add_extra(validate_settings)
|
||||
|
||||
IMAGE_SCHEMA = BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||
cv.Optional(
|
||||
CONF_USE_TRANSPARENCY, default=CONF_OPAQUE
|
||||
): validate_transparency(),
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = IMAGE_SCHEMA
|
||||
|
||||
|
||||
def load_svg_image(file: bytes, resize: tuple[int, int]):
|
||||
# Local imports only to allow "validate_pillow_installed" to run *before* importing it
|
||||
# cairosvg is only needed in case of SVG images; adding it
|
||||
# to the top would force configurations not using SVG to also have it
|
||||
# installed for no reason.
|
||||
from cairosvg import svg2png
|
||||
from PIL import Image
|
||||
|
||||
if resize:
|
||||
req_width, req_height = resize
|
||||
svg_image = svg2png(
|
||||
file,
|
||||
output_width=req_width,
|
||||
output_height=req_height,
|
||||
def typed_image_schema(image_type):
|
||||
"""
|
||||
Construct a schema for a specific image type, allowing transparency options
|
||||
"""
|
||||
return cv.Any(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(t.lower()): cv.ensure_list(
|
||||
BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_USE_TRANSPARENCY, default=t
|
||||
): validate_transparency((t,)),
|
||||
cv.Optional(CONF_TYPE, default=image_type): validate_type(
|
||||
(image_type,)
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
for t in IMAGE_TYPE[image_type].allow_config.intersection(
|
||||
TRANSPARENCY_TYPES
|
||||
)
|
||||
}
|
||||
),
|
||||
# Allow a default configuration with no transparency preselected
|
||||
cv.ensure_list(
|
||||
BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_USE_TRANSPARENCY, default=CONF_OPAQUE
|
||||
): validate_transparency(),
|
||||
cv.Optional(CONF_TYPE, default=image_type): validate_type(
|
||||
(image_type,)
|
||||
),
|
||||
}
|
||||
)
|
||||
),
|
||||
)
|
||||
else:
|
||||
svg_image = svg2png(file)
|
||||
|
||||
return Image.open(io.BytesIO(svg_image))
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# Local import only to allow "validate_pillow_installed" to run *before* importing it
|
||||
from PIL import Image
|
||||
# The config schema can be a (possibly empty) single list of images,
|
||||
# or a dictionary of image types each with a list of images
|
||||
CONFIG_SCHEMA = cv.Any(
|
||||
cv.Schema({cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}),
|
||||
cv.ensure_list(IMAGE_SCHEMA),
|
||||
)
|
||||
|
||||
conf_file = config[CONF_FILE]
|
||||
|
||||
if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
|
||||
path = CORE.relative_config_path(conf_file[CONF_PATH])
|
||||
|
||||
elif conf_file[CONF_SOURCE] == SOURCE_MDI:
|
||||
path = _compute_local_icon_path(conf_file).as_posix()
|
||||
|
||||
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
|
||||
path = compute_local_image_path(conf_file).as_posix()
|
||||
|
||||
else:
|
||||
raise core.EsphomeError(f"Unknown image source: {conf_file[CONF_SOURCE]}")
|
||||
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
file_contents = f.read()
|
||||
except Exception as e:
|
||||
raise core.EsphomeError(f"Could not load image file {path}: {e}")
|
||||
|
||||
file_type = puremagic.from_string(file_contents, mime=True)
|
||||
async def write_image(config, all_frames=False):
|
||||
path = Path(config[CONF_FILE])
|
||||
if not path.is_file():
|
||||
raise core.EsphomeError(f"Could not load image file {path}")
|
||||
|
||||
resize = config.get(CONF_RESIZE)
|
||||
if "svg" in file_type:
|
||||
image = load_svg_image(file_contents, resize)
|
||||
else:
|
||||
image = Image.open(io.BytesIO(file_contents))
|
||||
if resize:
|
||||
image.thumbnail(resize)
|
||||
if is_svg_file(path):
|
||||
# Local import so use of non-SVG files needn't require cairosvg installed
|
||||
from cairosvg import svg2png
|
||||
|
||||
if not resize:
|
||||
resize = (None, None)
|
||||
with open(path, "rb") as file:
|
||||
image = svg2png(
|
||||
file_obj=file,
|
||||
output_width=resize[0],
|
||||
output_height=resize[1],
|
||||
)
|
||||
image = Image.open(io.BytesIO(image))
|
||||
width, height = image.size
|
||||
else:
|
||||
image = Image.open(path)
|
||||
width, height = image.size
|
||||
if resize:
|
||||
# Preserve aspect ratio
|
||||
new_width_max = min(width, resize[0])
|
||||
new_height_max = min(height, resize[1])
|
||||
ratio = min(new_width_max / width, new_height_max / height)
|
||||
width, height = int(width * ratio), int(height * ratio)
|
||||
|
||||
if CONF_RESIZE not in config and (width > 500 or height > 500):
|
||||
if not resize and (width > 500 or height > 500):
|
||||
_LOGGER.warning(
|
||||
'The image "%s" you requested is very big. Please consider'
|
||||
" using the resize parameter.",
|
||||
path,
|
||||
)
|
||||
|
||||
transparent = config[CONF_USE_TRANSPARENCY]
|
||||
|
||||
dither = (
|
||||
Image.Dither.NONE
|
||||
if config[CONF_DITHER] == "NONE"
|
||||
else Image.Dither.FLOYDSTEINBERG
|
||||
)
|
||||
if config[CONF_TYPE] == "GRAYSCALE":
|
||||
image = image.convert("LA", dither=dither)
|
||||
pixels = list(image.getdata())
|
||||
data = [0 for _ in range(height * width)]
|
||||
pos = 0
|
||||
for g, a in pixels:
|
||||
if transparent:
|
||||
if g == 1:
|
||||
g = 0
|
||||
if a < 0x80:
|
||||
g = 1
|
||||
type = config[CONF_TYPE]
|
||||
transparency = config[CONF_USE_TRANSPARENCY]
|
||||
invert_alpha = config[CONF_INVERT_ALPHA]
|
||||
frame_count = 1
|
||||
if all_frames:
|
||||
try:
|
||||
frame_count = image.n_frames
|
||||
except AttributeError:
|
||||
pass
|
||||
if frame_count <= 1:
|
||||
_LOGGER.warning("Image file %s has no animation frames", path)
|
||||
|
||||
data[pos] = g
|
||||
pos += 1
|
||||
total_rows = height * frame_count
|
||||
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
|
||||
for frame_index in range(frame_count):
|
||||
image.seek(frame_index)
|
||||
pixels = encoder.convert(image.resize((width, height)), path).getdata()
|
||||
for row in range(height):
|
||||
for col in range(width):
|
||||
encoder.encode(pixels[row * width + col])
|
||||
encoder.end_row()
|
||||
|
||||
elif config[CONF_TYPE] == "RGBA":
|
||||
image = image.convert("RGBA")
|
||||
pixels = list(image.getdata())
|
||||
data = [0 for _ in range(height * width * 4)]
|
||||
pos = 0
|
||||
for r, g, b, a in pixels:
|
||||
data[pos] = r
|
||||
pos += 1
|
||||
data[pos] = g
|
||||
pos += 1
|
||||
data[pos] = b
|
||||
pos += 1
|
||||
data[pos] = a
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] == "RGB24":
|
||||
image = image.convert("RGBA")
|
||||
pixels = list(image.getdata())
|
||||
data = [0 for _ in range(height * width * 3)]
|
||||
pos = 0
|
||||
for r, g, b, a in pixels:
|
||||
if transparent:
|
||||
if r == 0 and g == 0 and b == 1:
|
||||
b = 0
|
||||
if a < 0x80:
|
||||
r = 0
|
||||
g = 0
|
||||
b = 1
|
||||
|
||||
data[pos] = r
|
||||
pos += 1
|
||||
data[pos] = g
|
||||
pos += 1
|
||||
data[pos] = b
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] in ["RGB565"]:
|
||||
image = image.convert("RGBA")
|
||||
pixels = list(image.getdata())
|
||||
bytes_per_pixel = 3 if transparent else 2
|
||||
data = [0 for _ in range(height * width * bytes_per_pixel)]
|
||||
pos = 0
|
||||
for r, g, b, a in pixels:
|
||||
R = r >> 3
|
||||
G = g >> 2
|
||||
B = b >> 3
|
||||
rgb = (R << 11) | (G << 5) | B
|
||||
data[pos] = rgb >> 8
|
||||
pos += 1
|
||||
data[pos] = rgb & 0xFF
|
||||
pos += 1
|
||||
if transparent:
|
||||
data[pos] = a
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
|
||||
if transparent:
|
||||
alpha = image.split()[-1]
|
||||
has_alpha = alpha.getextrema()[0] < 0xFF
|
||||
_LOGGER.debug("%s Has alpha: %s", config[CONF_ID], has_alpha)
|
||||
image = image.convert("1", dither=dither)
|
||||
width8 = ((width + 7) // 8) * 8
|
||||
data = [0 for _ in range(height * width8 // 8)]
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
if transparent and has_alpha:
|
||||
a = alpha.getpixel((x, y))
|
||||
if not a:
|
||||
continue
|
||||
elif image.getpixel((x, y)):
|
||||
continue
|
||||
pos = x + y * width8
|
||||
data[pos // 8] |= 0x80 >> (pos % 8)
|
||||
else:
|
||||
raise core.EsphomeError(
|
||||
f"Image f{config[CONF_ID]} has an unsupported type: {config[CONF_TYPE]}."
|
||||
)
|
||||
|
||||
rhs = [HexInt(x) for x in data]
|
||||
rhs = [HexInt(x) for x in encoder.data]
|
||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]]
|
||||
image_type = get_image_type_enum(type)
|
||||
trans_value = get_transparency_enum(encoder.transparency)
|
||||
|
||||
return prog_arr, width, height, image_type, trans_value, frame_count
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
if isinstance(config, list):
|
||||
for entry in config:
|
||||
await to_code(entry)
|
||||
elif CONF_ID not in config:
|
||||
for entry in config.values():
|
||||
await to_code(entry)
|
||||
else:
|
||||
prog_arr, width, height, image_type, trans_value, _ = await write_image(config)
|
||||
cg.new_Pvariable(
|
||||
config[CONF_ID], prog_arr, width, height, image_type, trans_value
|
||||
)
|
||||
cg.add(var.set_transparency(transparent))
|
||||
|
@ -12,7 +12,7 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
if (this->get_binary_pixel_(img_x, img_y)) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_on);
|
||||
} else if (!this->transparent_) {
|
||||
} else if (!this->transparency_) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_off);
|
||||
}
|
||||
}
|
||||
@ -22,10 +22,27 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
for (int img_x = 0; img_x < width_; img_x++) {
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
auto color = this->get_grayscale_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
const uint32_t pos = (img_x + img_y * this->width_);
|
||||
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
|
||||
Color color = Color(gray, gray, gray, 0xFF);
|
||||
switch (this->transparency_) {
|
||||
case TRANSPARENCY_CHROMA_KEY:
|
||||
if (gray == 1) {
|
||||
continue; // skip drawing
|
||||
}
|
||||
break;
|
||||
case TRANSPARENCY_ALPHA_CHANNEL: {
|
||||
auto on = (float) gray / 255.0f;
|
||||
auto off = 1.0f - on;
|
||||
// blend color_on and color_off
|
||||
color = Color(color_on.r * on + color_off.r * off, color_on.g * on + color_off.g * off,
|
||||
color_on.b * on + color_off.b * off, 0xFF);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -39,20 +56,10 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IMAGE_TYPE_RGB24:
|
||||
case IMAGE_TYPE_RGB:
|
||||
for (int img_x = 0; img_x < width_; img_x++) {
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
auto color = this->get_rgb24_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IMAGE_TYPE_RGBA:
|
||||
for (int img_x = 0; img_x < width_; img_x++) {
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
auto color = this->get_rgba_pixel_(img_x, img_y);
|
||||
auto color = this->get_rgb_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
@ -61,20 +68,20 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
|
||||
break;
|
||||
}
|
||||
}
|
||||
Color Image::get_pixel(int x, int y, Color color_on, Color color_off) const {
|
||||
Color Image::get_pixel(int x, int y, const Color color_on, const Color color_off) const {
|
||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||
return color_off;
|
||||
switch (this->type_) {
|
||||
case IMAGE_TYPE_BINARY:
|
||||
return this->get_binary_pixel_(x, y) ? color_on : color_off;
|
||||
if (this->get_binary_pixel_(x, y))
|
||||
return color_on;
|
||||
return color_off;
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
return this->get_grayscale_pixel_(x, y);
|
||||
case IMAGE_TYPE_RGB565:
|
||||
return this->get_rgb565_pixel_(x, y);
|
||||
case IMAGE_TYPE_RGB24:
|
||||
return this->get_rgb24_pixel_(x, y);
|
||||
case IMAGE_TYPE_RGBA:
|
||||
return this->get_rgba_pixel_(x, y);
|
||||
case IMAGE_TYPE_RGB:
|
||||
return this->get_rgb_pixel_(x, y);
|
||||
default:
|
||||
return color_off;
|
||||
}
|
||||
@ -98,23 +105,40 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
|
||||
this->dsc_.header.cf = LV_IMG_CF_ALPHA_8BIT;
|
||||
break;
|
||||
|
||||
case IMAGE_TYPE_RGB24:
|
||||
this->dsc_.header.cf = LV_IMG_CF_RGB888;
|
||||
case IMAGE_TYPE_RGB:
|
||||
#if LV_COLOR_DEPTH == 32
|
||||
switch (this->transparent_) {
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
|
||||
break;
|
||||
case TRANSPARENCY_CHROMA_KEY:
|
||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED;
|
||||
break;
|
||||
default:
|
||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR;
|
||||
break;
|
||||
}
|
||||
#else
|
||||
this->dsc_.header.cf =
|
||||
this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGBA8888 : LV_IMG_CF_RGB888;
|
||||
#endif
|
||||
break;
|
||||
|
||||
case IMAGE_TYPE_RGB565:
|
||||
#if LV_COLOR_DEPTH == 16
|
||||
this->dsc_.header.cf = this->has_transparency() ? LV_IMG_CF_TRUE_COLOR_ALPHA : LV_IMG_CF_TRUE_COLOR;
|
||||
#else
|
||||
this->dsc_.header.cf = LV_IMG_CF_RGB565;
|
||||
#endif
|
||||
break;
|
||||
|
||||
case IMAGE_TYPE_RGBA:
|
||||
#if LV_COLOR_DEPTH == 32
|
||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR;
|
||||
#else
|
||||
switch (this->transparency_) {
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
|
||||
break;
|
||||
case TRANSPARENCY_CHROMA_KEY:
|
||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED;
|
||||
break;
|
||||
default:
|
||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR;
|
||||
break;
|
||||
}
|
||||
#else
|
||||
this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565;
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
@ -128,51 +152,81 @@ bool Image::get_binary_pixel_(int x, int y) const {
|
||||
const uint32_t pos = x + y * width_8;
|
||||
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
|
||||
}
|
||||
Color Image::get_rgba_pixel_(int x, int y) const {
|
||||
const uint32_t pos = (x + y * this->width_) * 4;
|
||||
return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
|
||||
progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3));
|
||||
}
|
||||
Color Image::get_rgb24_pixel_(int x, int y) const {
|
||||
const uint32_t pos = (x + y * this->width_) * 3;
|
||||
Color Image::get_rgb_pixel_(int x, int y) const {
|
||||
const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8;
|
||||
Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
|
||||
progmem_read_byte(this->data_start_ + pos + 2));
|
||||
if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) {
|
||||
// (0, 0, 1) has been defined as transparent color for non-alpha images.
|
||||
// putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if)
|
||||
progmem_read_byte(this->data_start_ + pos + 2), 0xFF);
|
||||
|
||||
switch (this->transparency_) {
|
||||
case TRANSPARENCY_CHROMA_KEY:
|
||||
if (color.g == 1 && color.r == 0 && color.b == 0) {
|
||||
// (0, 1, 0) has been defined as transparent color for non-alpha images.
|
||||
color.w = 0;
|
||||
} else {
|
||||
color.w = 0xFF;
|
||||
}
|
||||
break;
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
color.w = progmem_read_byte(this->data_start_ + (pos + 3));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
Color Image::get_rgb565_pixel_(int x, int y) const {
|
||||
const uint8_t *pos = this->data_start_;
|
||||
if (this->transparent_) {
|
||||
pos += (x + y * this->width_) * 3;
|
||||
} else {
|
||||
pos += (x + y * this->width_) * 2;
|
||||
}
|
||||
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
|
||||
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
|
||||
auto r = (rgb565 & 0xF800) >> 11;
|
||||
auto g = (rgb565 & 0x07E0) >> 5;
|
||||
auto b = rgb565 & 0x001F;
|
||||
auto a = this->transparent_ ? progmem_read_byte(pos + 2) : 0xFF;
|
||||
Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a);
|
||||
return color;
|
||||
auto a = 0xFF;
|
||||
switch (this->transparency_) {
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
a = progmem_read_byte(pos + 2);
|
||||
break;
|
||||
case TRANSPARENCY_CHROMA_KEY:
|
||||
if (rgb565 == 0x0020)
|
||||
a = 0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a);
|
||||
}
|
||||
|
||||
Color Image::get_grayscale_pixel_(int x, int y) const {
|
||||
const uint32_t pos = (x + y * this->width_);
|
||||
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
|
||||
uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF;
|
||||
return Color(gray, gray, gray, alpha);
|
||||
switch (this->transparency_) {
|
||||
case TRANSPARENCY_CHROMA_KEY:
|
||||
if (gray == 1)
|
||||
return Color(0, 0, 0, 0);
|
||||
return Color(gray, gray, gray, 0xFF);
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
return Color(0, 0, 0, gray);
|
||||
default:
|
||||
return Color(gray, gray, gray, 0xFF);
|
||||
}
|
||||
}
|
||||
int Image::get_width() const { return this->width_; }
|
||||
int Image::get_height() const { return this->height_; }
|
||||
ImageType Image::get_type() const { return this->type_; }
|
||||
Image::Image(const uint8_t *data_start, int width, int height, ImageType type)
|
||||
: width_(width), height_(height), type_(type), data_start_(data_start) {}
|
||||
Image::Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency)
|
||||
: width_(width), height_(height), type_(type), data_start_(data_start), transparency_(transparency) {
|
||||
switch (this->type_) {
|
||||
case IMAGE_TYPE_BINARY:
|
||||
this->bpp_ = 1;
|
||||
break;
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
this->bpp_ = 8;
|
||||
break;
|
||||
case IMAGE_TYPE_RGB565:
|
||||
this->bpp_ = transparency == TRANSPARENCY_ALPHA_CHANNEL ? 24 : 16;
|
||||
break;
|
||||
case IMAGE_TYPE_RGB:
|
||||
this->bpp_ = this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? 32 : 24;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace image
|
||||
} // namespace esphome
|
||||
|
@ -12,51 +12,40 @@ namespace image {
|
||||
enum ImageType {
|
||||
IMAGE_TYPE_BINARY = 0,
|
||||
IMAGE_TYPE_GRAYSCALE = 1,
|
||||
IMAGE_TYPE_RGB24 = 2,
|
||||
IMAGE_TYPE_RGB = 2,
|
||||
IMAGE_TYPE_RGB565 = 3,
|
||||
IMAGE_TYPE_RGBA = 4,
|
||||
};
|
||||
|
||||
enum Transparency {
|
||||
TRANSPARENCY_OPAQUE = 0,
|
||||
TRANSPARENCY_CHROMA_KEY = 1,
|
||||
TRANSPARENCY_ALPHA_CHANNEL = 2,
|
||||
};
|
||||
|
||||
class Image : public display::BaseImage {
|
||||
public:
|
||||
Image(const uint8_t *data_start, int width, int height, ImageType type);
|
||||
Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency);
|
||||
Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const;
|
||||
int get_width() const override;
|
||||
int get_height() const override;
|
||||
const uint8_t *get_data_start() const { return this->data_start_; }
|
||||
ImageType get_type() const;
|
||||
|
||||
int get_bpp() const {
|
||||
switch (this->type_) {
|
||||
case IMAGE_TYPE_BINARY:
|
||||
return 1;
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
return 8;
|
||||
case IMAGE_TYPE_RGB565:
|
||||
return this->transparent_ ? 24 : 16;
|
||||
case IMAGE_TYPE_RGB24:
|
||||
return 24;
|
||||
case IMAGE_TYPE_RGBA:
|
||||
return 32;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int get_bpp() const { return this->bpp_; }
|
||||
|
||||
/// Return the stride of the image in bytes, that is, the distance in bytes
|
||||
/// between two consecutive rows of pixels.
|
||||
uint32_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; }
|
||||
size_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; }
|
||||
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
||||
|
||||
void set_transparency(bool transparent) { transparent_ = transparent; }
|
||||
bool has_transparency() const { return transparent_; }
|
||||
bool has_transparency() const { return this->transparency_ != TRANSPARENCY_OPAQUE; }
|
||||
|
||||
#ifdef USE_LVGL
|
||||
lv_img_dsc_t *get_lv_img_dsc();
|
||||
#endif
|
||||
protected:
|
||||
bool get_binary_pixel_(int x, int y) const;
|
||||
Color get_rgb24_pixel_(int x, int y) const;
|
||||
Color get_rgba_pixel_(int x, int y) const;
|
||||
Color get_rgb_pixel_(int x, int y) const;
|
||||
Color get_rgb565_pixel_(int x, int y) const;
|
||||
Color get_grayscale_pixel_(int x, int y) const;
|
||||
|
||||
@ -64,7 +53,9 @@ class Image : public display::BaseImage {
|
||||
int height_;
|
||||
ImageType type_;
|
||||
const uint8_t *data_start_;
|
||||
bool transparent_;
|
||||
Transparency transparency_;
|
||||
size_t bpp_{};
|
||||
size_t stride_{};
|
||||
#ifdef USE_LVGL
|
||||
lv_img_dsc_t dsc_{};
|
||||
#endif
|
||||
|
@ -17,11 +17,11 @@ std::string build_json(const json_build_t &f) {
|
||||
auto free_heap = ALLOCATOR.get_max_free_block_size();
|
||||
size_t request_size = std::min(free_heap, (size_t) 512);
|
||||
while (true) {
|
||||
ESP_LOGV(TAG, "Attempting to allocate %u bytes for JSON serialization", request_size);
|
||||
ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size);
|
||||
DynamicJsonDocument json_document(request_size);
|
||||
if (json_document.capacity() == 0) {
|
||||
ESP_LOGE(TAG,
|
||||
"Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes",
|
||||
"Could not allocate memory for JSON document! Requested %zu bytes, largest free heap block: %zu bytes",
|
||||
request_size, free_heap);
|
||||
return "{}";
|
||||
}
|
||||
@ -29,7 +29,7 @@ std::string build_json(const json_build_t &f) {
|
||||
f(root);
|
||||
if (json_document.overflowed()) {
|
||||
if (request_size == free_heap) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %u bytes",
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %zu bytes",
|
||||
free_heap);
|
||||
return "{}";
|
||||
}
|
||||
@ -37,7 +37,7 @@ std::string build_json(const json_build_t &f) {
|
||||
continue;
|
||||
}
|
||||
json_document.shrinkToFit();
|
||||
ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity());
|
||||
ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity());
|
||||
std::string output;
|
||||
serializeJson(json_document, output);
|
||||
return output;
|
||||
|
@ -147,7 +147,7 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", to_save.key.c_str());
|
||||
return true;
|
||||
}
|
||||
stored_data.data.reserve(kv.value_len);
|
||||
stored_data.data.resize(kv.value_len);
|
||||
fdb_blob_make(&blob, stored_data.data.data(), kv.value_len);
|
||||
size_t actual_len = fdb_kv_get_blob(db, to_save.key.c_str(), &blob);
|
||||
if (actual_len != kv.value_len) {
|
||||
|
@ -36,7 +36,7 @@ inline static uint8_t to_uint8_scale(float x) { return static_cast<uint8_t>(roun
|
||||
* range as set in the traits, so the output needs to do this.
|
||||
*
|
||||
* For COLD_WARM_WHITE capability:
|
||||
* - cold_white, warm_white: The brightness of the cald and warm white channels of the light.
|
||||
* - cold_white, warm_white: The brightness of the light's cold and warm white channels.
|
||||
*
|
||||
* All values (except color temperature) are represented using floats in the range 0.0 (off) to 1.0 (on), and are
|
||||
* automatically clamped to this range. Properties not used in the current color mode can still have (invalid) values
|
||||
|
@ -197,11 +197,11 @@ def final_validation(configs):
|
||||
for display_id in config[df.CONF_DISPLAYS]:
|
||||
path = global_config.get_path_for_id(display_id)[:-1]
|
||||
display = global_config.get_config_for_path(path)
|
||||
if CONF_LAMBDA in display:
|
||||
if CONF_LAMBDA in display or CONF_PAGES in display:
|
||||
raise cv.Invalid(
|
||||
"Using lambda: in display config not compatible with LVGL"
|
||||
"Using lambda: or pages: in display config is not compatible with LVGL"
|
||||
)
|
||||
if display[CONF_AUTO_CLEAR_ENABLED]:
|
||||
if display.get(CONF_AUTO_CLEAR_ENABLED) is True:
|
||||
raise cv.Invalid(
|
||||
"Using auto_clear_enabled: true in display config not compatible with LVGL"
|
||||
)
|
||||
|
@ -4,24 +4,26 @@ from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT
|
||||
from esphome.cpp_generator import get_variable
|
||||
from esphome.cpp_generator import TemplateArguments, get_variable
|
||||
from esphome.cpp_types import nullptr
|
||||
|
||||
from .defines import (
|
||||
CONF_DISP_BG_COLOR,
|
||||
CONF_DISP_BG_IMAGE,
|
||||
CONF_DISP_BG_OPA,
|
||||
CONF_EDITING,
|
||||
CONF_FREEZE,
|
||||
CONF_LVGL_ID,
|
||||
CONF_SHOW_SNOW,
|
||||
literal,
|
||||
)
|
||||
from .lv_validation import lv_bool, lv_color, lv_image
|
||||
from .lv_validation import lv_bool, lv_color, lv_image, opacity
|
||||
from .lvcode import (
|
||||
LVGL_COMP_ARG,
|
||||
UPDATE_EVENT,
|
||||
LambdaContext,
|
||||
LocalVariable,
|
||||
LvglComponent,
|
||||
ReturnStatement,
|
||||
add_line_marks,
|
||||
lv,
|
||||
@ -92,7 +94,11 @@ async def lvgl_is_paused(config, condition_id, template_arg, args):
|
||||
lvgl = config[CONF_LVGL_ID]
|
||||
async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context:
|
||||
lv_add(ReturnStatement(lvgl_comp.is_paused()))
|
||||
var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda())
|
||||
var = cg.new_Pvariable(
|
||||
condition_id,
|
||||
TemplateArguments(LvglComponent, *template_arg),
|
||||
await context.get_lambda(),
|
||||
)
|
||||
await cg.register_parented(var, lvgl)
|
||||
return var
|
||||
|
||||
@ -113,19 +119,32 @@ async def lvgl_is_idle(config, condition_id, template_arg, args):
|
||||
timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32)
|
||||
async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context:
|
||||
lv_add(ReturnStatement(lvgl_comp.is_idle(timeout)))
|
||||
var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda())
|
||||
var = cg.new_Pvariable(
|
||||
condition_id,
|
||||
TemplateArguments(LvglComponent, *template_arg),
|
||||
await context.get_lambda(),
|
||||
)
|
||||
await cg.register_parented(var, lvgl)
|
||||
return var
|
||||
|
||||
|
||||
async def disp_update(disp, config: dict):
|
||||
if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config:
|
||||
if (
|
||||
CONF_DISP_BG_COLOR not in config
|
||||
and CONF_DISP_BG_IMAGE not in config
|
||||
and CONF_DISP_BG_OPA not in config
|
||||
):
|
||||
return
|
||||
with LocalVariable("lv_disp_tmp", lv_disp_t, disp) as disp_temp:
|
||||
if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None:
|
||||
lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color))
|
||||
if bg_image := config.get(CONF_DISP_BG_IMAGE):
|
||||
if bg_image == "none":
|
||||
lv.disp_set_bg_image(disp_temp, static_cast("void *", "nullptr"))
|
||||
else:
|
||||
lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image))
|
||||
if (bg_opa := config.get(CONF_DISP_BG_OPA)) is not None:
|
||||
lv.disp_set_bg_opa(disp_temp, await opacity.process(bg_opa))
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
|
@ -215,7 +215,7 @@ LV_LONG_MODES = LvConstant(
|
||||
)
|
||||
|
||||
STATES = (
|
||||
"default",
|
||||
# default state not included here
|
||||
"checked",
|
||||
"focused",
|
||||
"focus_key",
|
||||
@ -403,6 +403,7 @@ CONF_COLUMN = "column"
|
||||
CONF_DIGITS = "digits"
|
||||
CONF_DISP_BG_COLOR = "disp_bg_color"
|
||||
CONF_DISP_BG_IMAGE = "disp_bg_image"
|
||||
CONF_DISP_BG_OPA = "disp_bg_opa"
|
||||
CONF_BODY = "body"
|
||||
CONF_BUTTONS = "buttons"
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
|
@ -119,6 +119,7 @@ void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_ev
|
||||
}
|
||||
void LvglComponent::add_page(LvPageType *page) {
|
||||
this->pages_.push_back(page);
|
||||
page->set_parent(this);
|
||||
page->setup(this->pages_.size() - 1);
|
||||
}
|
||||
void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) {
|
||||
@ -143,6 +144,8 @@ void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) {
|
||||
} while (this->pages_[this->current_page_]->skip); // skip empty pages()
|
||||
this->show_page(this->current_page_, anim, time);
|
||||
}
|
||||
size_t LvglComponent::get_current_page() const { return this->current_page_; }
|
||||
bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; }
|
||||
void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
|
||||
auto width = lv_area_get_width(area);
|
||||
auto height = lv_area_get_height(area);
|
||||
@ -498,9 +501,7 @@ size_t lv_millis(void) { return esphome::millis(); }
|
||||
void *lv_custom_mem_alloc(size_t size) {
|
||||
auto *ptr = malloc(size); // NOLINT
|
||||
if (ptr == nullptr) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
|
||||
esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
|
||||
#endif
|
||||
ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
@ -517,30 +518,22 @@ void *lv_custom_mem_alloc(size_t size) {
|
||||
ptr = heap_caps_malloc(size, cap_bits);
|
||||
}
|
||||
if (ptr == nullptr) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
|
||||
esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
|
||||
#endif
|
||||
ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
|
||||
return nullptr;
|
||||
}
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
esphome::ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr);
|
||||
#endif
|
||||
ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void lv_custom_mem_free(void *ptr) {
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
esphome::ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr);
|
||||
#endif
|
||||
ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr);
|
||||
if (ptr == nullptr)
|
||||
return;
|
||||
heap_caps_free(ptr);
|
||||
}
|
||||
|
||||
void *lv_custom_mem_realloc(void *ptr, size_t size) {
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
esphome::ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size);
|
||||
#endif
|
||||
ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size);
|
||||
return heap_caps_realloc(ptr, size, cap_bits);
|
||||
}
|
||||
#endif
|
||||
|
@ -59,6 +59,16 @@ inline void lv_img_set_src(lv_obj_t *obj, esphome::image::Image *image) {
|
||||
inline void lv_disp_set_bg_image(lv_disp_t *disp, esphome::image::Image *image) {
|
||||
lv_disp_set_bg_image(disp, image->get_lv_img_dsc());
|
||||
}
|
||||
|
||||
inline void lv_obj_set_style_bg_img_src(lv_obj_t *obj, esphome::image::Image *image, lv_style_selector_t selector) {
|
||||
lv_obj_set_style_bg_img_src(obj, image->get_lv_img_dsc(), selector);
|
||||
}
|
||||
#ifdef USE_LVGL_METER
|
||||
inline lv_meter_indicator_t *lv_meter_add_needle_img(lv_obj_t *obj, lv_meter_scale_t *scale, esphome::image::Image *src,
|
||||
lv_coord_t pivot_x, lv_coord_t pivot_y) {
|
||||
return lv_meter_add_needle_img(obj, scale, src->get_lv_img_dsc(), pivot_x, pivot_y);
|
||||
}
|
||||
#endif // USE_LVGL_METER
|
||||
#endif // USE_LVGL_IMAGE
|
||||
#ifdef USE_LVGL_ANIMIMG
|
||||
inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) {
|
||||
@ -84,7 +94,9 @@ class LvCompound {
|
||||
lv_obj_t *obj{};
|
||||
};
|
||||
|
||||
class LvPageType {
|
||||
class LvglComponent;
|
||||
|
||||
class LvPageType : public Parented<LvglComponent> {
|
||||
public:
|
||||
LvPageType(bool skip) : skip(skip) {}
|
||||
|
||||
@ -92,6 +104,9 @@ class LvPageType {
|
||||
this->index = index;
|
||||
this->obj = lv_obj_create(nullptr);
|
||||
}
|
||||
|
||||
bool is_showing() const;
|
||||
|
||||
lv_obj_t *obj{};
|
||||
size_t index{};
|
||||
bool skip;
|
||||
@ -178,6 +193,7 @@ class LvglComponent : public PollingComponent {
|
||||
void show_next_page(lv_scr_load_anim_t anim, uint32_t time);
|
||||
void show_prev_page(lv_scr_load_anim_t anim, uint32_t time);
|
||||
void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; }
|
||||
size_t get_current_page() const;
|
||||
void set_focus_mark(lv_group_t *group) { this->focus_marks_[group] = lv_group_get_focused(group); }
|
||||
void restore_focus_mark(lv_group_t *group) {
|
||||
auto *mark = this->focus_marks_[group];
|
||||
@ -241,14 +257,13 @@ template<typename... Ts> class LvglAction : public Action<Ts...>, public Parente
|
||||
std::function<void(LvglComponent *)> action_{};
|
||||
};
|
||||
|
||||
template<typename... Ts> class LvglCondition : public Condition<Ts...>, public Parented<LvglComponent> {
|
||||
template<typename Tc, typename... Ts> class LvglCondition : public Condition<Ts...>, public Parented<Tc> {
|
||||
public:
|
||||
LvglCondition(std::function<bool(LvglComponent *)> &&condition_lambda)
|
||||
: condition_lambda_(std::move(condition_lambda)) {}
|
||||
LvglCondition(std::function<bool(Tc *)> &&condition_lambda) : condition_lambda_(std::move(condition_lambda)) {}
|
||||
bool check(Ts... x) override { return this->condition_lambda_(this->parent_); }
|
||||
|
||||
protected:
|
||||
std::function<bool(LvglComponent *)> condition_lambda_{};
|
||||
std::function<bool(Tc *)> condition_lambda_{};
|
||||
};
|
||||
|
||||
#ifdef USE_LVGL_TOUCHSCREEN
|
||||
|
@ -19,7 +19,7 @@ from esphome.schema_extractors import SCHEMA_EXTRACT
|
||||
from . import defines as df, lv_validation as lvalid
|
||||
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
|
||||
from .helpers import add_lv_use, requires_component, validate_printf
|
||||
from .lv_validation import lv_color, lv_font, lv_gradient, lv_image
|
||||
from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity
|
||||
from .lvcode import LvglComponent, lv_event_t_ptr
|
||||
from .types import (
|
||||
LVEncoderListener,
|
||||
@ -344,8 +344,11 @@ FLEX_OBJ_SCHEMA = {
|
||||
|
||||
DISP_BG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(df.CONF_DISP_BG_IMAGE): lv_image,
|
||||
cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any(
|
||||
cv.one_of("none", lower=True), lv_image
|
||||
),
|
||||
cv.Optional(df.CONF_DISP_BG_COLOR): lv_color,
|
||||
cv.Optional(df.CONF_DISP_BG_OPA): opacity,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -27,7 +27,7 @@ from ..defines import (
|
||||
CONF_START_VALUE,
|
||||
CONF_TICKS,
|
||||
)
|
||||
from ..helpers import add_lv_use
|
||||
from ..helpers import add_lv_use, lvgl_components_required
|
||||
from ..lv_validation import (
|
||||
angle,
|
||||
get_end_value,
|
||||
@ -182,6 +182,7 @@ class MeterType(WidgetType):
|
||||
async def to_code(self, w: Widget, config):
|
||||
"""For a meter object, create and set parameters"""
|
||||
|
||||
lvgl_components_required.add(CONF_METER)
|
||||
var = w.obj
|
||||
for scale_conf in config.get(CONF_SCALES, ()):
|
||||
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
|
||||
|
@ -2,6 +2,7 @@ from esphome import automation, codegen as cg
|
||||
from esphome.automation import Trigger
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME, CONF_TRIGGER_ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArguments
|
||||
|
||||
from ..defines import (
|
||||
CONF_ANIMATION,
|
||||
@ -17,18 +18,28 @@ from ..lvcode import (
|
||||
EVENT_ARG,
|
||||
LVGL_COMP_ARG,
|
||||
LambdaContext,
|
||||
ReturnStatement,
|
||||
add_line_marks,
|
||||
lv_add,
|
||||
lvgl_comp,
|
||||
lvgl_static,
|
||||
)
|
||||
from ..schemas import LVGL_SCHEMA
|
||||
from ..types import LvglAction, lv_page_t
|
||||
from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties
|
||||
from ..types import LvglAction, LvglCondition, lv_page_t
|
||||
from . import (
|
||||
Widget,
|
||||
WidgetType,
|
||||
add_widgets,
|
||||
get_widgets,
|
||||
set_obj_properties,
|
||||
wait_for_widgets,
|
||||
)
|
||||
|
||||
CONF_ON_LOAD = "on_load"
|
||||
CONF_ON_UNLOAD = "on_unload"
|
||||
|
||||
PAGE_ARG = "_page"
|
||||
|
||||
PAGE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_SKIP, default=False): lv_bool,
|
||||
@ -86,6 +97,30 @@ async def page_next_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"lvgl.page.is_showing",
|
||||
LvglCondition,
|
||||
cv.maybe_simple_value(
|
||||
cv.Schema({cv.Required(CONF_ID): cv.use_id(lv_page_t)}),
|
||||
key=CONF_ID,
|
||||
),
|
||||
)
|
||||
async def page_is_showing_to_code(config, condition_id, template_arg, args):
|
||||
await wait_for_widgets()
|
||||
page = await cg.get_variable(config[CONF_ID])
|
||||
async with LambdaContext(
|
||||
[(lv_page_t.operator("ptr"), PAGE_ARG)], return_type=cg.bool_
|
||||
) as context:
|
||||
lv_add(ReturnStatement(MockObj(PAGE_ARG, "->").is_showing()))
|
||||
var = cg.new_Pvariable(
|
||||
condition_id,
|
||||
TemplateArguments(lv_page_t, *template_arg),
|
||||
await context.get_lambda(),
|
||||
)
|
||||
await cg.register_parented(var, page)
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.page.previous",
|
||||
LvglAction,
|
||||
|
@ -11,16 +11,18 @@ void MicroNovaSwitch::write_state(bool state) {
|
||||
if (this->micronova_->get_current_stove_state() == 0) {
|
||||
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_on_);
|
||||
this->publish_state(true);
|
||||
} else
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unable to turn stove on, invalid state: %d", micronova_->get_current_stove_state());
|
||||
}
|
||||
} else {
|
||||
// don't send power-off when status is Off or Final cleaning
|
||||
if (this->micronova_->get_current_stove_state() != 0 && micronova_->get_current_stove_state() != 6) {
|
||||
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_off_);
|
||||
this->publish_state(false);
|
||||
} else
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unable to turn stove off, invalid state: %d", micronova_->get_current_stove_state());
|
||||
}
|
||||
}
|
||||
this->micronova_->update();
|
||||
break;
|
||||
|
||||
|
@ -373,7 +373,7 @@ async def to_code(config):
|
||||
)
|
||||
)
|
||||
|
||||
cg.add(var.set_topic_prefix(config[CONF_TOPIC_PREFIX]))
|
||||
cg.add(var.set_topic_prefix(config[CONF_TOPIC_PREFIX], CORE.name))
|
||||
|
||||
if config[CONF_USE_ABBREVIATIONS]:
|
||||
cg.add_define("USE_MQTT_ABBREVIATIONS")
|
||||
|
@ -606,7 +606,13 @@ void MQTTClientComponent::set_log_level(int level) { this->log_level_ = level; }
|
||||
void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_backend_.set_keep_alive(keep_alive_s); }
|
||||
void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this->log_message_ = std::move(message); }
|
||||
const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; }
|
||||
void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix) { this->topic_prefix_ = topic_prefix; }
|
||||
void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix) {
|
||||
if (App.is_name_add_mac_suffix_enabled() && (topic_prefix == check_topic_prefix)) {
|
||||
this->topic_prefix_ = str_sanitize(App.get_name());
|
||||
} else {
|
||||
this->topic_prefix_ = topic_prefix;
|
||||
}
|
||||
}
|
||||
const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; }
|
||||
void MQTTClientComponent::set_publish_nan_as_none(bool publish_nan_as_none) {
|
||||
this->publish_nan_as_none_ = publish_nan_as_none;
|
||||
|
@ -165,7 +165,7 @@ class MQTTClientComponent : public Component {
|
||||
*
|
||||
* @param topic_prefix The topic prefix. The last "/" is appended automatically.
|
||||
*/
|
||||
void set_topic_prefix(const std::string &topic_prefix);
|
||||
void set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix);
|
||||
/// Get the topic prefix of this device, using default if necessary
|
||||
const std::string &get_topic_prefix() const;
|
||||
|
||||
|
@ -49,6 +49,23 @@ class TouchTrigger : public Trigger<uint8_t, uint8_t, bool> {
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class NextionSetBrightnessAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit NextionSetBrightnessAction(Nextion *component) : component_(component) {}
|
||||
|
||||
TEMPLATABLE_VALUE(float, brightness)
|
||||
|
||||
void play(Ts... x) override {
|
||||
this->component_->set_brightness(this->brightness_.value(x...));
|
||||
this->component_->set_backlight_brightness(this->brightness_.value(x...));
|
||||
}
|
||||
|
||||
void set_brightness(std::function<void(Ts..., float)> brightness) { this->brightness_ = brightness; }
|
||||
|
||||
protected:
|
||||
Nextion *component_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class NextionPublishFloatAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit NextionPublishFloatAction(NextionComponent *component) : component_(component) {}
|
||||
|
@ -1,30 +1,30 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import automation
|
||||
from esphome.components import display, uart
|
||||
from esphome.components import esp32
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, esp32, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BRIGHTNESS,
|
||||
CONF_ID,
|
||||
CONF_LAMBDA,
|
||||
CONF_BRIGHTNESS,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_ON_TOUCH,
|
||||
CONF_TRIGGER_ID,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
from . import Nextion, nextion_ns, nextion_ref
|
||||
from .base_component import (
|
||||
CONF_AUTO_WAKE_ON_TOUCH,
|
||||
CONF_EXIT_REPARSE_ON_START,
|
||||
CONF_ON_BUFFER_OVERFLOW,
|
||||
CONF_ON_PAGE,
|
||||
CONF_ON_SETUP,
|
||||
CONF_ON_SLEEP,
|
||||
CONF_ON_WAKE,
|
||||
CONF_ON_SETUP,
|
||||
CONF_ON_PAGE,
|
||||
CONF_SKIP_CONNECTION_HANDSHAKE,
|
||||
CONF_START_UP_PAGE,
|
||||
CONF_TFT_URL,
|
||||
CONF_TOUCH_SLEEP_TIMEOUT,
|
||||
CONF_WAKE_UP_PAGE,
|
||||
CONF_START_UP_PAGE,
|
||||
CONF_AUTO_WAKE_ON_TOUCH,
|
||||
CONF_EXIT_REPARSE_ON_START,
|
||||
CONF_SKIP_CONNECTION_HANDSHAKE,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"]
|
||||
@ -32,6 +32,9 @@ CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"]
|
||||
|
||||
NextionSetBrightnessAction = nextion_ns.class_(
|
||||
"NextionSetBrightnessAction", automation.Action
|
||||
)
|
||||
SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template())
|
||||
SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template())
|
||||
WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template())
|
||||
@ -46,7 +49,7 @@ CONFIG_SCHEMA = (
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Nextion),
|
||||
cv.Optional(CONF_TFT_URL): cv.url,
|
||||
cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage,
|
||||
cv.Optional(CONF_BRIGHTNESS): cv.percentage,
|
||||
cv.Optional(CONF_ON_SETUP): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SetupTrigger),
|
||||
@ -92,12 +95,34 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"display.nextion.set_brightness",
|
||||
NextionSetBrightnessAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(Nextion),
|
||||
cv.Required(CONF_BRIGHTNESS): cv.templatable(cv.percentage),
|
||||
},
|
||||
key=CONF_BRIGHTNESS,
|
||||
),
|
||||
)
|
||||
async def nextion_set_brightness_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, float)
|
||||
cg.add(var.set_brightness(template_))
|
||||
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
if CONF_BRIGHTNESS in config:
|
||||
cg.add(var.set_brightness(config[CONF_BRIGHTNESS]))
|
||||
|
||||
if CONF_LAMBDA in config:
|
||||
lambda_ = await cg.process_lambda(
|
||||
config[CONF_LAMBDA], [(nextion_ref, "it")], return_type=cg.void
|
||||
|
@ -273,7 +273,9 @@ void Nextion::loop() {
|
||||
this->sent_setup_commands_ = true;
|
||||
this->send_command_("bkcmd=3"); // Always, returns 0x00 to 0x23 result of serial command.
|
||||
|
||||
this->set_backlight_brightness(this->brightness_);
|
||||
if (this->brightness_.has_value()) {
|
||||
this->set_backlight_brightness(this->brightness_.value());
|
||||
}
|
||||
|
||||
// Check if a startup page has been set and send the command
|
||||
if (this->start_up_page_ != -1) {
|
||||
|
@ -1339,7 +1339,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
CallbackManager<void()> buffer_overflow_callback_{};
|
||||
|
||||
optional<nextion_writer_t> writer_;
|
||||
float brightness_{1.0};
|
||||
optional<float> brightness_;
|
||||
|
||||
std::string device_model_;
|
||||
std::string firmware_version_;
|
||||
|
@ -4,14 +4,18 @@ from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
|
||||
from esphome.components.image import (
|
||||
CONF_INVERT_ALPHA,
|
||||
CONF_USE_TRANSPARENCY,
|
||||
IMAGE_TYPE,
|
||||
IMAGE_SCHEMA,
|
||||
Image_,
|
||||
validate_cross_dependencies,
|
||||
get_image_type_enum,
|
||||
get_transparency_enum,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUFFER_SIZE,
|
||||
CONF_DITHER,
|
||||
CONF_FILE,
|
||||
CONF_FORMAT,
|
||||
CONF_ID,
|
||||
CONF_ON_ERROR,
|
||||
@ -23,7 +27,7 @@ from esphome.const import (
|
||||
|
||||
AUTO_LOAD = ["image"]
|
||||
DEPENDENCIES = ["display", "http_request"]
|
||||
CODEOWNERS = ["@guillempages"]
|
||||
CODEOWNERS = ["@guillempages", "@clydebarrow"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
|
||||
@ -35,9 +39,30 @@ online_image_ns = cg.esphome_ns.namespace("online_image")
|
||||
|
||||
ImageFormat = online_image_ns.enum("ImageFormat")
|
||||
|
||||
FORMAT_PNG = "PNG"
|
||||
|
||||
IMAGE_FORMAT = {FORMAT_PNG: ImageFormat.PNG} # Add new supported formats here
|
||||
class Format:
|
||||
def __init__(self, image_type):
|
||||
self.image_type = image_type
|
||||
|
||||
@property
|
||||
def enum(self):
|
||||
return getattr(ImageFormat, self.image_type)
|
||||
|
||||
def actions(self):
|
||||
pass
|
||||
|
||||
|
||||
class PNGFormat(Format):
|
||||
def __init__(self):
|
||||
super().__init__("PNG")
|
||||
|
||||
def actions(self):
|
||||
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
|
||||
cg.add_library("pngle", "1.0.2")
|
||||
|
||||
|
||||
# New formats can be added here.
|
||||
IMAGE_FORMATS = {x.image_type: x for x in (PNGFormat(),)}
|
||||
|
||||
OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_)
|
||||
|
||||
@ -57,28 +82,32 @@ DownloadErrorTrigger = online_image_ns.class_(
|
||||
"DownloadErrorTrigger", automation.Trigger.template()
|
||||
)
|
||||
|
||||
ONLINE_IMAGE_SCHEMA = cv.Schema(
|
||||
|
||||
def remove_options(*options):
|
||||
return {
|
||||
cv.Optional(option): cv.invalid(
|
||||
f"{option} is an invalid option for online_image"
|
||||
)
|
||||
for option in options
|
||||
}
|
||||
|
||||
|
||||
ONLINE_IMAGE_SCHEMA = (
|
||||
IMAGE_SCHEMA.extend(remove_options(CONF_FILE, CONF_INVERT_ALPHA, CONF_DITHER))
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(OnlineImage),
|
||||
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
|
||||
#
|
||||
# Common image options
|
||||
#
|
||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True),
|
||||
# Not setting default here on purpose; the default depends on the image type,
|
||||
# and thus will be set in the "validate_cross_dependencies" validator.
|
||||
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
|
||||
#
|
||||
# Online Image specific options
|
||||
#
|
||||
cv.Required(CONF_URL): cv.url,
|
||||
cv.Required(CONF_FORMAT): cv.enum(IMAGE_FORMAT, upper=True),
|
||||
cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True),
|
||||
cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_),
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536),
|
||||
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadFinishedTrigger),
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DownloadFinishedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ERROR): automation.validate_automation(
|
||||
@ -87,18 +116,20 @@ ONLINE_IMAGE_SCHEMA = cv.Schema(
|
||||
}
|
||||
),
|
||||
}
|
||||
).extend(cv.polling_component_schema("never"))
|
||||
)
|
||||
.extend(cv.polling_component_schema("never"))
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
cv.All(
|
||||
ONLINE_IMAGE_SCHEMA,
|
||||
validate_cross_dependencies,
|
||||
cv.require_framework_version(
|
||||
# esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed
|
||||
# esp8266_arduino=cv.Version(2, 7, 0),
|
||||
esp32_arduino=cv.Version(0, 0, 0),
|
||||
esp_idf=cv.Version(4, 0, 0),
|
||||
rp2040_arduino=cv.Version(0, 0, 0),
|
||||
host=cv.Version(0, 0, 0),
|
||||
),
|
||||
)
|
||||
)
|
||||
@ -132,29 +163,26 @@ async def online_image_action_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
format = config[CONF_FORMAT]
|
||||
if format in [FORMAT_PNG]:
|
||||
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
|
||||
cg.add_library("pngle", "1.0.2")
|
||||
image_format = IMAGE_FORMATS[config[CONF_FORMAT]]
|
||||
image_format.actions()
|
||||
|
||||
url = config[CONF_URL]
|
||||
width, height = config.get(CONF_RESIZE, (0, 0))
|
||||
transparent = config[CONF_USE_TRANSPARENCY]
|
||||
transparent = get_transparency_enum(config[CONF_USE_TRANSPARENCY])
|
||||
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
url,
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
config[CONF_TYPE],
|
||||
image_format.enum,
|
||||
get_image_type_enum(config[CONF_TYPE]),
|
||||
transparent,
|
||||
config[CONF_BUFFER_SIZE],
|
||||
)
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
|
||||
|
||||
cg.add(var.set_transparency(transparent))
|
||||
|
||||
if placeholder_id := config.get(CONF_PLACEHOLDER):
|
||||
placeholder = await cg.get_variable(placeholder_id)
|
||||
cg.add(var.set_placeholder(placeholder))
|
||||
|
@ -1,5 +1,4 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/color.h"
|
||||
|
||||
namespace esphome {
|
||||
@ -23,7 +22,7 @@ class ImageDecoder {
|
||||
/**
|
||||
* @brief Initialize the decoder.
|
||||
*
|
||||
* @param download_size The total number of bytes that need to be download for the image.
|
||||
* @param download_size The total number of bytes that need to be downloaded for the image.
|
||||
*/
|
||||
virtual void prepare(uint32_t download_size) { this->download_size_ = download_size; }
|
||||
|
||||
@ -38,7 +37,7 @@ class ImageDecoder {
|
||||
* @return int The amount of bytes read. It can be 0 if the buffer does not have enough content to meaningfully
|
||||
* decode anything, or negative in case of a decoding error.
|
||||
*/
|
||||
virtual int decode(uint8_t *buffer, size_t size);
|
||||
virtual int decode(uint8_t *buffer, size_t size) = 0;
|
||||
|
||||
/**
|
||||
* @brief Request the image to be resized once the actual dimensions are known.
|
||||
@ -50,7 +49,7 @@ class ImageDecoder {
|
||||
void set_size(int width, int height);
|
||||
|
||||
/**
|
||||
* @brief Draw a rectangle on the display_buffer using the defined color.
|
||||
* @brief Fill a rectangle on the display_buffer using the defined color.
|
||||
* Will check the given coordinates for out-of-bounds, and clip the rectangle accordingly.
|
||||
* In case of binary displays, the color will be converted to binary as well.
|
||||
* Called by the callback functions, to be able to access the parent Image class.
|
||||
@ -59,7 +58,7 @@ class ImageDecoder {
|
||||
* @param y The top-most coordinate of the rectangle.
|
||||
* @param w The width of the rectangle.
|
||||
* @param h The height of the rectangle.
|
||||
* @param color The color to draw the rectangle with.
|
||||
* @param color The fill color
|
||||
*/
|
||||
void draw(int x, int y, int w, int h, const Color &color);
|
||||
|
||||
@ -67,7 +66,7 @@ class ImageDecoder {
|
||||
|
||||
protected:
|
||||
OnlineImage *image_;
|
||||
// Initializing to 1, to ensure it is different than initial "decoded_bytes_".
|
||||
// Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_".
|
||||
// Will be overwritten anyway once the download size is known.
|
||||
uint32_t download_size_ = 1;
|
||||
uint32_t decoded_bytes_ = 0;
|
||||
|
@ -25,8 +25,8 @@ inline bool is_color_on(const Color &color) {
|
||||
}
|
||||
|
||||
OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
|
||||
uint32_t download_buffer_size)
|
||||
: Image(nullptr, 0, 0, type),
|
||||
image::Transparency transparency, uint32_t download_buffer_size)
|
||||
: Image(nullptr, 0, 0, type, transparency),
|
||||
buffer_(nullptr),
|
||||
download_buffer_(download_buffer_size),
|
||||
format_(format),
|
||||
@ -45,7 +45,7 @@ void OnlineImage::draw(int x, int y, display::Display *display, Color color_on,
|
||||
|
||||
void OnlineImage::release() {
|
||||
if (this->buffer_) {
|
||||
ESP_LOGD(TAG, "Deallocating old buffer...");
|
||||
ESP_LOGV(TAG, "Deallocating old buffer...");
|
||||
this->allocator_.deallocate(this->buffer_, this->get_buffer_size_());
|
||||
this->data_start_ = nullptr;
|
||||
this->buffer_ = nullptr;
|
||||
@ -70,20 +70,19 @@ bool OnlineImage::resize_(int width_in, int height_in) {
|
||||
if (this->buffer_) {
|
||||
return false;
|
||||
}
|
||||
auto new_size = this->get_buffer_size_(width, height);
|
||||
ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size);
|
||||
delay_microseconds_safe(2000);
|
||||
size_t new_size = this->get_buffer_size_(width, height);
|
||||
ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size);
|
||||
this->buffer_ = this->allocator_.allocate(new_size);
|
||||
if (this->buffer_) {
|
||||
this->buffer_width_ = width;
|
||||
this->buffer_height_ = height;
|
||||
this->width_ = width;
|
||||
ESP_LOGD(TAG, "New size: (%d, %d)", width, height);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "allocation failed. Biggest block in heap: %zu Bytes", this->allocator_.get_max_free_block_size());
|
||||
if (this->buffer_ == nullptr) {
|
||||
ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size,
|
||||
this->allocator_.get_max_free_block_size());
|
||||
this->end_connection_();
|
||||
return false;
|
||||
}
|
||||
this->buffer_width_ = width;
|
||||
this->buffer_height_ = height;
|
||||
this->width_ = width;
|
||||
ESP_LOGV(TAG, "New size: (%d, %d)", width, height);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -91,9 +90,8 @@ void OnlineImage::update() {
|
||||
if (this->decoder_) {
|
||||
ESP_LOGW(TAG, "Image already being updated.");
|
||||
return;
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Updating image");
|
||||
}
|
||||
ESP_LOGI(TAG, "Updating image %s", this->url_.c_str());
|
||||
|
||||
this->downloader_ = this->parent_->get(this->url_);
|
||||
|
||||
@ -142,10 +140,11 @@ void OnlineImage::loop() {
|
||||
return;
|
||||
}
|
||||
if (!this->downloader_ || this->decoder_->is_finished()) {
|
||||
ESP_LOGD(TAG, "Image fully downloaded");
|
||||
this->data_start_ = buffer_;
|
||||
this->width_ = buffer_width_;
|
||||
this->height_ = buffer_height_;
|
||||
ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(),
|
||||
this->width_, this->height_);
|
||||
this->end_connection_();
|
||||
this->download_finished_callback_.call();
|
||||
return;
|
||||
@ -171,6 +170,19 @@ void OnlineImage::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineImage::map_chroma_key(Color &color) {
|
||||
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
||||
if (color.g == 1 && color.r == 0 && color.b == 0) {
|
||||
color.g = 0;
|
||||
}
|
||||
if (color.w < 0x80) {
|
||||
color.r = 0;
|
||||
color.g = this->type_ == ImageType::IMAGE_TYPE_RGB565 ? 4 : 1;
|
||||
color.b = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
||||
if (!this->buffer_) {
|
||||
ESP_LOGE(TAG, "Buffer not allocated!");
|
||||
@ -184,57 +196,53 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
||||
switch (this->type_) {
|
||||
case ImageType::IMAGE_TYPE_BINARY: {
|
||||
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
|
||||
const uint32_t pos = x + y * width_8;
|
||||
if ((this->has_transparency() && color.w > 127) || is_color_on(color)) {
|
||||
this->buffer_[pos / 8u] |= (0x80 >> (pos % 8u));
|
||||
pos = x + y * width_8;
|
||||
auto bitno = 0x80 >> (pos % 8u);
|
||||
pos /= 8u;
|
||||
auto on = is_color_on(color);
|
||||
if (this->has_transparency() && color.w < 0x80)
|
||||
on = false;
|
||||
if (on) {
|
||||
this->buffer_[pos] |= bitno;
|
||||
} else {
|
||||
this->buffer_[pos / 8u] &= ~(0x80 >> (pos % 8u));
|
||||
this->buffer_[pos] &= ~bitno;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_GRAYSCALE: {
|
||||
uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
||||
if (this->has_transparency()) {
|
||||
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
||||
if (gray == 1) {
|
||||
gray = 0;
|
||||
}
|
||||
if (color.w < 0x80) {
|
||||
gray = 1;
|
||||
}
|
||||
} else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
if (color.w != 0xFF)
|
||||
gray = color.w;
|
||||
}
|
||||
this->buffer_[pos] = gray;
|
||||
break;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_RGB565: {
|
||||
this->map_chroma_key(color);
|
||||
uint16_t col565 = display::ColorUtil::color_to_565(color);
|
||||
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
||||
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
|
||||
if (this->has_transparency())
|
||||
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
this->buffer_[pos + 2] = color.w;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_RGBA: {
|
||||
case ImageType::IMAGE_TYPE_RGB: {
|
||||
this->map_chroma_key(color);
|
||||
this->buffer_[pos + 0] = color.r;
|
||||
this->buffer_[pos + 1] = color.g;
|
||||
this->buffer_[pos + 2] = color.b;
|
||||
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
this->buffer_[pos + 3] = color.w;
|
||||
break;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_RGB24:
|
||||
default: {
|
||||
if (this->has_transparency()) {
|
||||
if (color.b == 1 && color.r == 0 && color.g == 0) {
|
||||
color.b = 0;
|
||||
}
|
||||
if (color.w < 0x80) {
|
||||
color.r = 0;
|
||||
color.g = 0;
|
||||
color.b = 1;
|
||||
}
|
||||
}
|
||||
this->buffer_[pos + 0] = color.r;
|
||||
this->buffer_[pos + 1] = color.g;
|
||||
this->buffer_[pos + 2] = color.b;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -48,12 +48,13 @@ class OnlineImage : public PollingComponent,
|
||||
* @param buffer_size Size of the buffer used to download the image.
|
||||
*/
|
||||
OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type,
|
||||
uint32_t buffer_size);
|
||||
image::Transparency transparency, uint32_t buffer_size);
|
||||
|
||||
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
||||
|
||||
void update() override;
|
||||
void loop() override;
|
||||
void map_chroma_key(Color &color);
|
||||
|
||||
/** Set the URL to download the image from. */
|
||||
void set_url(const std::string &url) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "image_decoder.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
#include <pngle.h>
|
||||
|
||||
|
@ -21,7 +21,14 @@ void PsramComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available));
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0)
|
||||
if (available) {
|
||||
ESP_LOGCONFIG(TAG, " Size: %d KB", heap_caps_get_total_size(MALLOC_CAP_SPIRAM) / 1024);
|
||||
const size_t psram_total_size_bytes = heap_caps_get_total_size(MALLOC_CAP_SPIRAM);
|
||||
const float psram_total_size_kb = psram_total_size_bytes / 1024.0f;
|
||||
|
||||
if (abs(std::round(psram_total_size_kb) - psram_total_size_kb) < 0.05f) {
|
||||
ESP_LOGCONFIG(TAG, " Size: %.0f KB", psram_total_size_kb);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Size: %zu bytes", psram_total_size_bytes);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -97,11 +97,7 @@ RP_SPI_PINSETS = [
|
||||
|
||||
|
||||
def get_target_platform():
|
||||
return (
|
||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM]
|
||||
if KEY_TARGET_PLATFORM in CORE.data[KEY_CORE]
|
||||
else ""
|
||||
)
|
||||
return CORE.data[KEY_CORE][KEY_TARGET_PLATFORM]
|
||||
|
||||
|
||||
def get_target_variant():
|
||||
|
@ -1,8 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import light, spi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import light
|
||||
from esphome.components import spi
|
||||
from esphome.const import CONF_OUTPUT_ID, CONF_NUM_LEDS
|
||||
from esphome.const import CONF_NUM_LEDS, CONF_OUTPUT_ID
|
||||
|
||||
spi_led_strip_ns = cg.esphome_ns.namespace("spi_led_strip")
|
||||
SpiLedStrip = spi_led_strip_ns.class_(
|
||||
@ -18,8 +17,7 @@ CONFIG_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
|
||||
cg.add(var.set_num_leds(config[CONF_NUM_LEDS]))
|
||||
var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_NUM_LEDS])
|
||||
await light.register_light(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await cg.register_component(var, config)
|
||||
|
67
esphome/components/spi_led_strip/spi_led_strip.cpp
Normal file
67
esphome/components/spi_led_strip/spi_led_strip.cpp
Normal file
@ -0,0 +1,67 @@
|
||||
#include "spi_led_strip.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace spi_led_strip {
|
||||
|
||||
SpiLedStrip::SpiLedStrip(uint16_t num_leds) {
|
||||
this->num_leds_ = num_leds;
|
||||
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
this->buffer_size_ = num_leds * 4 + 8;
|
||||
this->buf_ = allocator.allocate(this->buffer_size_);
|
||||
if (this->buf_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate buffer of size %u", this->buffer_size_);
|
||||
return;
|
||||
}
|
||||
|
||||
this->effect_data_ = allocator.allocate(num_leds);
|
||||
if (this->effect_data_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate effect data of size %u", num_leds);
|
||||
return;
|
||||
}
|
||||
memset(this->buf_, 0xFF, this->buffer_size_);
|
||||
memset(this->buf_, 0, 4);
|
||||
}
|
||||
void SpiLedStrip::setup() {
|
||||
if (this->effect_data_ == nullptr || this->buf_ == nullptr) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->spi_setup();
|
||||
}
|
||||
light::LightTraits SpiLedStrip::get_traits() {
|
||||
auto traits = light::LightTraits();
|
||||
traits.set_supported_color_modes({light::ColorMode::RGB});
|
||||
return traits;
|
||||
}
|
||||
void SpiLedStrip::dump_config() {
|
||||
esph_log_config(TAG, "SPI LED Strip:");
|
||||
esph_log_config(TAG, " LEDs: %d", this->num_leds_);
|
||||
if (this->data_rate_ >= spi::DATA_RATE_1MHZ) {
|
||||
esph_log_config(TAG, " Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000));
|
||||
} else {
|
||||
esph_log_config(TAG, " Data rate: %ukHz", (unsigned) (this->data_rate_ / 1000));
|
||||
}
|
||||
}
|
||||
void SpiLedStrip::write_state(light::LightState *state) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
|
||||
char strbuf[49];
|
||||
size_t len = std::min(this->buffer_size_, (size_t) (sizeof(strbuf) - 1) / 3);
|
||||
memset(strbuf, 0, sizeof(strbuf));
|
||||
for (size_t i = 0; i != len; i++) {
|
||||
sprintf(strbuf + i * 3, "%02X ", this->buf_[i]);
|
||||
}
|
||||
esph_log_v(TAG, "write_state: buf = %s", strbuf);
|
||||
}
|
||||
this->enable();
|
||||
this->write_array(this->buf_, this->buffer_size_);
|
||||
this->disable();
|
||||
}
|
||||
light::ESPColorView SpiLedStrip::get_view_internal(int32_t index) const {
|
||||
size_t pos = index * 4 + 5;
|
||||
return {this->buf_ + pos + 2, this->buf_ + pos + 1, this->buf_ + pos + 0, nullptr,
|
||||
this->effect_data_ + index, &this->correction_};
|
||||
}
|
||||
} // namespace spi_led_strip
|
||||
} // namespace esphome
|
@ -13,74 +13,22 @@ class SpiLedStrip : public light::AddressableLight,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
|
||||
spi::DATA_RATE_1MHZ> {
|
||||
public:
|
||||
void setup() override { this->spi_setup(); }
|
||||
SpiLedStrip(uint16_t num_leds);
|
||||
void setup() override;
|
||||
float get_setup_priority() const override { return setup_priority::IO; }
|
||||
|
||||
int32_t size() const override { return this->num_leds_; }
|
||||
|
||||
light::LightTraits get_traits() override {
|
||||
auto traits = light::LightTraits();
|
||||
traits.set_supported_color_modes({light::ColorMode::RGB});
|
||||
return traits;
|
||||
}
|
||||
void set_num_leds(uint16_t num_leds) {
|
||||
this->num_leds_ = num_leds;
|
||||
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
this->buffer_size_ = num_leds * 4 + 8;
|
||||
this->buf_ = allocator.allocate(this->buffer_size_);
|
||||
if (this->buf_ == nullptr) {
|
||||
esph_log_e(TAG, "Failed to allocate buffer of size %u", this->buffer_size_);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
light::LightTraits get_traits() override;
|
||||
|
||||
this->effect_data_ = allocator.allocate(num_leds);
|
||||
if (this->effect_data_ == nullptr) {
|
||||
esph_log_e(TAG, "Failed to allocate effect data of size %u", num_leds);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
memset(this->buf_, 0xFF, this->buffer_size_);
|
||||
memset(this->buf_, 0, 4);
|
||||
}
|
||||
void dump_config() override;
|
||||
|
||||
void dump_config() override {
|
||||
esph_log_config(TAG, "SPI LED Strip:");
|
||||
esph_log_config(TAG, " LEDs: %d", this->num_leds_);
|
||||
if (this->data_rate_ >= spi::DATA_RATE_1MHZ) {
|
||||
esph_log_config(TAG, " Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000));
|
||||
} else {
|
||||
esph_log_config(TAG, " Data rate: %ukHz", (unsigned) (this->data_rate_ / 1000));
|
||||
}
|
||||
}
|
||||
void write_state(light::LightState *state) override;
|
||||
|
||||
void write_state(light::LightState *state) override {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
|
||||
char strbuf[49];
|
||||
size_t len = std::min(this->buffer_size_, (size_t) (sizeof(strbuf) - 1) / 3);
|
||||
memset(strbuf, 0, sizeof(strbuf));
|
||||
for (size_t i = 0; i != len; i++) {
|
||||
sprintf(strbuf + i * 3, "%02X ", this->buf_[i]);
|
||||
}
|
||||
esph_log_v(TAG, "write_state: buf = %s", strbuf);
|
||||
}
|
||||
this->enable();
|
||||
this->write_array(this->buf_, this->buffer_size_);
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void clear_effect_data() override {
|
||||
for (int i = 0; i < this->size(); i++)
|
||||
this->effect_data_[i] = 0;
|
||||
}
|
||||
void clear_effect_data() override { memset(this->effect_data_, 0, this->num_leds_ * sizeof(this->effect_data_[0])); }
|
||||
|
||||
protected:
|
||||
light::ESPColorView get_view_internal(int32_t index) const override {
|
||||
size_t pos = index * 4 + 5;
|
||||
return {this->buf_ + pos + 2, this->buf_ + pos + 1, this->buf_ + pos + 0, nullptr,
|
||||
this->effect_data_ + index, &this->correction_};
|
||||
}
|
||||
light::ESPColorView get_view_internal(int32_t index) const override;
|
||||
|
||||
size_t buffer_size_{};
|
||||
uint8_t *effect_data_{nullptr};
|
||||
|
@ -184,11 +184,13 @@ void SprinklerValveOperator::set_controller(Sprinkler *controller) {
|
||||
|
||||
void SprinklerValveOperator::set_valve(SprinklerValve *valve) {
|
||||
if (valve != nullptr) {
|
||||
if (this->state_ != IDLE) { // Only kill if not already idle
|
||||
this->kill_(); // ensure everything is off before we let go!
|
||||
}
|
||||
this->state_ = IDLE; // reset state
|
||||
this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it
|
||||
this->start_millis_ = 0; // reset because (new) valve has not been started yet
|
||||
this->stop_millis_ = 0; // reset because (new) valve has not been started yet
|
||||
this->kill_(); // ensure everything is off before we let go!
|
||||
this->valve_ = valve; // finally, set the pointer to the new valve
|
||||
}
|
||||
}
|
||||
|
@ -106,8 +106,9 @@ void ToshibaClimate::setup() {
|
||||
this->publish_state();
|
||||
});
|
||||
this->current_temperature = this->sensor_->state;
|
||||
} else
|
||||
} else {
|
||||
this->current_temperature = NAN;
|
||||
}
|
||||
// restore set points
|
||||
auto restore = this->restore_state_();
|
||||
if (restore.has_value()) {
|
||||
|
@ -120,8 +120,9 @@ light::LightTraits TuyaLight::get_traits() {
|
||||
traits.set_supported_color_modes(
|
||||
{light::ColorMode::RGB_COLOR_TEMPERATURE, light::ColorMode::COLOR_TEMPERATURE});
|
||||
}
|
||||
} else
|
||||
} else {
|
||||
traits.set_supported_color_modes({light::ColorMode::COLOR_TEMPERATURE});
|
||||
}
|
||||
traits.set_min_mireds(this->cold_white_temperature_);
|
||||
traits.set_max_mireds(this->warm_white_temperature_);
|
||||
} else if (this->color_id_.has_value()) {
|
||||
@ -131,8 +132,9 @@ light::LightTraits TuyaLight::get_traits() {
|
||||
} else {
|
||||
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE});
|
||||
}
|
||||
} else
|
||||
} else {
|
||||
traits.set_supported_color_modes({light::ColorMode::RGB});
|
||||
}
|
||||
} else if (this->dimmer_id_.has_value()) {
|
||||
traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS});
|
||||
} else {
|
||||
|
@ -85,7 +85,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.GenerateID(): cv.declare_id(UDPComponent),
|
||||
cv.Optional(CONF_PORT, default=18511): cv.port,
|
||||
cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list(
|
||||
cv.ipv4
|
||||
cv.ipv4address,
|
||||
),
|
||||
cv.Optional(CONF_ROLLING_CODE_ENABLE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PING_PONG_ENABLE, default=False): cv.boolean,
|
||||
|
@ -245,13 +245,9 @@ void UDPComponent::setup() {
|
||||
}
|
||||
struct sockaddr_in server {};
|
||||
|
||||
socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_);
|
||||
if (sl == 0) {
|
||||
ESP_LOGE(TAG, "Socket unable to set sockaddr: errno %d", errno);
|
||||
this->mark_failed();
|
||||
this->status_set_error("Unable to set sockaddr");
|
||||
return;
|
||||
}
|
||||
server.sin_family = AF_INET;
|
||||
server.sin_addr.s_addr = ESPHOME_INADDR_ANY;
|
||||
server.sin_port = htons(this->port_);
|
||||
|
||||
err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server));
|
||||
if (err != 0) {
|
||||
|
@ -1,14 +1,14 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor, time
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_TIME_ID,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
ICON_TIMER,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_SECOND,
|
||||
ICON_TIMER,
|
||||
DEVICE_CLASS_DURATION,
|
||||
)
|
||||
|
||||
uptime_ns = cg.esphome_ns.namespace("uptime")
|
19
esphome/components/uptime/text_sensor/__init__.py
Normal file
19
esphome/components/uptime/text_sensor/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ENTITY_CATEGORY_DIAGNOSTIC, ICON_TIMER
|
||||
|
||||
uptime_ns = cg.esphome_ns.namespace("uptime")
|
||||
UptimeTextSensor = uptime_ns.class_(
|
||||
"UptimeTextSensor", text_sensor.TextSensor, cg.PollingComponent
|
||||
)
|
||||
CONFIG_SCHEMA = text_sensor.text_sensor_schema(
|
||||
UptimeTextSensor,
|
||||
icon=ICON_TIMER,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
).extend(cv.polling_component_schema("60s"))
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
await cg.register_component(var, config)
|
46
esphome/components/uptime/text_sensor/uptime_text_sensor.cpp
Normal file
46
esphome/components/uptime/text_sensor/uptime_text_sensor.cpp
Normal file
@ -0,0 +1,46 @@
|
||||
#include "uptime_text_sensor.h"
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace uptime {
|
||||
|
||||
static const char *const TAG = "uptime.sensor";
|
||||
|
||||
void UptimeTextSensor::setup() { this->last_ms_ = millis(); }
|
||||
|
||||
void UptimeTextSensor::update() {
|
||||
const auto now = millis();
|
||||
// get whole seconds since last update. Note that even if the millis count has overflowed between updates,
|
||||
// the difference will still be correct due to the way twos-complement arithmetic works.
|
||||
const uint32_t delta = (now - this->last_ms_) / 1000;
|
||||
if (delta == 0)
|
||||
return;
|
||||
// set last_ms_ to the last second boundary
|
||||
this->last_ms_ = now - (now % 1000);
|
||||
this->uptime_ += delta;
|
||||
auto uptime = this->uptime_;
|
||||
unsigned days = uptime / (24 * 3600);
|
||||
unsigned seconds = uptime % (24 * 3600);
|
||||
unsigned hours = seconds / 3600;
|
||||
seconds %= 3600;
|
||||
unsigned minutes = seconds / 60;
|
||||
seconds %= 60;
|
||||
if (days != 0) {
|
||||
this->publish_state(str_sprintf("%dd%dh%dm%ds", days, hours, minutes, seconds));
|
||||
} else if (hours != 0) {
|
||||
this->publish_state(str_sprintf("%dh%dm%ds", hours, minutes, seconds));
|
||||
} else if (minutes != 0) {
|
||||
this->publish_state(str_sprintf("%dm%ds", minutes, seconds));
|
||||
} else {
|
||||
this->publish_state(str_sprintf("%ds", seconds));
|
||||
}
|
||||
}
|
||||
|
||||
float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
void UptimeTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Uptime Text Sensor", this); }
|
||||
|
||||
} // namespace uptime
|
||||
} // namespace esphome
|
25
esphome/components/uptime/text_sensor/uptime_text_sensor.h
Normal file
25
esphome/components/uptime/text_sensor/uptime_text_sensor.h
Normal file
@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace uptime {
|
||||
|
||||
class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent {
|
||||
public:
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
void setup() override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
uint64_t uptime_{0};
|
||||
uint64_t last_ms_{0};
|
||||
};
|
||||
|
||||
} // namespace uptime
|
||||
} // namespace esphome
|
@ -2425,28 +2425,21 @@ void WaveshareEPaper7P5InBV3BWR::init_display_() {
|
||||
this->command(0x01);
|
||||
|
||||
// 1-0=11: internal power
|
||||
this->data(0x07);
|
||||
this->data(0x17); // VGH&VGL
|
||||
this->data(0x3F); // VSH
|
||||
this->data(0x26); // VSL
|
||||
this->data(0x11); // VSHR
|
||||
this->data(0x07); // VRS_EN=1, VS_EN=1, VG_EN=1
|
||||
this->data(0x17); // VGH&VGL ??? VCOM_SLEW=1 but this is fixed, VG_LVL[2:0]=111 => VGH=20V VGL=-20V, it could be 0x07
|
||||
this->data(0x3F); // VSH=15V?
|
||||
this->data(0x26); // VSL=-9.4V?
|
||||
this->data(0x11); // VSHR=5.8V?
|
||||
|
||||
// VCOM DC Setting
|
||||
this->command(0x82);
|
||||
this->data(0x24); // VCOM
|
||||
|
||||
// Booster Setting
|
||||
this->command(0x06);
|
||||
this->data(0x27);
|
||||
this->data(0x27);
|
||||
this->data(0x2F);
|
||||
this->data(0x17);
|
||||
this->data(0x24); // VCOM=-1.9V
|
||||
|
||||
// POWER ON
|
||||
this->command(0x04);
|
||||
|
||||
delay(100); // NOLINT
|
||||
this->wait_until_idle_();
|
||||
|
||||
// COMMAND PANEL SETTING
|
||||
this->command(0x00);
|
||||
this->data(0x0F); // KW-3f KWR-2F BWROTP 0f BWOTP 1f
|
||||
@ -2457,16 +2450,16 @@ void WaveshareEPaper7P5InBV3BWR::init_display_() {
|
||||
this->data(0x20);
|
||||
this->data(0x01); // gate 480
|
||||
this->data(0xE0);
|
||||
// COMMAND ...?
|
||||
this->command(0x15);
|
||||
this->data(0x00);
|
||||
|
||||
// COMMAND VCOM AND DATA INTERVAL SETTING
|
||||
this->command(0x50);
|
||||
this->data(0x20);
|
||||
this->data(0x00);
|
||||
|
||||
// COMMAND TCON SETTING
|
||||
this->command(0x60);
|
||||
this->data(0x22);
|
||||
|
||||
// Resolution setting
|
||||
this->command(0x65);
|
||||
this->data(0x00);
|
||||
|
@ -1415,6 +1415,30 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
|
||||
request->send(200, "application/json", data.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
auto call = obj->make_call();
|
||||
if (request->hasParam("code")) {
|
||||
call.set_code(request->getParam("code")->value().c_str());
|
||||
}
|
||||
|
||||
if (match.method == "disarm") {
|
||||
call.disarm();
|
||||
} else if (match.method == "arm_away") {
|
||||
call.arm_away();
|
||||
} else if (match.method == "arm_home") {
|
||||
call.arm_home();
|
||||
} else if (match.method == "arm_night") {
|
||||
call.arm_night();
|
||||
} else if (match.method == "arm_vacation") {
|
||||
call.arm_vacation();
|
||||
} else {
|
||||
request->send(404);
|
||||
return;
|
||||
}
|
||||
|
||||
this->schedule_([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
@ -1664,7 +1688,7 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
if (request->method() == HTTP_GET && match.domain == "alarm_control_panel")
|
||||
if ((request->method() == HTTP_GET || request->method() == HTTP_POST) && match.domain == "alarm_control_panel")
|
||||
return true;
|
||||
#endif
|
||||
|
||||
|
@ -93,16 +93,16 @@ def validate_channel(value):
|
||||
|
||||
AP_MANUAL_IP_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_STATIC_IP): cv.ipv4,
|
||||
cv.Required(CONF_GATEWAY): cv.ipv4,
|
||||
cv.Required(CONF_SUBNET): cv.ipv4,
|
||||
cv.Required(CONF_STATIC_IP): cv.ipv4address,
|
||||
cv.Required(CONF_GATEWAY): cv.ipv4address,
|
||||
cv.Required(CONF_SUBNET): cv.ipv4address,
|
||||
}
|
||||
)
|
||||
|
||||
STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4,
|
||||
cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4,
|
||||
cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4address,
|
||||
cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4address,
|
||||
}
|
||||
)
|
||||
|
||||
@ -364,7 +364,7 @@ def eap_auth(config):
|
||||
def safe_ip(ip):
|
||||
if ip is None:
|
||||
return IPAddress(0, 0, 0, 0)
|
||||
return IPAddress(*ip.args)
|
||||
return IPAddress(str(ip))
|
||||
|
||||
|
||||
def manual_ip(config):
|
||||
|
@ -11,10 +11,19 @@
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
#include <esp_wpa2.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
#include "dhcpserver/dhcpserver.h"
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
#include "lwip/apps/sntp.h"
|
||||
#include "lwip/dns.h"
|
||||
#include "lwip/err.h"
|
||||
|
||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||
#include "lwip/priv/tcpip_priv.h"
|
||||
#endif
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@ -286,11 +295,26 @@ bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
|
||||
}
|
||||
|
||||
if (!manual_ip.has_value()) {
|
||||
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
|
||||
// https://github.com/esphome/issues/issues/6591
|
||||
// https://github.com/espressif/arduino-esp32/issues/10526
|
||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||
if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
||||
LOCK_TCPIP_CORE();
|
||||
}
|
||||
#endif
|
||||
|
||||
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
|
||||
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
|
||||
// https://github.com/esphome/issues/issues/2299
|
||||
sntp_servermode_dhcp(false);
|
||||
|
||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||
if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
||||
UNLOCK_TCPIP_CORE();
|
||||
}
|
||||
#endif
|
||||
|
||||
// No manual IP is set; use DHCP client
|
||||
if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
|
||||
err = esp_netif_dhcpc_start(s_sta_netif);
|
||||
@ -638,7 +662,12 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
|
||||
}
|
||||
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
|
||||
auto status = WiFiClass::status();
|
||||
#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
|
||||
const auto status = WiFiClass::status();
|
||||
#else
|
||||
const auto status = WiFi.status();
|
||||
#endif
|
||||
|
||||
if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) {
|
||||
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
|
||||
}
|
||||
|
@ -67,8 +67,8 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Wireguard),
|
||||
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
cv.Required(CONF_ADDRESS): cv.ipv4,
|
||||
cv.Optional(CONF_NETMASK, default="255.255.255.255"): cv.ipv4,
|
||||
cv.Required(CONF_ADDRESS): cv.ipv4address,
|
||||
cv.Optional(CONF_NETMASK, default="255.255.255.255"): cv.ipv4address,
|
||||
cv.Required(CONF_PRIVATE_KEY): _wireguard_key,
|
||||
cv.Required(CONF_PEER_ENDPOINT): cv.string,
|
||||
cv.Required(CONF_PEER_PUBLIC_KEY): _wireguard_key,
|
||||
|
@ -104,8 +104,9 @@ void YashimaClimate::setup() {
|
||||
this->publish_state();
|
||||
});
|
||||
this->current_temperature = this->sensor_->state;
|
||||
} else
|
||||
} else {
|
||||
this->current_temperature = NAN;
|
||||
}
|
||||
// restore set points
|
||||
auto restore = this->restore_state_();
|
||||
if (restore.has_value()) {
|
||||
|
@ -18,6 +18,7 @@ from esphome.const import (
|
||||
CONF_ESPHOME,
|
||||
CONF_EXTERNAL_COMPONENTS,
|
||||
CONF_ID,
|
||||
CONF_MIN_VERSION,
|
||||
CONF_PACKAGES,
|
||||
CONF_PLATFORM,
|
||||
CONF_SUBSTITUTIONS,
|
||||
@ -839,6 +840,10 @@ def validate_config(
|
||||
# Remove temporary esphome config path again, it will be reloaded later
|
||||
result.remove_output_path([CONF_ESPHOME], CONF_ESPHOME)
|
||||
|
||||
# Check version number now to avoid loading components that are not supported
|
||||
if min_version := config[CONF_ESPHOME].get(CONF_MIN_VERSION):
|
||||
cv.All(cv.version_number, cv.validate_esphome_version)(min_version)
|
||||
|
||||
# First run platform validation steps
|
||||
for key in TARGET_PLATFORMS:
|
||||
if key in config:
|
||||
|
@ -3,6 +3,7 @@
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from ipaddress import AddressValueError, IPv4Address, ip_address
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@ -67,7 +68,6 @@ from esphome.const import (
|
||||
from esphome.core import (
|
||||
CORE,
|
||||
HexInt,
|
||||
IPAddress,
|
||||
Lambda,
|
||||
TimePeriod,
|
||||
TimePeriodMicroseconds,
|
||||
@ -1130,7 +1130,7 @@ def domain(value):
|
||||
if re.match(vol.DOMAIN_REGEX, value) is not None:
|
||||
return value
|
||||
try:
|
||||
return str(ipv4(value))
|
||||
return str(ipaddress(value))
|
||||
except Invalid as err:
|
||||
raise Invalid(f"Invalid domain: {value}") from err
|
||||
|
||||
@ -1160,21 +1160,20 @@ def ssid(value):
|
||||
return value
|
||||
|
||||
|
||||
def ipv4(value):
|
||||
if isinstance(value, list):
|
||||
parts = value
|
||||
elif isinstance(value, str):
|
||||
parts = value.split(".")
|
||||
elif isinstance(value, IPAddress):
|
||||
return value
|
||||
else:
|
||||
raise Invalid("IPv4 address must consist of either string or integer list")
|
||||
if len(parts) != 4:
|
||||
raise Invalid("IPv4 address must consist of four point-separated integers")
|
||||
parts_ = list(map(int, parts))
|
||||
if not all(0 <= x < 256 for x in parts_):
|
||||
raise Invalid("IPv4 address parts must be in range from 0 to 255")
|
||||
return IPAddress(*parts_)
|
||||
def ipv4address(value):
|
||||
try:
|
||||
address = IPv4Address(value)
|
||||
except AddressValueError as exc:
|
||||
raise Invalid(f"{value} is not a valid IPv4 address") from exc
|
||||
return address
|
||||
|
||||
|
||||
def ipaddress(value):
|
||||
try:
|
||||
address = ip_address(value)
|
||||
except ValueError as exc:
|
||||
raise Invalid(f"{value} is not a valid IP address") from exc
|
||||
return address
|
||||
|
||||
|
||||
def _valid_topic(value):
|
||||
|
@ -54,16 +54,6 @@ class HexInt(int):
|
||||
return f"{sign}0x{value:X}"
|
||||
|
||||
|
||||
class IPAddress:
|
||||
def __init__(self, *args):
|
||||
if len(args) != 4:
|
||||
raise ValueError("IPAddress must consist of 4 items")
|
||||
self.args = args
|
||||
|
||||
def __str__(self):
|
||||
return ".".join(str(x) for x in self.args)
|
||||
|
||||
|
||||
class MACAddress:
|
||||
def __init__(self, *parts):
|
||||
if len(parts) != 6:
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
|
@ -49,6 +49,7 @@
|
||||
#define USE_LVGL_IMAGE
|
||||
#define USE_LVGL_KEY_LISTENER
|
||||
#define USE_LVGL_KEYBOARD
|
||||
#define USE_LVGL_METER
|
||||
#define USE_LVGL_ROLLER
|
||||
#define USE_LVGL_ROTARY_ENCODER
|
||||
#define USE_LVGL_TOUCHSCREEN
|
||||
|
@ -45,7 +45,9 @@
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
#include "esp32/rom/crc.h"
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 2)
|
||||
#include "esp_mac.h"
|
||||
#endif
|
||||
#include "esp_efuse.h"
|
||||
#include "esp_efuse_table.h"
|
||||
#endif
|
||||
@ -126,6 +128,7 @@ uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t reverse
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
if (reverse_poly == 0xa001) {
|
||||
while (len--) {
|
||||
uint8_t combo = crc ^ (uint8_t) *data++;
|
||||
@ -143,6 +146,7 @@ uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t reverse
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return refout ? (crc ^ 0xffff) : crc;
|
||||
}
|
||||
|
||||
@ -259,7 +263,7 @@ bool random_bytes(uint8_t *data, size_t len) {
|
||||
bool str_equals_case_insensitive(const std::string &a, const std::string &b) {
|
||||
return strcasecmp(a.c_str(), b.c_str()) == 0;
|
||||
}
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
#if __cplusplus >= 202002L
|
||||
bool str_startswith(const std::string &str, const std::string &start) { return str.starts_with(start); }
|
||||
bool str_endswith(const std::string &str, const std::string &end) { return str.ends_with(end); }
|
||||
#else
|
||||
|
@ -163,7 +163,7 @@ template<typename T, typename U> T remap(U value, U min, U max, T min_out, T max
|
||||
return (value - min) * (max_out - min_out) / (max - min) + min_out;
|
||||
}
|
||||
|
||||
/// Calculate a CRC-8 checksum of \p data with size \p len.
|
||||
/// Calculate a CRC-8 checksum of \p data with size \p len using the CRC-8-Dallas/Maxim polynomial.
|
||||
uint8_t crc8(const uint8_t *data, uint8_t len);
|
||||
|
||||
/// Calculate a CRC-16 checksum of \p data with size \p len.
|
||||
|
@ -74,7 +74,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
#define esph_log_vv(tag, format, ...) \
|
||||
esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
|
||||
::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
|
||||
|
||||
#define ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
#else
|
||||
@ -83,7 +83,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
#define esph_log_v(tag, format, ...) \
|
||||
esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
|
||||
::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
|
||||
|
||||
#define ESPHOME_LOG_HAS_VERBOSE
|
||||
#else
|
||||
@ -92,9 +92,9 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
|
||||
#define esph_log_d(tag, format, ...) \
|
||||
esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
|
||||
::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
|
||||
#define esph_log_config(tag, format, ...) \
|
||||
esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
|
||||
::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
|
||||
|
||||
#define ESPHOME_LOG_HAS_DEBUG
|
||||
#define ESPHOME_LOG_HAS_CONFIG
|
||||
@ -105,7 +105,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO
|
||||
#define esph_log_i(tag, format, ...) \
|
||||
esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
|
||||
::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
|
||||
|
||||
#define ESPHOME_LOG_HAS_INFO
|
||||
#else
|
||||
@ -114,7 +114,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
|
||||
#define esph_log_w(tag, format, ...) \
|
||||
esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
|
||||
::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
|
||||
|
||||
#define ESPHOME_LOG_HAS_WARN
|
||||
#else
|
||||
@ -123,7 +123,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
|
||||
#define esph_log_e(tag, format, ...) \
|
||||
esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
|
||||
::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
|
||||
|
||||
#define ESPHOME_LOG_HAS_ERROR
|
||||
#else
|
||||
|
@ -67,20 +67,18 @@ class ESPHomeLogFormatter(logging.Formatter):
|
||||
|
||||
|
||||
def setup_log(
|
||||
debug: bool = False, quiet: bool = False, include_timestamp: bool = False
|
||||
log_level=logging.INFO,
|
||||
include_timestamp: bool = False,
|
||||
) -> None:
|
||||
import colorama
|
||||
|
||||
colorama.init()
|
||||
|
||||
if debug:
|
||||
log_level = logging.DEBUG
|
||||
if log_level == logging.DEBUG:
|
||||
CORE.verbose = True
|
||||
elif quiet:
|
||||
log_level = logging.CRITICAL
|
||||
elif log_level == logging.CRITICAL:
|
||||
CORE.quiet = True
|
||||
else:
|
||||
log_level = logging.INFO
|
||||
|
||||
logging.basicConfig(level=log_level)
|
||||
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
|
@ -4,6 +4,7 @@ import fnmatch
|
||||
import functools
|
||||
import inspect
|
||||
from io import TextIOWrapper
|
||||
from ipaddress import _BaseAddress
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
@ -25,7 +26,6 @@ from esphome.core import (
|
||||
CORE,
|
||||
DocumentRange,
|
||||
EsphomeError,
|
||||
IPAddress,
|
||||
Lambda,
|
||||
MACAddress,
|
||||
TimePeriod,
|
||||
@ -576,7 +576,7 @@ ESPHomeDumper.add_multi_representer(bool, ESPHomeDumper.represent_bool)
|
||||
ESPHomeDumper.add_multi_representer(str, ESPHomeDumper.represent_stringify)
|
||||
ESPHomeDumper.add_multi_representer(int, ESPHomeDumper.represent_int)
|
||||
ESPHomeDumper.add_multi_representer(float, ESPHomeDumper.represent_float)
|
||||
ESPHomeDumper.add_multi_representer(IPAddress, ESPHomeDumper.represent_stringify)
|
||||
ESPHomeDumper.add_multi_representer(_BaseAddress, ESPHomeDumper.represent_stringify)
|
||||
ESPHomeDumper.add_multi_representer(MACAddress, ESPHomeDumper.represent_stringify)
|
||||
ESPHomeDumper.add_multi_representer(TimePeriod, ESPHomeDumper.represent_stringify)
|
||||
ESPHomeDumper.add_multi_representer(Lambda, ESPHomeDumper.represent_lambda)
|
||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
||||
import sys
|
||||
|
||||
from esphome.config import get_component, get_platform
|
||||
from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK
|
||||
from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM
|
||||
from esphome.core import CORE
|
||||
from esphome.helpers import write_file_if_changed
|
||||
|
||||
@ -39,7 +39,7 @@ parts = [BASE]
|
||||
|
||||
# Fake some directory so that get_component works
|
||||
CORE.config_path = str(root)
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_FRAMEWORK: None}
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: None}
|
||||
|
||||
codeowners = defaultdict(list)
|
||||
|
||||
|
@ -85,12 +85,12 @@ def load_components():
|
||||
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from esphome.const import CONF_TYPE, KEY_CORE
|
||||
from esphome.const import CONF_TYPE, KEY_CORE, KEY_TARGET_PLATFORM
|
||||
from esphome.core import CORE
|
||||
|
||||
# pylint: enable=wrong-import-position
|
||||
|
||||
CORE.data[KEY_CORE] = {}
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: None}
|
||||
load_components()
|
||||
|
||||
# Import esphome after loading components (so schema is tracked)
|
||||
|
@ -58,7 +58,19 @@ file_types = (
|
||||
)
|
||||
cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc")
|
||||
py_include = ("*.py",)
|
||||
ignore_types = (".ico", ".png", ".woff", ".woff2", "", ".ttf", ".otf", ".pcf")
|
||||
ignore_types = (
|
||||
".ico",
|
||||
".png",
|
||||
".woff",
|
||||
".woff2",
|
||||
"",
|
||||
".ttf",
|
||||
".otf",
|
||||
".pcf",
|
||||
".apng",
|
||||
".gif",
|
||||
".webp",
|
||||
)
|
||||
|
||||
LINT_FILE_CHECKS = []
|
||||
LINT_CONTENT_CHECKS = []
|
||||
@ -669,8 +681,7 @@ def main():
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
global EXECUTABLE_BIT
|
||||
EXECUTABLE_BIT = git_ls_files()
|
||||
EXECUTABLE_BIT.update(git_ls_files())
|
||||
files = list(EXECUTABLE_BIT.keys())
|
||||
# Match against re
|
||||
file_name_re = re.compile("|".join(args.files))
|
||||
|
@ -6,7 +6,6 @@ light:
|
||||
rgb_order: GRB
|
||||
num_leds: 256
|
||||
pin: 2
|
||||
rmt_channel: 0
|
||||
|
||||
display:
|
||||
- platform: addressable_light
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user