mirror of
https://github.com/esphome/esphome.git
synced 2025-01-30 23:02:14 +01:00
Merge branch 'dev' into vornado-ir
This commit is contained in:
commit
67987bb718
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
|
- name: Build and push to ghcr by digest
|
||||||
id: build-ghcr
|
id: build-ghcr
|
||||||
uses: docker/build-push-action@v6.10.0
|
uses: docker/build-push-action@v6.11.0
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILD_SUMMARY: false
|
DOCKER_BUILD_SUMMARY: false
|
||||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||||
@ -72,7 +72,7 @@ runs:
|
|||||||
|
|
||||||
- name: Build and push to dockerhub by digest
|
- name: Build and push to dockerhub by digest
|
||||||
id: build-dockerhub
|
id: build-dockerhub
|
||||||
uses: docker/build-push-action@v6.10.0
|
uses: docker/build-push-action@v6.11.0
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILD_SUMMARY: false
|
DOCKER_BUILD_SUMMARY: false
|
||||||
DOCKER_BUILD_RECORD_UPLOAD: 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
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.8.0
|
uses: docker/setup-buildx-action@v3.8.0
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3.2.0
|
uses: docker/setup-qemu-action@v3.3.0
|
||||||
|
|
||||||
- name: Set TAG
|
- name: Set TAG
|
||||||
run: |
|
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
|
uses: docker/setup-buildx-action@v3.8.0
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
if: matrix.platform != 'linux/amd64'
|
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
|
- name: Log in to docker hub
|
||||||
uses: docker/login-action@v3.3.0
|
uses: docker/login-action@v3.3.0
|
||||||
@ -141,7 +141,7 @@ jobs:
|
|||||||
echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT
|
echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Upload digests
|
- name: Upload digests
|
||||||
uses: actions/upload-artifact@v4.5.0
|
uses: actions/upload-artifact@v4.6.0
|
||||||
with:
|
with:
|
||||||
name: digests-${{ steps.sanitize.outputs.name }}
|
name: digests-${{ steps.sanitize.outputs.name }}
|
||||||
path: /tmp/digests
|
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
|
python ./script/sync-device_class.py
|
||||||
|
|
||||||
- name: Commit changes
|
- name: Commit changes
|
||||||
uses: peter-evans/create-pull-request@v7.0.5
|
uses: peter-evans/create-pull-request@v7.0.6
|
||||||
with:
|
with:
|
||||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||||
committer: esphomebot <esphome@nabucasa.com>
|
committer: esphomebot <esphome@nabucasa.com>
|
||||||
|
@ -302,7 +302,7 @@ esphome/components/noblex/* @AGalfra
|
|||||||
esphome/components/npi19/* @bakerkj
|
esphome/components/npi19/* @bakerkj
|
||||||
esphome/components/number/* @esphome/core
|
esphome/components/number/* @esphome/core
|
||||||
esphome/components/one_wire/* @ssieb
|
esphome/components/one_wire/* @ssieb
|
||||||
esphome/components/online_image/* @guillempages
|
esphome/components/online_image/* @clydebarrow @guillempages
|
||||||
esphome/components/opentherm/* @olegtarasov
|
esphome/components/opentherm/* @olegtarasov
|
||||||
esphome/components/ota/* @esphome/core
|
esphome/components/ota/* @esphome/core
|
||||||
esphome/components/output/* @esphome/core
|
esphome/components/output/* @esphome/core
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
# ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/)
|
# ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/)
|
||||||
|
|
||||||
[![ESPHome Logo](https://esphome.io/_images/logo-text.png)](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/
|
**Documentation:** https://esphome.io/
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ RUN \
|
|||||||
# Use pinned versions so that we get updates with build caching
|
# Use pinned versions so that we get updates with build caching
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
python3-pip=23.0.1+dfsg-1 \
|
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-venv=3.11.2-1+b1 \
|
||||||
python3-wheel=0.38.4-2 \
|
python3-wheel=0.38.4-2 \
|
||||||
iputils-ping=3:20221126-1+deb12u1 \
|
iputils-ping=3:20221126-1+deb12u1 \
|
||||||
|
@ -1,28 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from esphome import automation, core
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.components.image as espImage
|
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
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import CONF_ID, CONF_REPEAT
|
||||||
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
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -30,6 +12,7 @@ AUTO_LOAD = ["image"]
|
|||||||
CODEOWNERS = ["@syndlex"]
|
CODEOWNERS = ["@syndlex"]
|
||||||
DEPENDENCIES = ["display"]
|
DEPENDENCIES = ["display"]
|
||||||
MULTI_CONF = True
|
MULTI_CONF = True
|
||||||
|
MULTI_CONF_NO_DEFAULT = True
|
||||||
|
|
||||||
CONF_LOOP = "loop"
|
CONF_LOOP = "loop"
|
||||||
CONF_START_FRAME = "start_frame"
|
CONF_START_FRAME = "start_frame"
|
||||||
@ -51,86 +34,19 @@ SetFrameAction = animation_ns.class_(
|
|||||||
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
|
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||||
)
|
)
|
||||||
|
|
||||||
TYPED_FILE_SCHEMA = cv.typed_schema(
|
CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
SOURCE_LOCAL: LOCAL_SCHEMA,
|
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
||||||
SOURCE_WEB: WEB_SCHEMA,
|
cv.Optional(CONF_LOOP): cv.All(
|
||||||
},
|
|
||||||
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,
|
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
|
||||||
CONF_URL: value,
|
cv.Optional(CONF_END_FRAME): cv.positive_int,
|
||||||
|
cv.Optional(CONF_REPEAT): cv.positive_int,
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
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(
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
cv.Optional(CONF_END_FRAME): cv.positive_int,
|
|
||||||
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(
|
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):
|
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(
|
var = cg.new_Pvariable(
|
||||||
config[CONF_ID],
|
config[CONF_ID],
|
||||||
prog_arr,
|
prog_arr,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
frames,
|
frame_count,
|
||||||
espImage.IMAGE_TYPE[config[CONF_TYPE]],
|
image_type,
|
||||||
|
trans_value,
|
||||||
)
|
)
|
||||||
cg.add(var.set_transparency(transparent))
|
|
||||||
if loop_config := config.get(CONF_LOOP):
|
if loop_config := config.get(CONF_LOOP):
|
||||||
start = loop_config[CONF_START_FRAME]
|
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)
|
count = loop_config.get(CONF_REPEAT, -1)
|
||||||
cg.add(var.set_loop(start, end, count))
|
cg.add(var.set_loop(start, end, count))
|
||||||
|
@ -6,8 +6,8 @@ namespace esphome {
|
|||||||
namespace animation {
|
namespace animation {
|
||||||
|
|
||||||
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
|
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
|
||||||
image::ImageType type)
|
image::ImageType type, image::Transparency transparent)
|
||||||
: Image(data_start, width, height, type),
|
: Image(data_start, width, height, type, transparent),
|
||||||
animation_data_start_(data_start),
|
animation_data_start_(data_start),
|
||||||
current_frame_(0),
|
current_frame_(0),
|
||||||
animation_frame_count_(animation_frame_count),
|
animation_frame_count_(animation_frame_count),
|
||||||
|
@ -8,7 +8,8 @@ namespace animation {
|
|||||||
|
|
||||||
class Animation : public image::Image {
|
class Animation : public image::Image {
|
||||||
public:
|
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;
|
uint32_t get_animation_frame_count() const;
|
||||||
int get_current_frame() const;
|
int get_current_frame() const;
|
||||||
|
@ -50,6 +50,10 @@ void DebugComponent::dump_config() {
|
|||||||
this->reset_reason_->publish_state(get_reset_reason_());
|
this->reset_reason_->publish_state(get_reset_reason_());
|
||||||
}
|
}
|
||||||
#endif // USE_TEXT_SENSOR
|
#endif // USE_TEXT_SENSOR
|
||||||
|
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
this->log_partition_info_(); // Log partition information for ESP32
|
||||||
|
#endif // USE_ESP32
|
||||||
}
|
}
|
||||||
|
|
||||||
void DebugComponent::loop() {
|
void DebugComponent::loop() {
|
||||||
|
@ -55,6 +55,20 @@ class DebugComponent : public PollingComponent {
|
|||||||
#endif // USE_ESP32
|
#endif // USE_ESP32
|
||||||
#endif // USE_SENSOR
|
#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
|
#ifdef USE_TEXT_SENSOR
|
||||||
text_sensor::TextSensor *device_info_{nullptr};
|
text_sensor::TextSensor *device_info_{nullptr};
|
||||||
text_sensor::TextSensor *reset_reason_{nullptr};
|
text_sensor::TextSensor *reset_reason_{nullptr};
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
#include <esp_heap_caps.h>
|
#include <esp_heap_caps.h>
|
||||||
#include <esp_system.h>
|
#include <esp_system.h>
|
||||||
#include <esp_chip_info.h>
|
#include <esp_chip_info.h>
|
||||||
|
#include <esp_partition.h>
|
||||||
|
|
||||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||||
#include <esp32/rom/rtc.h>
|
#include <esp32/rom/rtc.h>
|
||||||
@ -28,6 +29,19 @@ namespace debug {
|
|||||||
|
|
||||||
static const char *const TAG = "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 DebugComponent::get_reset_reason_() {
|
||||||
std::string reset_reason;
|
std::string reset_reason;
|
||||||
switch (esp_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 += " Cores:" + to_string(info.cores);
|
||||||
device_info += " Revision:" + to_string(info.revision);
|
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());
|
ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version());
|
||||||
device_info += "|ESP-IDF: ";
|
device_info += "|ESP-IDF: ";
|
||||||
device_info += esp_get_idf_version();
|
device_info += esp_get_idf_version();
|
||||||
|
@ -159,6 +159,15 @@ void DFPlayer::loop() {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 9: // End byte
|
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) {
|
if (byte != 0xEF) {
|
||||||
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
|
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
|
||||||
this->read_pos_ = 0;
|
this->read_pos_ = 0;
|
||||||
@ -238,13 +247,17 @@ void DFPlayer::loop() {
|
|||||||
this->ack_set_is_playing_ = false;
|
this->ack_set_is_playing_ = false;
|
||||||
this->ack_reset_is_playing_ = false;
|
this->ack_reset_is_playing_ = false;
|
||||||
break;
|
break;
|
||||||
|
case 0x3C:
|
||||||
|
ESP_LOGV(TAG, "Playback finished (USB drive)");
|
||||||
|
this->is_playing_ = false;
|
||||||
|
this->on_finished_playback_callback_.call();
|
||||||
case 0x3D:
|
case 0x3D:
|
||||||
ESP_LOGV(TAG, "Playback finished");
|
ESP_LOGV(TAG, "Playback finished (SD card)");
|
||||||
this->is_playing_ = false;
|
this->is_playing_ = false;
|
||||||
this->on_finished_playback_callback_.call();
|
this->on_finished_playback_callback_.call();
|
||||||
break;
|
break;
|
||||||
default:
|
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->sent_cmd_ = 0;
|
||||||
this->read_pos_ = 0;
|
this->read_pos_ = 0;
|
||||||
|
@ -58,7 +58,11 @@ uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
|
|||||||
#else
|
#else
|
||||||
uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); }
|
uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); }
|
||||||
#endif
|
#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
|
#ifdef USE_ESP_IDF
|
||||||
TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
|
|
||||||
from esphome import pins
|
from esphome import pins
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
@ -15,6 +16,9 @@ from esphome.const import (
|
|||||||
CONF_RMT_CHANNEL,
|
CONF_RMT_CHANNEL,
|
||||||
CONF_RMT_SYMBOLS,
|
CONF_RMT_SYMBOLS,
|
||||||
)
|
)
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CODEOWNERS = ["@jesserockz"]
|
CODEOWNERS = ["@jesserockz"]
|
||||||
DEPENDENCIES = ["esp32"]
|
DEPENDENCIES = ["esp32"]
|
||||||
@ -64,13 +68,53 @@ CONF_RESET_HIGH = "reset_high"
|
|||||||
CONF_RESET_LOW = "reset_low"
|
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):
|
def final_validation(config):
|
||||||
if not esp32_rmt.use_new_rmt_driver() and CONF_RMT_CHANNEL not in config:
|
if not esp32_rmt.use_new_rmt_driver():
|
||||||
raise cv.Invalid("rmt_channel is a required option.")
|
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
|
FINAL_VALIDATE_SCHEMA = final_validation
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
light.ADDRESSABLE_LIGHT_SCHEMA.extend(
|
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_NUM_LEDS): cv.positive_not_null_int,
|
||||||
cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
|
cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
|
||||||
cv.Optional(CONF_RMT_CHANNEL): cv.All(
|
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,
|
CONF_RMT_SYMBOLS,
|
||||||
esp32_idf=64,
|
esp32_idf=64,
|
||||||
esp32_s2_idf=64,
|
esp32_s2_idf=64,
|
||||||
@ -89,7 +133,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
esp32_c3_idf=48,
|
esp32_c3_idf=48,
|
||||||
esp32_c6_idf=48,
|
esp32_c6_idf=48,
|
||||||
esp32_h2_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_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
|
||||||
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
|
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
|
||||||
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
|
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
|
||||||
|
@ -6,7 +6,7 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import puremagic
|
from PIL import Image, UnidentifiedImageError
|
||||||
|
|
||||||
from esphome import core, external_files
|
from esphome import core, external_files
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
@ -29,21 +29,236 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
DOMAIN = "image"
|
DOMAIN = "image"
|
||||||
DEPENDENCIES = ["display"]
|
DEPENDENCIES = ["display"]
|
||||||
MULTI_CONF = True
|
|
||||||
MULTI_CONF_NO_DEFAULT = True
|
|
||||||
|
|
||||||
image_ns = cg.esphome_ns.namespace("image")
|
image_ns = cg.esphome_ns.namespace("image")
|
||||||
|
|
||||||
ImageType = image_ns.enum("ImageType")
|
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
|
||||||
|
|
||||||
|
def convert(self, image):
|
||||||
|
"""
|
||||||
|
Convert the image format
|
||||||
|
:param image: Input image
|
||||||
|
: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:
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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 = {
|
IMAGE_TYPE = {
|
||||||
"BINARY": ImageType.IMAGE_TYPE_BINARY,
|
"BINARY": ImageBinary,
|
||||||
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY,
|
"GRAYSCALE": ImageGrayscale,
|
||||||
"GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE,
|
"RGB565": ImageRGB565,
|
||||||
"RGB565": ImageType.IMAGE_TYPE_RGB565,
|
"RGB": ImageRGB,
|
||||||
"RGB24": ImageType.IMAGE_TYPE_RGB24,
|
"TRANSPARENT_BINARY": ReplaceWith(
|
||||||
"RGBA": ImageType.IMAGE_TYPE_RGBA,
|
"'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"
|
CONF_USE_TRANSPARENCY = "use_transparency"
|
||||||
|
|
||||||
# If the MDI file cannot be downloaded within this time, abort.
|
# If the MDI file cannot be downloaded within this time, abort.
|
||||||
@ -53,17 +268,11 @@ SOURCE_LOCAL = "local"
|
|||||||
SOURCE_MDI = "mdi"
|
SOURCE_MDI = "mdi"
|
||||||
SOURCE_WEB = "web"
|
SOURCE_WEB = "web"
|
||||||
|
|
||||||
|
|
||||||
Image_ = image_ns.class_("Image")
|
Image_ = image_ns.class_("Image")
|
||||||
|
|
||||||
|
|
||||||
def _compute_local_icon_path(value: dict) -> Path:
|
def compute_local_image_path(value) -> Path:
|
||||||
base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi"
|
url = value[CONF_URL] if isinstance(value, dict) else value
|
||||||
return base_dir / f"{value[CONF_ICON]}.svg"
|
|
||||||
|
|
||||||
|
|
||||||
def compute_local_image_path(value: dict) -> Path:
|
|
||||||
url = value[CONF_URL]
|
|
||||||
h = hashlib.new("sha256")
|
h = hashlib.new("sha256")
|
||||||
h.update(url.encode())
|
h.update(url.encode())
|
||||||
key = h.hexdigest()[:8]
|
key = h.hexdigest()[:8]
|
||||||
@ -71,30 +280,38 @@ def compute_local_image_path(value: dict) -> Path:
|
|||||||
return base_dir / key
|
return base_dir / key
|
||||||
|
|
||||||
|
|
||||||
def download_mdi(value):
|
def local_path(value):
|
||||||
validate_cairosvg_installed(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"
|
url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg"
|
||||||
|
return download_file(url, path)
|
||||||
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def download_image(value):
|
def download_image(value):
|
||||||
url = value[CONF_URL]
|
value = value[CONF_URL] if isinstance(value, dict) else value
|
||||||
path = compute_local_image_path(value)
|
return download_file(value, compute_local_image_path(value))
|
||||||
|
|
||||||
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def validate_cairosvg_installed(value):
|
def is_svg_file(file):
|
||||||
"""Validate that cairosvg is installed"""
|
if not file:
|
||||||
|
return False
|
||||||
|
with open(file, "rb") as f:
|
||||||
|
return "<svg " in str(f.read(1024))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_cairosvg_installed():
|
||||||
try:
|
try:
|
||||||
import cairosvg
|
import cairosvg
|
||||||
except ImportError as err:
|
except ImportError as err:
|
||||||
@ -110,73 +327,28 @@ def validate_cairosvg_installed(value):
|
|||||||
"(pip install -U cairosvg)"
|
"(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):
|
def validate_file_shorthand(value):
|
||||||
value = cv.string_strict(value)
|
value = cv.string_strict(value)
|
||||||
if value.startswith("mdi:"):
|
if value.startswith("mdi:"):
|
||||||
validate_cairosvg_installed(value)
|
|
||||||
|
|
||||||
match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value)
|
match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value)
|
||||||
if match is None:
|
if match is None:
|
||||||
raise cv.Invalid("Could not parse mdi icon name.")
|
raise cv.Invalid("Could not parse mdi icon name.")
|
||||||
icon = match.group(1)
|
icon = match.group(1)
|
||||||
return FILE_SCHEMA(
|
return download_mdi(icon)
|
||||||
{
|
|
||||||
CONF_SOURCE: SOURCE_MDI,
|
|
||||||
CONF_ICON: icon,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if value.startswith("http://") or value.startswith("https://"):
|
if value.startswith("http://") or value.startswith("https://"):
|
||||||
return FILE_SCHEMA(
|
return download_image(value)
|
||||||
{
|
|
||||||
CONF_SOURCE: SOURCE_WEB,
|
value = cv.file_(value)
|
||||||
CONF_URL: value,
|
return local_path(value)
|
||||||
}
|
|
||||||
)
|
|
||||||
return FILE_SCHEMA(
|
|
||||||
{
|
|
||||||
CONF_SOURCE: SOURCE_LOCAL,
|
|
||||||
CONF_PATH: value,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
LOCAL_SCHEMA = cv.Schema(
|
LOCAL_SCHEMA = cv.All(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_PATH): cv.file_,
|
cv.Required(CONF_PATH): cv.file_,
|
||||||
}
|
},
|
||||||
|
local_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
MDI_SCHEMA = cv.All(
|
MDI_SCHEMA = cv.All(
|
||||||
@ -203,205 +375,202 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _file_schema(value):
|
def validate_transparency(choices=TRANSPARENCY_TYPES):
|
||||||
if isinstance(value, str):
|
def validate(value):
|
||||||
return validate_file_shorthand(value)
|
if isinstance(value, bool):
|
||||||
return TYPED_FILE_SCHEMA(value)
|
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(
|
return validate
|
||||||
cv.All(
|
|
||||||
{
|
|
||||||
cv.Required(CONF_ID): cv.declare_id(Image_),
|
def validate_settings(value):
|
||||||
cv.Required(CONF_FILE): FILE_SCHEMA,
|
type = value[CONF_TYPE]
|
||||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
transparency = value[CONF_USE_TRANSPARENCY].lower()
|
||||||
# Not setting default here on purpose; the default depends on the source type
|
allow_config = IMAGE_TYPE[type].allow_config
|
||||||
# (file or mdi), and will be set in the "validate_cross_dependencies" validator.
|
if transparency not in allow_config:
|
||||||
cv.Optional(CONF_TYPE): cv.enum(IMAGE_TYPE, upper=True),
|
raise cv.Invalid(
|
||||||
# Not setting default here on purpose; the default depends on the image type,
|
f"Image format '{type}' cannot have transparency: {transparency}"
|
||||||
# and thus will be set in the "validate_cross_dependencies" validator.
|
)
|
||||||
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
|
invert_alpha = value.get(CONF_INVERT_ALPHA, False)
|
||||||
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
|
if (
|
||||||
"NONE", "FLOYDSTEINBERG", upper=True
|
invert_alpha
|
||||||
),
|
and transparency != CONF_ALPHA_CHANNEL
|
||||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
and CONF_INVERT_ALPHA not in allow_config
|
||||||
},
|
):
|
||||||
validate_cross_dependencies,
|
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): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
|
||||||
|
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
).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 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,)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_svg_image(file: bytes, resize: tuple[int, int]):
|
# The config schema can be a (possibly empty) single list of images,
|
||||||
# Local imports only to allow "validate_pillow_installed" to run *before* importing it
|
# or a dictionary of image types each with a list of images
|
||||||
# cairosvg is only needed in case of SVG images; adding it
|
CONFIG_SCHEMA = cv.Any(
|
||||||
# to the top would force configurations not using SVG to also have it
|
cv.Schema({cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}),
|
||||||
# installed for no reason.
|
cv.ensure_list(IMAGE_SCHEMA),
|
||||||
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,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
svg_image = svg2png(file)
|
|
||||||
|
|
||||||
return Image.open(io.BytesIO(svg_image))
|
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def write_image(config, all_frames=False):
|
||||||
# Local import only to allow "validate_pillow_installed" to run *before* importing it
|
path = Path(config[CONF_FILE])
|
||||||
from PIL import Image
|
if not path.is_file():
|
||||||
|
raise core.EsphomeError(f"Could not load image file {path}")
|
||||||
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)
|
|
||||||
|
|
||||||
resize = config.get(CONF_RESIZE)
|
resize = config.get(CONF_RESIZE)
|
||||||
if "svg" in file_type:
|
if is_svg_file(path):
|
||||||
image = load_svg_image(file_contents, resize)
|
# 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:
|
else:
|
||||||
image = Image.open(io.BytesIO(file_contents))
|
image = Image.open(path)
|
||||||
|
width, height = image.size
|
||||||
if resize:
|
if resize:
|
||||||
image.thumbnail(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)
|
||||||
|
|
||||||
width, height = image.size
|
if not resize and (width > 500 or height > 500):
|
||||||
|
|
||||||
if CONF_RESIZE not in config and (width > 500 or height > 500):
|
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
'The image "%s" you requested is very big. Please consider'
|
'The image "%s" you requested is very big. Please consider'
|
||||||
" using the resize parameter.",
|
" using the resize parameter.",
|
||||||
path,
|
path,
|
||||||
)
|
)
|
||||||
|
|
||||||
transparent = config[CONF_USE_TRANSPARENCY]
|
|
||||||
|
|
||||||
dither = (
|
dither = (
|
||||||
Image.Dither.NONE
|
Image.Dither.NONE
|
||||||
if config[CONF_DITHER] == "NONE"
|
if config[CONF_DITHER] == "NONE"
|
||||||
else Image.Dither.FLOYDSTEINBERG
|
else Image.Dither.FLOYDSTEINBERG
|
||||||
)
|
)
|
||||||
if config[CONF_TYPE] == "GRAYSCALE":
|
type = config[CONF_TYPE]
|
||||||
image = image.convert("LA", dither=dither)
|
transparency = config[CONF_USE_TRANSPARENCY]
|
||||||
pixels = list(image.getdata())
|
invert_alpha = config[CONF_INVERT_ALPHA]
|
||||||
data = [0 for _ in range(height * width)]
|
frame_count = 1
|
||||||
pos = 0
|
if all_frames:
|
||||||
for g, a in pixels:
|
try:
|
||||||
if transparent:
|
frame_count = image.n_frames
|
||||||
if g == 1:
|
except AttributeError:
|
||||||
g = 0
|
pass
|
||||||
if a < 0x80:
|
if frame_count <= 1:
|
||||||
g = 1
|
_LOGGER.warning("Image file %s has no animation frames", path)
|
||||||
|
|
||||||
data[pos] = g
|
total_rows = height * frame_count
|
||||||
pos += 1
|
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))).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":
|
rhs = [HexInt(x) for x in encoder.data]
|
||||||
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]
|
|
||||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||||
var = cg.new_Pvariable(
|
image_type = get_image_type_enum(type)
|
||||||
config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]]
|
trans_value = get_transparency_enum(transparency)
|
||||||
)
|
|
||||||
cg.add(var.set_transparency(transparent))
|
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
|
||||||
|
)
|
||||||
|
@ -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++) {
|
for (int img_y = 0; img_y < height_; img_y++) {
|
||||||
if (this->get_binary_pixel_(img_x, img_y)) {
|
if (this->get_binary_pixel_(img_x, img_y)) {
|
||||||
display->draw_pixel_at(x + img_x, y + img_y, color_on);
|
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);
|
display->draw_pixel_at(x + img_x, y + img_y, color_off);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,20 +39,10 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case IMAGE_TYPE_RGB24:
|
case IMAGE_TYPE_RGB:
|
||||||
for (int img_x = 0; img_x < width_; img_x++) {
|
for (int img_x = 0; img_x < width_; img_x++) {
|
||||||
for (int img_y = 0; img_y < height_; img_y++) {
|
for (int img_y = 0; img_y < height_; img_y++) {
|
||||||
auto color = this->get_rgb24_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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
if (color.w >= 0x80) {
|
if (color.w >= 0x80) {
|
||||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||||
}
|
}
|
||||||
@ -61,20 +51,20 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
|
|||||||
break;
|
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_)
|
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||||
return color_off;
|
return color_off;
|
||||||
switch (this->type_) {
|
switch (this->type_) {
|
||||||
case IMAGE_TYPE_BINARY:
|
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:
|
case IMAGE_TYPE_GRAYSCALE:
|
||||||
return this->get_grayscale_pixel_(x, y);
|
return this->get_grayscale_pixel_(x, y);
|
||||||
case IMAGE_TYPE_RGB565:
|
case IMAGE_TYPE_RGB565:
|
||||||
return this->get_rgb565_pixel_(x, y);
|
return this->get_rgb565_pixel_(x, y);
|
||||||
case IMAGE_TYPE_RGB24:
|
case IMAGE_TYPE_RGB:
|
||||||
return this->get_rgb24_pixel_(x, y);
|
return this->get_rgb_pixel_(x, y);
|
||||||
case IMAGE_TYPE_RGBA:
|
|
||||||
return this->get_rgba_pixel_(x, y);
|
|
||||||
default:
|
default:
|
||||||
return color_off;
|
return color_off;
|
||||||
}
|
}
|
||||||
@ -98,23 +88,40 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
|
|||||||
this->dsc_.header.cf = LV_IMG_CF_ALPHA_8BIT;
|
this->dsc_.header.cf = LV_IMG_CF_ALPHA_8BIT;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case IMAGE_TYPE_RGB24:
|
case IMAGE_TYPE_RGB:
|
||||||
this->dsc_.header.cf = LV_IMG_CF_RGB888;
|
#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;
|
break;
|
||||||
|
|
||||||
case IMAGE_TYPE_RGB565:
|
case IMAGE_TYPE_RGB565:
|
||||||
#if LV_COLOR_DEPTH == 16
|
#if LV_COLOR_DEPTH == 16
|
||||||
this->dsc_.header.cf = this->has_transparency() ? LV_IMG_CF_TRUE_COLOR_ALPHA : LV_IMG_CF_TRUE_COLOR;
|
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
|
#else
|
||||||
this->dsc_.header.cf = LV_IMG_CF_RGB565;
|
this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : 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
|
|
||||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
|
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -128,51 +135,73 @@ bool Image::get_binary_pixel_(int x, int y) const {
|
|||||||
const uint32_t pos = x + y * width_8;
|
const uint32_t pos = x + y * width_8;
|
||||||
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
|
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
|
||||||
}
|
}
|
||||||
Color Image::get_rgba_pixel_(int x, int y) const {
|
Color Image::get_rgb_pixel_(int x, int y) const {
|
||||||
const uint32_t pos = (x + y * this->width_) * 4;
|
const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8;
|
||||||
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 color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
|
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));
|
progmem_read_byte(this->data_start_ + pos + 2), 0xFF);
|
||||||
if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) {
|
|
||||||
// (0, 0, 1) has been defined as transparent color for non-alpha images.
|
switch (this->transparency_) {
|
||||||
// putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if)
|
case TRANSPARENCY_CHROMA_KEY:
|
||||||
color.w = 0;
|
if (color.g == 1 && color.r == 0 && color.b == 0) {
|
||||||
} else {
|
// (0, 1, 0) has been defined as transparent color for non-alpha images.
|
||||||
color.w = 0xFF;
|
color.w = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||||
|
color.w = progmem_read_byte(this->data_start_ + (pos + 3));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
Color Image::get_rgb565_pixel_(int x, int y) const {
|
Color Image::get_rgb565_pixel_(int x, int y) const {
|
||||||
const uint8_t *pos = this->data_start_;
|
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
|
||||||
if (this->transparent_) {
|
|
||||||
pos += (x + y * this->width_) * 3;
|
|
||||||
} else {
|
|
||||||
pos += (x + y * this->width_) * 2;
|
|
||||||
}
|
|
||||||
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
|
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
|
||||||
auto r = (rgb565 & 0xF800) >> 11;
|
auto r = (rgb565 & 0xF800) >> 11;
|
||||||
auto g = (rgb565 & 0x07E0) >> 5;
|
auto g = (rgb565 & 0x07E0) >> 5;
|
||||||
auto b = rgb565 & 0x001F;
|
auto b = rgb565 & 0x001F;
|
||||||
auto a = this->transparent_ ? progmem_read_byte(pos + 2) : 0xFF;
|
auto a = 0xFF;
|
||||||
Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a);
|
switch (this->transparency_) {
|
||||||
return color;
|
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 {
|
Color Image::get_grayscale_pixel_(int x, int y) const {
|
||||||
const uint32_t pos = (x + y * this->width_);
|
const uint32_t pos = (x + y * this->width_);
|
||||||
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
|
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
|
||||||
uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF;
|
uint8_t alpha = (gray == 1 && this->transparency_ == TRANSPARENCY_CHROMA_KEY) ? 0 : 0xFF;
|
||||||
return Color(gray, gray, gray, alpha);
|
return Color(gray, gray, gray, alpha);
|
||||||
}
|
}
|
||||||
int Image::get_width() const { return this->width_; }
|
int Image::get_width() const { return this->width_; }
|
||||||
int Image::get_height() const { return this->height_; }
|
int Image::get_height() const { return this->height_; }
|
||||||
ImageType Image::get_type() const { return this->type_; }
|
ImageType Image::get_type() const { return this->type_; }
|
||||||
Image::Image(const uint8_t *data_start, int width, int height, ImageType type)
|
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) {}
|
: 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 image
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
@ -12,51 +12,40 @@ namespace image {
|
|||||||
enum ImageType {
|
enum ImageType {
|
||||||
IMAGE_TYPE_BINARY = 0,
|
IMAGE_TYPE_BINARY = 0,
|
||||||
IMAGE_TYPE_GRAYSCALE = 1,
|
IMAGE_TYPE_GRAYSCALE = 1,
|
||||||
IMAGE_TYPE_RGB24 = 2,
|
IMAGE_TYPE_RGB = 2,
|
||||||
IMAGE_TYPE_RGB565 = 3,
|
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 {
|
class Image : public display::BaseImage {
|
||||||
public:
|
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;
|
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_width() const override;
|
||||||
int get_height() const override;
|
int get_height() const override;
|
||||||
const uint8_t *get_data_start() const { return this->data_start_; }
|
const uint8_t *get_data_start() const { return this->data_start_; }
|
||||||
ImageType get_type() const;
|
ImageType get_type() const;
|
||||||
|
|
||||||
int get_bpp() const {
|
int get_bpp() const { return this->bpp_; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the stride of the image in bytes, that is, the distance in bytes
|
/// Return the stride of the image in bytes, that is, the distance in bytes
|
||||||
/// between two consecutive rows of pixels.
|
/// 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 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 this->transparency_ != TRANSPARENCY_OPAQUE; }
|
||||||
bool has_transparency() const { return transparent_; }
|
|
||||||
|
|
||||||
#ifdef USE_LVGL
|
#ifdef USE_LVGL
|
||||||
lv_img_dsc_t *get_lv_img_dsc();
|
lv_img_dsc_t *get_lv_img_dsc();
|
||||||
#endif
|
#endif
|
||||||
protected:
|
protected:
|
||||||
bool get_binary_pixel_(int x, int y) const;
|
bool get_binary_pixel_(int x, int y) const;
|
||||||
Color get_rgb24_pixel_(int x, int y) const;
|
Color get_rgb_pixel_(int x, int y) const;
|
||||||
Color get_rgba_pixel_(int x, int y) const;
|
|
||||||
Color get_rgb565_pixel_(int x, int y) const;
|
Color get_rgb565_pixel_(int x, int y) const;
|
||||||
Color get_grayscale_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_;
|
int height_;
|
||||||
ImageType type_;
|
ImageType type_;
|
||||||
const uint8_t *data_start_;
|
const uint8_t *data_start_;
|
||||||
bool transparent_;
|
Transparency transparency_;
|
||||||
|
size_t bpp_{};
|
||||||
|
size_t stride_{};
|
||||||
#ifdef USE_LVGL
|
#ifdef USE_LVGL
|
||||||
lv_img_dsc_t dsc_{};
|
lv_img_dsc_t dsc_{};
|
||||||
#endif
|
#endif
|
||||||
|
@ -17,11 +17,11 @@ std::string build_json(const json_build_t &f) {
|
|||||||
auto free_heap = ALLOCATOR.get_max_free_block_size();
|
auto free_heap = ALLOCATOR.get_max_free_block_size();
|
||||||
size_t request_size = std::min(free_heap, (size_t) 512);
|
size_t request_size = std::min(free_heap, (size_t) 512);
|
||||||
while (true) {
|
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);
|
DynamicJsonDocument json_document(request_size);
|
||||||
if (json_document.capacity() == 0) {
|
if (json_document.capacity() == 0) {
|
||||||
ESP_LOGE(TAG,
|
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);
|
request_size, free_heap);
|
||||||
return "{}";
|
return "{}";
|
||||||
}
|
}
|
||||||
@ -29,7 +29,7 @@ std::string build_json(const json_build_t &f) {
|
|||||||
f(root);
|
f(root);
|
||||||
if (json_document.overflowed()) {
|
if (json_document.overflowed()) {
|
||||||
if (request_size == free_heap) {
|
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);
|
free_heap);
|
||||||
return "{}";
|
return "{}";
|
||||||
}
|
}
|
||||||
@ -37,7 +37,7 @@ std::string build_json(const json_build_t &f) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
json_document.shrinkToFit();
|
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;
|
std::string output;
|
||||||
serializeJson(json_document, output);
|
serializeJson(json_document, output);
|
||||||
return 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());
|
ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", to_save.key.c_str());
|
||||||
return true;
|
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);
|
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);
|
size_t actual_len = fdb_kv_get_blob(db, to_save.key.c_str(), &blob);
|
||||||
if (actual_len != kv.value_len) {
|
if (actual_len != kv.value_len) {
|
||||||
|
@ -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]:
|
if config[CONF_USE_ABBREVIATIONS]:
|
||||||
cg.add_define("USE_MQTT_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_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); }
|
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_; }
|
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_; }
|
const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; }
|
||||||
void MQTTClientComponent::set_publish_nan_as_none(bool publish_nan_as_none) {
|
void MQTTClientComponent::set_publish_nan_as_none(bool publish_nan_as_none) {
|
||||||
this->publish_nan_as_none_ = 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.
|
* @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
|
/// Get the topic prefix of this device, using default if necessary
|
||||||
const std::string &get_topic_prefix() const;
|
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...> {
|
template<typename... Ts> class NextionPublishFloatAction : public Action<Ts...> {
|
||||||
public:
|
public:
|
||||||
explicit NextionPublishFloatAction(NextionComponent *component) : component_(component) {}
|
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 import automation
|
||||||
from esphome.components import display, uart
|
import esphome.codegen as cg
|
||||||
from esphome.components import esp32
|
from esphome.components import display, esp32, uart
|
||||||
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
|
CONF_BRIGHTNESS,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_LAMBDA,
|
CONF_LAMBDA,
|
||||||
CONF_BRIGHTNESS,
|
|
||||||
CONF_TRIGGER_ID,
|
|
||||||
CONF_ON_TOUCH,
|
CONF_ON_TOUCH,
|
||||||
|
CONF_TRIGGER_ID,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
|
|
||||||
from . import Nextion, nextion_ns, nextion_ref
|
from . import Nextion, nextion_ns, nextion_ref
|
||||||
from .base_component import (
|
from .base_component import (
|
||||||
|
CONF_AUTO_WAKE_ON_TOUCH,
|
||||||
|
CONF_EXIT_REPARSE_ON_START,
|
||||||
CONF_ON_BUFFER_OVERFLOW,
|
CONF_ON_BUFFER_OVERFLOW,
|
||||||
|
CONF_ON_PAGE,
|
||||||
|
CONF_ON_SETUP,
|
||||||
CONF_ON_SLEEP,
|
CONF_ON_SLEEP,
|
||||||
CONF_ON_WAKE,
|
CONF_ON_WAKE,
|
||||||
CONF_ON_SETUP,
|
CONF_SKIP_CONNECTION_HANDSHAKE,
|
||||||
CONF_ON_PAGE,
|
CONF_START_UP_PAGE,
|
||||||
CONF_TFT_URL,
|
CONF_TFT_URL,
|
||||||
CONF_TOUCH_SLEEP_TIMEOUT,
|
CONF_TOUCH_SLEEP_TIMEOUT,
|
||||||
CONF_WAKE_UP_PAGE,
|
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"]
|
CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"]
|
||||||
@ -32,6 +32,9 @@ CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"]
|
|||||||
DEPENDENCIES = ["uart"]
|
DEPENDENCIES = ["uart"]
|
||||||
AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"]
|
AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"]
|
||||||
|
|
||||||
|
NextionSetBrightnessAction = nextion_ns.class_(
|
||||||
|
"NextionSetBrightnessAction", automation.Action
|
||||||
|
)
|
||||||
SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template())
|
SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template())
|
||||||
SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template())
|
SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template())
|
||||||
WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template())
|
WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template())
|
||||||
@ -46,7 +49,7 @@ CONFIG_SCHEMA = (
|
|||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(Nextion),
|
cv.GenerateID(): cv.declare_id(Nextion),
|
||||||
cv.Optional(CONF_TFT_URL): cv.url,
|
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.Optional(CONF_ON_SETUP): automation.validate_automation(
|
||||||
{
|
{
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SetupTrigger),
|
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):
|
async def to_code(config):
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await uart.register_uart_device(var, config)
|
await uart.register_uart_device(var, config)
|
||||||
|
|
||||||
if CONF_BRIGHTNESS in config:
|
if CONF_BRIGHTNESS in config:
|
||||||
cg.add(var.set_brightness(config[CONF_BRIGHTNESS]))
|
cg.add(var.set_brightness(config[CONF_BRIGHTNESS]))
|
||||||
|
|
||||||
if CONF_LAMBDA in config:
|
if CONF_LAMBDA in config:
|
||||||
lambda_ = await cg.process_lambda(
|
lambda_ = await cg.process_lambda(
|
||||||
config[CONF_LAMBDA], [(nextion_ref, "it")], return_type=cg.void
|
config[CONF_LAMBDA], [(nextion_ref, "it")], return_type=cg.void
|
||||||
|
@ -273,7 +273,9 @@ void Nextion::loop() {
|
|||||||
this->sent_setup_commands_ = true;
|
this->sent_setup_commands_ = true;
|
||||||
this->send_command_("bkcmd=3"); // Always, returns 0x00 to 0x23 result of serial command.
|
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
|
// Check if a startup page has been set and send the command
|
||||||
if (this->start_up_page_ != -1) {
|
if (this->start_up_page_ != -1) {
|
||||||
|
@ -1339,7 +1339,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
|||||||
CallbackManager<void()> buffer_overflow_callback_{};
|
CallbackManager<void()> buffer_overflow_callback_{};
|
||||||
|
|
||||||
optional<nextion_writer_t> writer_;
|
optional<nextion_writer_t> writer_;
|
||||||
float brightness_{1.0};
|
optional<float> brightness_;
|
||||||
|
|
||||||
std::string device_model_;
|
std::string device_model_;
|
||||||
std::string firmware_version_;
|
std::string firmware_version_;
|
||||||
|
@ -4,14 +4,18 @@ from esphome import automation
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
|
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
|
||||||
from esphome.components.image import (
|
from esphome.components.image import (
|
||||||
|
CONF_INVERT_ALPHA,
|
||||||
CONF_USE_TRANSPARENCY,
|
CONF_USE_TRANSPARENCY,
|
||||||
IMAGE_TYPE,
|
IMAGE_SCHEMA,
|
||||||
Image_,
|
Image_,
|
||||||
validate_cross_dependencies,
|
get_image_type_enum,
|
||||||
|
get_transparency_enum,
|
||||||
)
|
)
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_BUFFER_SIZE,
|
CONF_BUFFER_SIZE,
|
||||||
|
CONF_DITHER,
|
||||||
|
CONF_FILE,
|
||||||
CONF_FORMAT,
|
CONF_FORMAT,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_ON_ERROR,
|
CONF_ON_ERROR,
|
||||||
@ -23,7 +27,7 @@ from esphome.const import (
|
|||||||
|
|
||||||
AUTO_LOAD = ["image"]
|
AUTO_LOAD = ["image"]
|
||||||
DEPENDENCIES = ["display", "http_request"]
|
DEPENDENCIES = ["display", "http_request"]
|
||||||
CODEOWNERS = ["@guillempages"]
|
CODEOWNERS = ["@guillempages", "@clydebarrow"]
|
||||||
MULTI_CONF = True
|
MULTI_CONF = True
|
||||||
|
|
||||||
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
|
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")
|
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_)
|
OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_)
|
||||||
|
|
||||||
@ -57,48 +82,54 @@ DownloadErrorTrigger = online_image_ns.class_(
|
|||||||
"DownloadErrorTrigger", automation.Trigger.template()
|
"DownloadErrorTrigger", automation.Trigger.template()
|
||||||
)
|
)
|
||||||
|
|
||||||
ONLINE_IMAGE_SCHEMA = cv.Schema(
|
|
||||||
{
|
def remove_options(*options):
|
||||||
cv.Required(CONF_ID): cv.declare_id(OnlineImage),
|
return {
|
||||||
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
|
cv.Optional(option): cv.invalid(
|
||||||
#
|
f"{option} is an invalid option for online_image"
|
||||||
# Common image options
|
)
|
||||||
#
|
for option in 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.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.Optional(CONF_ON_ERROR): automation.validate_automation(
|
|
||||||
{
|
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
).extend(cv.polling_component_schema("never"))
|
|
||||||
|
|
||||||
|
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),
|
||||||
|
# Online Image specific options
|
||||||
|
cv.Required(CONF_URL): cv.url,
|
||||||
|
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.Optional(CONF_ON_ERROR): automation.validate_automation(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.extend(cv.polling_component_schema("never"))
|
||||||
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.Schema(
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
cv.All(
|
cv.All(
|
||||||
ONLINE_IMAGE_SCHEMA,
|
ONLINE_IMAGE_SCHEMA,
|
||||||
validate_cross_dependencies,
|
|
||||||
cv.require_framework_version(
|
cv.require_framework_version(
|
||||||
# esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed
|
# esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed
|
||||||
# esp8266_arduino=cv.Version(2, 7, 0),
|
# esp8266_arduino=cv.Version(2, 7, 0),
|
||||||
esp32_arduino=cv.Version(0, 0, 0),
|
esp32_arduino=cv.Version(0, 0, 0),
|
||||||
esp_idf=cv.Version(4, 0, 0),
|
esp_idf=cv.Version(4, 0, 0),
|
||||||
rp2040_arduino=cv.Version(0, 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):
|
async def to_code(config):
|
||||||
format = config[CONF_FORMAT]
|
image_format = IMAGE_FORMATS[config[CONF_FORMAT]]
|
||||||
if format in [FORMAT_PNG]:
|
image_format.actions()
|
||||||
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
|
|
||||||
cg.add_library("pngle", "1.0.2")
|
|
||||||
|
|
||||||
url = config[CONF_URL]
|
url = config[CONF_URL]
|
||||||
width, height = config.get(CONF_RESIZE, (0, 0))
|
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(
|
var = cg.new_Pvariable(
|
||||||
config[CONF_ID],
|
config[CONF_ID],
|
||||||
url,
|
url,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
format,
|
image_format.enum,
|
||||||
config[CONF_TYPE],
|
get_image_type_enum(config[CONF_TYPE]),
|
||||||
|
transparent,
|
||||||
config[CONF_BUFFER_SIZE],
|
config[CONF_BUFFER_SIZE],
|
||||||
)
|
)
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
|
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
|
||||||
|
|
||||||
cg.add(var.set_transparency(transparent))
|
|
||||||
|
|
||||||
if placeholder_id := config.get(CONF_PLACEHOLDER):
|
if placeholder_id := config.get(CONF_PLACEHOLDER):
|
||||||
placeholder = await cg.get_variable(placeholder_id)
|
placeholder = await cg.get_variable(placeholder_id)
|
||||||
cg.add(var.set_placeholder(placeholder))
|
cg.add(var.set_placeholder(placeholder))
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "esphome/core/defines.h"
|
|
||||||
#include "esphome/core/color.h"
|
#include "esphome/core/color.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@ -23,7 +22,7 @@ class ImageDecoder {
|
|||||||
/**
|
/**
|
||||||
* @brief Initialize the decoder.
|
* @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; }
|
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
|
* @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.
|
* 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.
|
* @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);
|
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.
|
* 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.
|
* 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.
|
* 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 y The top-most coordinate of the rectangle.
|
||||||
* @param w The width of the rectangle.
|
* @param w The width of the rectangle.
|
||||||
* @param h The height 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);
|
void draw(int x, int y, int w, int h, const Color &color);
|
||||||
|
|
||||||
@ -67,7 +66,7 @@ class ImageDecoder {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
OnlineImage *image_;
|
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.
|
// Will be overwritten anyway once the download size is known.
|
||||||
uint32_t download_size_ = 1;
|
uint32_t download_size_ = 1;
|
||||||
uint32_t decoded_bytes_ = 0;
|
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,
|
OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
|
||||||
uint32_t download_buffer_size)
|
image::Transparency transparency, uint32_t download_buffer_size)
|
||||||
: Image(nullptr, 0, 0, type),
|
: Image(nullptr, 0, 0, type, transparency),
|
||||||
buffer_(nullptr),
|
buffer_(nullptr),
|
||||||
download_buffer_(download_buffer_size),
|
download_buffer_(download_buffer_size),
|
||||||
format_(format),
|
format_(format),
|
||||||
@ -45,7 +45,7 @@ void OnlineImage::draw(int x, int y, display::Display *display, Color color_on,
|
|||||||
|
|
||||||
void OnlineImage::release() {
|
void OnlineImage::release() {
|
||||||
if (this->buffer_) {
|
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->allocator_.deallocate(this->buffer_, this->get_buffer_size_());
|
||||||
this->data_start_ = nullptr;
|
this->data_start_ = nullptr;
|
||||||
this->buffer_ = nullptr;
|
this->buffer_ = nullptr;
|
||||||
@ -70,20 +70,19 @@ bool OnlineImage::resize_(int width_in, int height_in) {
|
|||||||
if (this->buffer_) {
|
if (this->buffer_) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto new_size = this->get_buffer_size_(width, height);
|
size_t new_size = this->get_buffer_size_(width, height);
|
||||||
ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size);
|
ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size);
|
||||||
delay_microseconds_safe(2000);
|
|
||||||
this->buffer_ = this->allocator_.allocate(new_size);
|
this->buffer_ = this->allocator_.allocate(new_size);
|
||||||
if (this->buffer_) {
|
if (this->buffer_ == nullptr) {
|
||||||
this->buffer_width_ = width;
|
ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size,
|
||||||
this->buffer_height_ = height;
|
this->allocator_.get_max_free_block_size());
|
||||||
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());
|
|
||||||
this->end_connection_();
|
this->end_connection_();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
this->buffer_width_ = width;
|
||||||
|
this->buffer_height_ = height;
|
||||||
|
this->width_ = width;
|
||||||
|
ESP_LOGV(TAG, "New size: (%d, %d)", width, height);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,9 +90,8 @@ void OnlineImage::update() {
|
|||||||
if (this->decoder_) {
|
if (this->decoder_) {
|
||||||
ESP_LOGW(TAG, "Image already being updated.");
|
ESP_LOGW(TAG, "Image already being updated.");
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
ESP_LOGI(TAG, "Updating image");
|
|
||||||
}
|
}
|
||||||
|
ESP_LOGI(TAG, "Updating image %s", this->url_.c_str());
|
||||||
|
|
||||||
this->downloader_ = this->parent_->get(this->url_);
|
this->downloader_ = this->parent_->get(this->url_);
|
||||||
|
|
||||||
@ -142,10 +140,11 @@ void OnlineImage::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this->downloader_ || this->decoder_->is_finished()) {
|
if (!this->downloader_ || this->decoder_->is_finished()) {
|
||||||
ESP_LOGD(TAG, "Image fully downloaded");
|
|
||||||
this->data_start_ = buffer_;
|
this->data_start_ = buffer_;
|
||||||
this->width_ = buffer_width_;
|
this->width_ = buffer_width_;
|
||||||
this->height_ = buffer_height_;
|
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->end_connection_();
|
||||||
this->download_finished_callback_.call();
|
this->download_finished_callback_.call();
|
||||||
return;
|
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) {
|
void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
||||||
if (!this->buffer_) {
|
if (!this->buffer_) {
|
||||||
ESP_LOGE(TAG, "Buffer not allocated!");
|
ESP_LOGE(TAG, "Buffer not allocated!");
|
||||||
@ -184,57 +196,53 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
|||||||
switch (this->type_) {
|
switch (this->type_) {
|
||||||
case ImageType::IMAGE_TYPE_BINARY: {
|
case ImageType::IMAGE_TYPE_BINARY: {
|
||||||
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
|
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
|
||||||
const uint32_t pos = x + y * width_8;
|
pos = x + y * width_8;
|
||||||
if ((this->has_transparency() && color.w > 127) || is_color_on(color)) {
|
auto bitno = 0x80 >> (pos % 8u);
|
||||||
this->buffer_[pos / 8u] |= (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 {
|
} else {
|
||||||
this->buffer_[pos / 8u] &= ~(0x80 >> (pos % 8u));
|
this->buffer_[pos] &= ~bitno;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ImageType::IMAGE_TYPE_GRAYSCALE: {
|
case ImageType::IMAGE_TYPE_GRAYSCALE: {
|
||||||
uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
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) {
|
if (gray == 1) {
|
||||||
gray = 0;
|
gray = 0;
|
||||||
}
|
}
|
||||||
if (color.w < 0x80) {
|
if (color.w < 0x80) {
|
||||||
gray = 1;
|
gray = 1;
|
||||||
}
|
}
|
||||||
|
} else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||||
|
if (color.w != 0xFF)
|
||||||
|
gray = color.w;
|
||||||
}
|
}
|
||||||
this->buffer_[pos] = gray;
|
this->buffer_[pos] = gray;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ImageType::IMAGE_TYPE_RGB565: {
|
case ImageType::IMAGE_TYPE_RGB565: {
|
||||||
|
this->map_chroma_key(color);
|
||||||
uint16_t col565 = display::ColorUtil::color_to_565(color);
|
uint16_t col565 = display::ColorUtil::color_to_565(color);
|
||||||
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
||||||
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 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;
|
this->buffer_[pos + 2] = color.w;
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ImageType::IMAGE_TYPE_RGBA: {
|
|
||||||
this->buffer_[pos + 0] = color.r;
|
|
||||||
this->buffer_[pos + 1] = color.g;
|
|
||||||
this->buffer_[pos + 2] = color.b;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ImageType::IMAGE_TYPE_RGB: {
|
||||||
|
this->map_chroma_key(color);
|
||||||
this->buffer_[pos + 0] = color.r;
|
this->buffer_[pos + 0] = color.r;
|
||||||
this->buffer_[pos + 1] = color.g;
|
this->buffer_[pos + 1] = color.g;
|
||||||
this->buffer_[pos + 2] = color.b;
|
this->buffer_[pos + 2] = color.b;
|
||||||
|
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||||
|
this->buffer_[pos + 3] = color.w;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,12 +48,13 @@ class OnlineImage : public PollingComponent,
|
|||||||
* @param buffer_size Size of the buffer used to download the image.
|
* @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,
|
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 draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
||||||
|
|
||||||
void update() override;
|
void update() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
void map_chroma_key(Color &color);
|
||||||
|
|
||||||
/** Set the URL to download the image from. */
|
/** Set the URL to download the image from. */
|
||||||
void set_url(const std::string &url) {
|
void set_url(const std::string &url) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "image_decoder.h"
|
#include "image_decoder.h"
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||||
#include <pngle.h>
|
#include <pngle.h>
|
||||||
|
|
||||||
|
@ -21,7 +21,14 @@ void PsramComponent::dump_config() {
|
|||||||
ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available));
|
ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available));
|
||||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0)
|
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0)
|
||||||
if (available) {
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -2425,28 +2425,21 @@ void WaveshareEPaper7P5InBV3BWR::init_display_() {
|
|||||||
this->command(0x01);
|
this->command(0x01);
|
||||||
|
|
||||||
// 1-0=11: internal power
|
// 1-0=11: internal power
|
||||||
this->data(0x07);
|
this->data(0x07); // VRS_EN=1, VS_EN=1, VG_EN=1
|
||||||
this->data(0x17); // VGH&VGL
|
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
|
this->data(0x3F); // VSH=15V?
|
||||||
this->data(0x26); // VSL
|
this->data(0x26); // VSL=-9.4V?
|
||||||
this->data(0x11); // VSHR
|
this->data(0x11); // VSHR=5.8V?
|
||||||
|
|
||||||
// VCOM DC Setting
|
// VCOM DC Setting
|
||||||
this->command(0x82);
|
this->command(0x82);
|
||||||
this->data(0x24); // VCOM
|
this->data(0x24); // VCOM=-1.9V
|
||||||
|
|
||||||
// Booster Setting
|
|
||||||
this->command(0x06);
|
|
||||||
this->data(0x27);
|
|
||||||
this->data(0x27);
|
|
||||||
this->data(0x2F);
|
|
||||||
this->data(0x17);
|
|
||||||
|
|
||||||
// POWER ON
|
// POWER ON
|
||||||
this->command(0x04);
|
this->command(0x04);
|
||||||
|
|
||||||
delay(100); // NOLINT
|
delay(100); // NOLINT
|
||||||
this->wait_until_idle_();
|
this->wait_until_idle_();
|
||||||
|
|
||||||
// COMMAND PANEL SETTING
|
// COMMAND PANEL SETTING
|
||||||
this->command(0x00);
|
this->command(0x00);
|
||||||
this->data(0x0F); // KW-3f KWR-2F BWROTP 0f BWOTP 1f
|
this->data(0x0F); // KW-3f KWR-2F BWROTP 0f BWOTP 1f
|
||||||
@ -2457,16 +2450,16 @@ void WaveshareEPaper7P5InBV3BWR::init_display_() {
|
|||||||
this->data(0x20);
|
this->data(0x20);
|
||||||
this->data(0x01); // gate 480
|
this->data(0x01); // gate 480
|
||||||
this->data(0xE0);
|
this->data(0xE0);
|
||||||
// COMMAND ...?
|
|
||||||
this->command(0x15);
|
|
||||||
this->data(0x00);
|
|
||||||
// COMMAND VCOM AND DATA INTERVAL SETTING
|
// COMMAND VCOM AND DATA INTERVAL SETTING
|
||||||
this->command(0x50);
|
this->command(0x50);
|
||||||
this->data(0x20);
|
this->data(0x20);
|
||||||
this->data(0x00);
|
this->data(0x00);
|
||||||
|
|
||||||
// COMMAND TCON SETTING
|
// COMMAND TCON SETTING
|
||||||
this->command(0x60);
|
this->command(0x60);
|
||||||
this->data(0x22);
|
this->data(0x22);
|
||||||
|
|
||||||
// Resolution setting
|
// Resolution setting
|
||||||
this->command(0x65);
|
this->command(0x65);
|
||||||
this->data(0x00);
|
this->data(0x00);
|
||||||
|
@ -1415,6 +1415,30 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
|
|||||||
request->send(200, "application/json", data.c_str());
|
request->send(200, "application/json", data.c_str());
|
||||||
return;
|
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);
|
request->send(404);
|
||||||
}
|
}
|
||||||
@ -1664,7 +1688,7 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ALARM_CONTROL_PANEL
|
#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;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -11,10 +11,19 @@
|
|||||||
#ifdef USE_WIFI_WPA2_EAP
|
#ifdef USE_WIFI_WPA2_EAP
|
||||||
#include <esp_wpa2.h>
|
#include <esp_wpa2.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_WIFI_AP
|
||||||
|
#include "dhcpserver/dhcpserver.h"
|
||||||
|
#endif // USE_WIFI_AP
|
||||||
|
|
||||||
#include "lwip/apps/sntp.h"
|
#include "lwip/apps/sntp.h"
|
||||||
#include "lwip/dns.h"
|
#include "lwip/dns.h"
|
||||||
#include "lwip/err.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/application.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/helpers.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()) {
|
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,
|
// 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.
|
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
|
||||||
// https://github.com/esphome/issues/issues/2299
|
// https://github.com/esphome/issues/issues/2299
|
||||||
sntp_servermode_dhcp(false);
|
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
|
// No manual IP is set; use DHCP client
|
||||||
if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
|
if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
|
||||||
err = esp_netif_dhcpc_start(s_sta_netif);
|
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_() {
|
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) {
|
if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) {
|
||||||
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
|
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#include "esphome/core/preferences.h"
|
#include "esphome/core/preferences.h"
|
||||||
|
|
||||||
|
@ -45,7 +45,9 @@
|
|||||||
#endif
|
#endif
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
#include "esp32/rom/crc.h"
|
#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.h"
|
||||||
#include "esp_efuse_table.h"
|
#include "esp_efuse_table.h"
|
||||||
#endif
|
#endif
|
||||||
@ -261,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) {
|
bool str_equals_case_insensitive(const std::string &a, const std::string &b) {
|
||||||
return strcasecmp(a.c_str(), b.c_str()) == 0;
|
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_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); }
|
bool str_endswith(const std::string &str, const std::string &end) { return str.ends_with(end); }
|
||||||
#else
|
#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;
|
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);
|
uint8_t crc8(const uint8_t *data, uint8_t len);
|
||||||
|
|
||||||
/// Calculate a CRC-16 checksum of \p data with size \p len.
|
/// Calculate a CRC-16 checksum of \p data with size \p len.
|
||||||
|
@ -58,7 +58,19 @@ file_types = (
|
|||||||
)
|
)
|
||||||
cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc")
|
cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc")
|
||||||
py_include = ("*.py",)
|
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_FILE_CHECKS = []
|
||||||
LINT_CONTENT_CHECKS = []
|
LINT_CONTENT_CHECKS = []
|
||||||
@ -669,8 +681,7 @@ def main():
|
|||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
global EXECUTABLE_BIT
|
EXECUTABLE_BIT.update(git_ls_files())
|
||||||
EXECUTABLE_BIT = git_ls_files()
|
|
||||||
files = list(EXECUTABLE_BIT.keys())
|
files = list(EXECUTABLE_BIT.keys())
|
||||||
# Match against re
|
# Match against re
|
||||||
file_name_re = re.compile("|".join(args.files))
|
file_name_re = re.compile("|".join(args.files))
|
||||||
|
4
tests/components/animation/.gitattributes
vendored
Normal file
4
tests/components/animation/.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
*.apng -text
|
||||||
|
*.webp -text
|
||||||
|
*.gif -text
|
||||||
|
|
BIN
tests/components/animation/anim.apng
Normal file
BIN
tests/components/animation/anim.apng
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
tests/components/animation/anim.gif
Normal file
BIN
tests/components/animation/anim.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
BIN
tests/components/animation/anim.webp
Normal file
BIN
tests/components/animation/anim.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
23
tests/components/animation/common.yaml
Normal file
23
tests/components/animation/common.yaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
animation:
|
||||||
|
- id: rgb565_animation
|
||||||
|
file: $component_dir/anim.gif
|
||||||
|
type: RGB565
|
||||||
|
use_transparency: opaque
|
||||||
|
resize: 50x50
|
||||||
|
- id: rgb_animation
|
||||||
|
file: $component_dir/anim.apng
|
||||||
|
type: RGB
|
||||||
|
use_transparency: chroma_key
|
||||||
|
resize: 50x50
|
||||||
|
- id: grayscale_animation
|
||||||
|
file: $component_dir/anim.apng
|
||||||
|
type: grayscale
|
||||||
|
|
||||||
|
display:
|
||||||
|
lambda: |-
|
||||||
|
id(rgb565_animation).next_frame();
|
||||||
|
id(rgb_animation1).next_frame();
|
||||||
|
id(grayscale_animation2).next_frame();
|
||||||
|
it.image(0, 0, rgb565_animation);
|
||||||
|
it.image(120, 0, rgb_animation1);
|
||||||
|
it.image(240, 0, grayscale_animation2);
|
@ -13,12 +13,6 @@ display:
|
|||||||
reset_pin: 21
|
reset_pin: 21
|
||||||
invert_colors: false
|
invert_colors: false
|
||||||
|
|
||||||
# Purposely test that `animation:` does auto-load `image:`
|
packages:
|
||||||
# Keep the `image:` undefined.
|
animation: !include common.yaml
|
||||||
# image:
|
|
||||||
|
|
||||||
animation:
|
|
||||||
- id: rgb565_animation
|
|
||||||
file: ../../pnglogo.png
|
|
||||||
type: RGB565
|
|
||||||
use_transparency: false
|
|
||||||
|
@ -13,12 +13,5 @@ display:
|
|||||||
reset_pin: 10
|
reset_pin: 10
|
||||||
invert_colors: false
|
invert_colors: false
|
||||||
|
|
||||||
# Purposely test that `animation:` does auto-load `image:`
|
packages:
|
||||||
# Keep the `image:` undefined.
|
animation: !include common.yaml
|
||||||
# image:
|
|
||||||
|
|
||||||
animation:
|
|
||||||
- id: rgb565_animation
|
|
||||||
file: ../../pnglogo.png
|
|
||||||
type: RGB565
|
|
||||||
use_transparency: false
|
|
||||||
|
@ -13,12 +13,5 @@ display:
|
|||||||
reset_pin: 10
|
reset_pin: 10
|
||||||
invert_colors: false
|
invert_colors: false
|
||||||
|
|
||||||
# Purposely test that `animation:` does auto-load `image:`
|
packages:
|
||||||
# Keep the `image:` undefined.
|
animation: !include common.yaml
|
||||||
# image:
|
|
||||||
|
|
||||||
animation:
|
|
||||||
- id: rgb565_animation
|
|
||||||
file: ../../pnglogo.png
|
|
||||||
type: RGB565
|
|
||||||
use_transparency: false
|
|
||||||
|
@ -13,12 +13,5 @@ display:
|
|||||||
reset_pin: 21
|
reset_pin: 21
|
||||||
invert_colors: false
|
invert_colors: false
|
||||||
|
|
||||||
# Purposely test that `animation:` does auto-load `image:`
|
packages:
|
||||||
# Keep the `image:` undefined.
|
animation: !include common.yaml
|
||||||
# image:
|
|
||||||
|
|
||||||
animation:
|
|
||||||
- id: rgb565_animation
|
|
||||||
file: ../../pnglogo.png
|
|
||||||
type: RGB565
|
|
||||||
use_transparency: false
|
|
||||||
|
@ -13,12 +13,5 @@ display:
|
|||||||
reset_pin: 16
|
reset_pin: 16
|
||||||
invert_colors: false
|
invert_colors: false
|
||||||
|
|
||||||
# Purposely test that `animation:` does auto-load `image:`
|
packages:
|
||||||
# Keep the `image:` undefined.
|
animation: !include common.yaml
|
||||||
# image:
|
|
||||||
|
|
||||||
animation:
|
|
||||||
- id: rgb565_animation
|
|
||||||
file: ../../pnglogo.png
|
|
||||||
type: RGB565
|
|
||||||
use_transparency: false
|
|
||||||
|
@ -13,12 +13,5 @@ display:
|
|||||||
reset_pin: 22
|
reset_pin: 22
|
||||||
invert_colors: false
|
invert_colors: false
|
||||||
|
|
||||||
# Purposely test that `animation:` does auto-load `image:`
|
packages:
|
||||||
# Keep the `image:` undefined.
|
animation: !include common.yaml
|
||||||
# image:
|
|
||||||
|
|
||||||
animation:
|
|
||||||
- id: rgb565_animation
|
|
||||||
file: ../../pnglogo.png
|
|
||||||
type: RGB565
|
|
||||||
use_transparency: false
|
|
||||||
|
1
tests/components/debug/test.esp32-s2-ard.yaml
Normal file
1
tests/components/debug/test.esp32-s2-ard.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
1
tests/components/debug/test.esp32-s2-idf.yaml
Normal file
1
tests/components/debug/test.esp32-s2-idf.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
1
tests/components/debug/test.esp32-s3-ard.yaml
Normal file
1
tests/components/debug/test.esp32-s3-ard.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
1
tests/components/debug/test.esp32-s3-idf.yaml
Normal file
1
tests/components/debug/test.esp32-s3-idf.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
@ -5,32 +5,65 @@ image:
|
|||||||
dither: FloydSteinberg
|
dither: FloydSteinberg
|
||||||
- id: transparent_transparent_image
|
- id: transparent_transparent_image
|
||||||
file: ../../pnglogo.png
|
file: ../../pnglogo.png
|
||||||
type: TRANSPARENT_BINARY
|
type: BINARY
|
||||||
|
use_transparency: chroma_key
|
||||||
|
|
||||||
- id: rgba_image
|
- id: rgba_image
|
||||||
file: ../../pnglogo.png
|
file: ../../pnglogo.png
|
||||||
type: RGBA
|
type: RGB
|
||||||
|
use_transparency: alpha_channel
|
||||||
resize: 50x50
|
resize: 50x50
|
||||||
- id: rgb24_image
|
- id: rgb24_image
|
||||||
file: ../../pnglogo.png
|
file: ../../pnglogo.png
|
||||||
type: RGB24
|
type: RGB
|
||||||
use_transparency: yes
|
use_transparency: chroma_key
|
||||||
|
- id: rgb_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
type: RGB
|
||||||
|
use_transparency: opaque
|
||||||
|
|
||||||
- id: rgb565_image
|
- id: rgb565_image
|
||||||
file: ../../pnglogo.png
|
file: ../../pnglogo.png
|
||||||
type: RGB565
|
type: RGB565
|
||||||
use_transparency: no
|
use_transparency: opaque
|
||||||
|
- id: rgb565_ck_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
type: RGB565
|
||||||
|
use_transparency: chroma_key
|
||||||
|
- id: rgb565_alpha_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
type: RGB565
|
||||||
|
use_transparency: alpha_channel
|
||||||
|
|
||||||
|
- id: grayscale_alpha_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
type: grayscale
|
||||||
|
use_transparency: alpha_channel
|
||||||
|
resize: 50x50
|
||||||
|
- id: grayscale_ck_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
type: grayscale
|
||||||
|
use_transparency: chroma_key
|
||||||
|
- id: grayscale_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
type: grayscale
|
||||||
|
use_transparency: opaque
|
||||||
|
|
||||||
- id: web_svg_image
|
- id: web_svg_image
|
||||||
file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
|
file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
|
||||||
resize: 256x48
|
resize: 256x48
|
||||||
type: TRANSPARENT_BINARY
|
type: BINARY
|
||||||
|
use_transparency: chroma_key
|
||||||
- id: web_tiff_image
|
- id: web_tiff_image
|
||||||
file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
|
file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
|
||||||
type: RGB24
|
type: RGB
|
||||||
resize: 48x48
|
resize: 48x48
|
||||||
- id: web_redirect_image
|
- id: web_redirect_image
|
||||||
file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
|
file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
|
||||||
type: RGB24
|
type: RGB
|
||||||
resize: 48x48
|
resize: 48x48
|
||||||
- id: mdi_alert
|
- id: mdi_alert
|
||||||
|
type: BINARY
|
||||||
file: mdi:alert-circle-outline
|
file: mdi:alert-circle-outline
|
||||||
resize: 50x50
|
resize: 50x50
|
||||||
- id: another_alert_icon
|
- id: another_alert_icon
|
||||||
|
@ -5,4 +5,44 @@ display:
|
|||||||
width: 480
|
width: 480
|
||||||
height: 480
|
height: 480
|
||||||
|
|
||||||
<<: !include common.yaml
|
image:
|
||||||
|
binary:
|
||||||
|
- id: binary_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
dither: FloydSteinberg
|
||||||
|
- id: transparent_transparent_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
use_transparency: chroma_key
|
||||||
|
rgb:
|
||||||
|
alpha_channel:
|
||||||
|
- id: rgba_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
resize: 50x50
|
||||||
|
chroma_key:
|
||||||
|
- id: rgb24_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
type: RGB
|
||||||
|
opaque:
|
||||||
|
- id: rgb_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
rgb565:
|
||||||
|
- id: rgb565_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
use_transparency: opaque
|
||||||
|
- id: rgb565_ck_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
use_transparency: chroma_key
|
||||||
|
- id: rgb565_alpha_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
use_transparency: alpha_channel
|
||||||
|
grayscale:
|
||||||
|
- id: grayscale_alpha_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
use_transparency: alpha_channel
|
||||||
|
resize: 50x50
|
||||||
|
- id: grayscale_ck_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
use_transparency: chroma_key
|
||||||
|
- id: grayscale_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
use_transparency: opaque
|
||||||
|
@ -2,6 +2,8 @@ esphome:
|
|||||||
on_boot:
|
on_boot:
|
||||||
- lambda: 'ESP_LOGD("display","is_connected(): %s", YESNO(id(main_lcd).is_connected()));'
|
- lambda: 'ESP_LOGD("display","is_connected(): %s", YESNO(id(main_lcd).is_connected()));'
|
||||||
|
|
||||||
|
- display.nextion.set_brightness: 80%
|
||||||
|
|
||||||
# Binary sensor publish action tests
|
# Binary sensor publish action tests
|
||||||
- binary_sensor.nextion.publish:
|
- binary_sensor.nextion.publish:
|
||||||
id: r0_sensor
|
id: r0_sensor
|
||||||
|
@ -13,33 +13,32 @@ online_image:
|
|||||||
resize: 50x50
|
resize: 50x50
|
||||||
- id: online_binary_transparent_image
|
- id: online_binary_transparent_image
|
||||||
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||||
type: TRANSPARENT_BINARY
|
type: BINARY
|
||||||
|
use_transparency: chroma_key
|
||||||
format: png
|
format: png
|
||||||
- id: online_rgba_image
|
- id: online_rgba_image
|
||||||
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||||
format: PNG
|
format: PNG
|
||||||
type: RGBA
|
type: RGB
|
||||||
|
use_transparency: alpha_channel
|
||||||
- id: online_rgb24_image
|
- id: online_rgb24_image
|
||||||
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||||
format: PNG
|
format: PNG
|
||||||
type: RGB24
|
type: RGB
|
||||||
use_transparency: true
|
use_transparency: chroma_key
|
||||||
|
|
||||||
# Check the set_url action
|
# Check the set_url action
|
||||||
time:
|
esphome:
|
||||||
- platform: sntp
|
on_boot:
|
||||||
on_time:
|
then:
|
||||||
- at: "13:37:42"
|
- online_image.set_url:
|
||||||
then:
|
id: online_rgba_image
|
||||||
- online_image.set_url:
|
url: http://www.example.org/example.png
|
||||||
id: online_rgba_image
|
- online_image.set_url:
|
||||||
url: http://www.example.org/example.png
|
id: online_rgba_image
|
||||||
- online_image.set_url:
|
url: !lambda |-
|
||||||
id: online_rgba_image
|
return "http://www.example.org/example.png";
|
||||||
url: !lambda |-
|
- online_image.set_url:
|
||||||
return "http://www.example.org/example.png";
|
id: online_rgba_image
|
||||||
- online_image.set_url:
|
url: !lambda |-
|
||||||
id: online_rgba_image
|
return str_sprintf("http://homeassistant.local:8123");
|
||||||
url: !lambda |-
|
|
||||||
return str_sprintf("http://homeassistant.local:8123");
|
|
||||||
|
|
||||||
|
1
tests/components/psram/test.esp32-s2-ard.yaml
Normal file
1
tests/components/psram/test.esp32-s2-ard.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
1
tests/components/psram/test.esp32-s2-idf.yaml
Normal file
1
tests/components/psram/test.esp32-s2-idf.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
1
tests/components/psram/test.esp32-s3-ard.yaml
Normal file
1
tests/components/psram/test.esp32-s3-ard.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
1
tests/components/psram/test.esp32-s3-idf.yaml
Normal file
1
tests/components/psram/test.esp32-s3-idf.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
Loading…
Reference in New Issue
Block a user