mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 13:56:10 +00:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a04032b345 | ||
|
|
611d8cfb4d | ||
|
|
7dace44d61 | ||
|
|
491f98f982 | ||
|
|
619283d74a | ||
|
|
18348b8794 | ||
|
|
f7dc2ecb51 | ||
|
|
ddcc5874ed | ||
|
|
28013a60e1 | ||
|
|
966193cb09 | ||
|
|
e1077748be | ||
|
|
cc89a011e2 | ||
|
|
c76e9e1d62 | ||
|
|
7a3aac407f | ||
|
|
b0a1054e8d | ||
|
|
a8a8acb918 | ||
|
|
3ae5e1dfad | ||
|
|
ef79d48b6f | ||
|
|
94804cc741 | ||
|
|
879c5e5e14 | ||
|
|
edda565237 | ||
|
|
3616052a7c | ||
|
|
fbc4bc9e10 |
@@ -0,0 +1,156 @@
|
||||
---
|
||||
name: display-test
|
||||
description: Test an ESPHome display driver on a real panel with a camera. Use when a task asks to verify a display (LCD, e-paper, OLED, RLCD) on hardware, check orientation/rotation, partial updates, colours, contrast, or that text and shapes render correctly. Covers finding the USB port, flashing a test pattern, capturing the panel with any camera, and reading the photo.
|
||||
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
||||
---
|
||||
|
||||
# Display testing with a camera
|
||||
|
||||
Point a camera at the panel to get visual evidence that a driver renders correctly. Any camera
|
||||
and capture tool works; this skill covers the common ones. `$SKILL` below is this directory
|
||||
(`.agents/skills/display-test`).
|
||||
|
||||
## Ask before using a camera
|
||||
|
||||
A camera can show people and rooms, not only the panel. Before the first capture, ask the user
|
||||
for permission and for which camera to use. List the cameras the host has (the commands below
|
||||
show how), name the one you intend to use, and wait for the user to confirm it. Do not pick a
|
||||
camera yourself, and do not fall back to another camera if the chosen one fails; ask again.
|
||||
|
||||
The permission covers this session and the camera the user named. Ask again for a different
|
||||
camera or in a new session. If the user declines, stop and report that the display was not
|
||||
checked visually.
|
||||
|
||||
## Capturing a frame
|
||||
|
||||
Grab a still image, skipping the first few frames so auto-exposure and white balance settle.
|
||||
Read the JPEG with the Read tool; the image is shown visually.
|
||||
|
||||
### macOS
|
||||
|
||||
The bundled Swift tool uses AVFoundation and avoids the frame-grabbing problems some USB
|
||||
cameras have with other tools:
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/camsnap /tmp/display-test
|
||||
swiftc -O "$SKILL/scripts/camsnap.swift" -o /tmp/camsnap/camsnap
|
||||
/tmp/camsnap/camsnap <camera-name-substring> /tmp/display-test/after.jpg 30
|
||||
```
|
||||
|
||||
The camera argument is a case-insensitive substring of the name AVFoundation reports. If it does
|
||||
not match, the tool lists the cameras it found, so a run with a name that matches nothing lists
|
||||
the cameras without capturing. Use a substring that matches only the camera the user approved.
|
||||
It creates the output directory if needed and reports encoding or write errors on stderr. Other
|
||||
options include `ffmpeg` with `avfoundation` input, `imagesnap`, or Photo Booth's still capture.
|
||||
|
||||
### Linux
|
||||
|
||||
Use `ffmpeg` with the v4l2 input, `fswebcam`, or `v4l2-ctl`. With `ffmpeg`, `-ss 2` after the
|
||||
input discards the first two seconds of frames while exposure settles:
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/display-test
|
||||
ffmpeg -f v4l2 -i /dev/video0 -ss 2 -frames:v 1 -y /tmp/display-test/after.jpg
|
||||
# or
|
||||
fswebcam -d /dev/video0 --skip 10 /tmp/display-test/after.jpg
|
||||
```
|
||||
|
||||
List devices with `v4l2-ctl --list-devices`.
|
||||
|
||||
### Windows
|
||||
|
||||
Use `ffmpeg` with the DirectShow input (`-ss 2` skips the first frames, as on Linux), or any
|
||||
camera application that can save a still image:
|
||||
|
||||
```powershell
|
||||
mkdir C:\temp -Force | Out-Null
|
||||
ffmpeg -f dshow -list_devices true -i dummy # find the device name
|
||||
ffmpeg -f dshow -i video="<device name>" -ss 2 -frames:v 1 -y C:\temp\after.jpg
|
||||
```
|
||||
|
||||
### Camera caveats
|
||||
|
||||
- The board may sit at an angle. Do not trust absolute pixel positions or aspect ratios from the
|
||||
photo; judge orientation, ordering and approximate spacing, using the corner markers as
|
||||
references.
|
||||
- Reflections or a glossy cover can brighten one side. Capture again if a region is unclear.
|
||||
- A moving pattern is caught at an arbitrary phase. Take several captures, or use a static marker
|
||||
per position.
|
||||
|
||||
## Board and port
|
||||
|
||||
Find the port; names change on every replug:
|
||||
|
||||
```bash
|
||||
python3 "$SKILL/../hardware-test/scripts/ports.py"
|
||||
```
|
||||
|
||||
Native-USB ESP32-S3/C3/C6/H2 boards show up as `USB JTAG/serial debug unit` and need
|
||||
`logger: hardware_uart: USB_SERIAL_JTAG`.
|
||||
|
||||
## Test pattern
|
||||
|
||||
Write a scratch config outside the checkout. Use one `name:` per board and include:
|
||||
|
||||
- a white background, a black border and a filled square at the centre, drawn in the first
|
||||
update (full-window check and scale);
|
||||
- a 60x20 bar near each corner, one bar per later update with `auto_clear_enabled: false`, so
|
||||
each bar is written by its own partial window. The bars are wider than tall, which shows a
|
||||
90 or 270 degree rotation; the corners exercise the window alignment;
|
||||
- a 20x20 square under the left end of the top-left bar, drawn in the same update, which marks
|
||||
the top-left corner and shows a 180 degree rotation or a mirrored image.
|
||||
|
||||
Use `Color::BLACK` and `Color::WHITE` in lambdas. Do not use the old `COLOR_OFF`/`COLOR_ON`
|
||||
constants; black must mean black.
|
||||
|
||||
A ready template is in `$SKILL/templates/display-pattern.yaml`. Substitute the display platform,
|
||||
pins and (for a driver with fonts) a font. The marker pattern needs no font.
|
||||
|
||||
## Build, flash, capture
|
||||
|
||||
Run from the worktree under test so `python -m esphome` imports the checkout, not an installed
|
||||
copy:
|
||||
|
||||
```bash
|
||||
cd <worktree>
|
||||
python3 -c "import esphome; print(esphome.__file__)" # must print the worktree path
|
||||
PYTHONUNBUFFERED=1 python3 -m esphome compile /tmp/display-test/display-pattern.yaml
|
||||
PYTHONUNBUFFERED=1 python3 -m esphome upload /tmp/display-test/display-pattern.yaml \
|
||||
--device /dev/cu.usbmodemXXXX
|
||||
sleep 6 # boot plus a few update intervals
|
||||
<capture command> /tmp/display-test/after.jpg
|
||||
```
|
||||
|
||||
The device path differs by OS and host; use the path printed by `ports.py`. If the USB console
|
||||
wedges during flashing (esptool `No serial data received`), replug the board and retry. A board
|
||||
powered only by a battery may need the USB cable for flashing; the panel keeps its image when
|
||||
power is removed, which is useful for checking that an image is bistable.
|
||||
|
||||
## What to check
|
||||
|
||||
1. **Orientation**: the extra square sits under the bar in the expected top-left corner. If it is
|
||||
in another corner, the image is rotated or mirrored. Test `rotation:` values with the same
|
||||
pattern. The logical width and height swap at 90 and
|
||||
270 degrees, so the 60x20 corner bars should then appear taller than wide on the physical
|
||||
panel.
|
||||
2. **Coverage**: the border touches all four edges, with no dead rows or columns at the edges.
|
||||
3. **Partial updates**: with `auto_clear_enabled: false`, draw one marker per update. Each should
|
||||
appear exactly at its requested position with no smearing, offset or mirrored placement.
|
||||
Include all four corners.
|
||||
4. **Text and contrast**: sharp glyphs, correct polarity (black text on white, not inverted), no
|
||||
ghosting from the previous frame.
|
||||
5. **Speed**: if the driver logs transfer sizes, check that a full refresh and a partial window
|
||||
take the expected times.
|
||||
|
||||
Record the build commit, board, config, and the captured image path when reporting results.
|
||||
|
||||
## When a partial update looks wrong
|
||||
|
||||
Take a marker-only capture before assuming the mapping is wrong: the camera's perspective makes
|
||||
absolute positions deceptive. If markers land in mirrored positions, the controller's address
|
||||
order (`MADCTL` MX/MY/MV) or the RAM block mapping is inverted for the window; verify the
|
||||
full-frame path separately, then fix the window computation.
|
||||
|
||||
## Add findings
|
||||
|
||||
Add new capture tools, panel quirks, or recovery steps here so the next session starts ahead.
|
||||
@@ -0,0 +1,71 @@
|
||||
// camsnap <camera name substring> <output.jpg> [frames to skip]
|
||||
// macOS only: grabs one frame from an AVFoundation camera after letting exposure settle.
|
||||
// See SKILL.md for capture tools on other systems.
|
||||
import AVFoundation
|
||||
import AppKit
|
||||
import CoreImage
|
||||
|
||||
let args = CommandLine.arguments
|
||||
guard args.count >= 3 else {
|
||||
FileHandle.standardError.write("usage: camsnap <camera name> <out.jpg> [skip]\n".data(using: .utf8)!)
|
||||
exit(2)
|
||||
}
|
||||
let wanted = args[1].lowercased()
|
||||
let outPath = args[2]
|
||||
let skip = args.count > 3 ? Int(args[3]) ?? 30 : 30
|
||||
|
||||
let discovery = AVCaptureDevice.DiscoverySession(
|
||||
deviceTypes: [.external, .builtInWideAngleCamera], mediaType: .video, position: .unspecified)
|
||||
guard let device = discovery.devices.first(where: { $0.localizedName.lowercased().contains(wanted) }) else {
|
||||
let names = discovery.devices.map { $0.localizedName }.joined(separator: ", ")
|
||||
FileHandle.standardError.write("no camera matching '\(wanted)'; found: \(names)\n".data(using: .utf8)!)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
final class Grabber: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
|
||||
var seen = 0
|
||||
let skip: Int
|
||||
let out: String
|
||||
init(skip: Int, out: String) { self.skip = skip; self.out = out }
|
||||
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer,
|
||||
from connection: AVCaptureConnection) {
|
||||
seen += 1
|
||||
guard seen > skip, let pixels = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
|
||||
let image = CIImage(cvPixelBuffer: pixels)
|
||||
let rep = NSBitmapImageRep(ciImage: image)
|
||||
guard let jpeg = rep.representation(using: .jpeg, properties: [.compressionFactor: 0.9]) else {
|
||||
FileHandle.standardError.write("failed to encode \(out) as JPEG\n".data(using: .utf8)!)
|
||||
exit(1)
|
||||
}
|
||||
let url = URL(fileURLWithPath: out)
|
||||
try? FileManager.default.createDirectory(
|
||||
at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
do {
|
||||
try jpeg.write(to: url)
|
||||
} catch {
|
||||
FileHandle.standardError.write("failed to write \(out): \(error)\n".data(using: .utf8)!)
|
||||
exit(1)
|
||||
}
|
||||
print("saved \(out) after \(seen) frames")
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
let session = AVCaptureSession()
|
||||
session.sessionPreset = .high
|
||||
guard let input = try? AVCaptureDeviceInput(device: device), session.canAddInput(input) else {
|
||||
FileHandle.standardError.write("cannot open \(device.localizedName)\n".data(using: .utf8)!)
|
||||
exit(1)
|
||||
}
|
||||
session.addInput(input)
|
||||
let output = AVCaptureVideoDataOutput()
|
||||
output.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
|
||||
let grabber = Grabber(skip: skip, out: outPath)
|
||||
output.setSampleBufferDelegate(grabber, queue: DispatchQueue(label: "camsnap"))
|
||||
session.addOutput(output)
|
||||
session.startRunning()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 15) {
|
||||
FileHandle.standardError.write("timed out waiting for frames\n".data(using: .utf8)!)
|
||||
exit(1)
|
||||
}
|
||||
RunLoop.main.run()
|
||||
@@ -0,0 +1,43 @@
|
||||
# Display test pattern: corner markers, centre reference and partial-update markers.
|
||||
# Replace the display platform and pins with the driver under test.
|
||||
esphome:
|
||||
name: display-pattern
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
level: DEBUG
|
||||
|
||||
spi:
|
||||
clk_pin: GPIO11
|
||||
mosi_pin: GPIO12
|
||||
|
||||
display:
|
||||
- platform: st7305
|
||||
id: test_display
|
||||
cs_pin: GPIO40
|
||||
dc_pin: GPIO5
|
||||
reset_pin: GPIO41
|
||||
update_interval: 1s
|
||||
auto_clear_enabled: false
|
||||
lambda: |-
|
||||
static int frame = 0;
|
||||
if (frame == 0) {
|
||||
it.fill(Color::WHITE);
|
||||
it.rectangle(0, 0, it.get_width(), it.get_height(), Color::BLACK);
|
||||
it.filled_rectangle(it.get_width() / 2 - 10, it.get_height() / 2 - 10, 20, 20, Color::BLACK);
|
||||
} else if (frame == 1) {
|
||||
it.filled_rectangle(20, 20, 60, 20, Color::BLACK);
|
||||
it.filled_rectangle(20, 50, 20, 20, Color::BLACK);
|
||||
} else if (frame == 2) {
|
||||
it.filled_rectangle(20, it.get_height() - 40, 60, 20, Color::BLACK);
|
||||
} else if (frame == 3) {
|
||||
it.filled_rectangle(it.get_width() - 80, 20, 60, 20, Color::BLACK);
|
||||
} else if (frame == 4) {
|
||||
it.filled_rectangle(it.get_width() - 80, it.get_height() - 40, 60, 20, Color::BLACK);
|
||||
}
|
||||
frame++;
|
||||
@@ -12,6 +12,7 @@ Checks: >-
|
||||
-bugprone-multi-level-implicit-pointer-conversion,
|
||||
-bugprone-narrowing-conversions,
|
||||
-bugprone-tagged-union-member-count,
|
||||
-bugprone-signed-bitwise,
|
||||
-bugprone-signed-char-misuse,
|
||||
-bugprone-switch-missing-default-case,
|
||||
-cert-dcl50-cpp,
|
||||
@@ -39,6 +40,7 @@ Checks: >-
|
||||
-cppcoreguidelines-avoid-const-or-ref-data-members,
|
||||
-cppcoreguidelines-avoid-do-while,
|
||||
-cppcoreguidelines-avoid-magic-numbers,
|
||||
-cppcoreguidelines-explicit-constructor,
|
||||
-cppcoreguidelines-init-variables,
|
||||
-cppcoreguidelines-macro-to-enum,
|
||||
-cppcoreguidelines-macro-usage,
|
||||
@@ -84,6 +86,7 @@ Checks: >-
|
||||
-llvm-use-ranges,
|
||||
-llvmlibc-*,
|
||||
-misc-const-correctness,
|
||||
-misc-explicit-constructor,
|
||||
-misc-include-cleaner,
|
||||
-misc-multiple-inheritance,
|
||||
-misc-no-recursion,
|
||||
@@ -113,6 +116,7 @@ Checks: >-
|
||||
-objc-*,
|
||||
-performance-enum-size,
|
||||
-portability-avoid-pragma-once,
|
||||
-portability-no-assembler,
|
||||
-portability-template-virtual-member-function,
|
||||
-readability-ambiguous-smartptr-reset-call,
|
||||
-readability-avoid-nested-conditional-operator,
|
||||
@@ -129,9 +133,13 @@ Checks: >-
|
||||
-readability-named-parameter,
|
||||
-readability-redundant-casting,
|
||||
-readability-redundant-inline-specifier,
|
||||
-readability-redundant-lambda-parameter-list,
|
||||
-readability-redundant-member-init,
|
||||
-readability-redundant-nested-if,
|
||||
-readability-redundant-parentheses,
|
||||
-readability-redundant-typename,
|
||||
-readability-trailing-comma,
|
||||
-readability-trivial-switch,
|
||||
-readability-uppercase-literal-suffix,
|
||||
-readability-use-anyofallof,
|
||||
-readability-use-std-min-max,
|
||||
|
||||
@@ -38,7 +38,7 @@ repos:
|
||||
- id: yamllint
|
||||
exclude: ^(\.clang-format|\.clang-tidy)$
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v22.1.8
|
||||
rev: v23.1.3
|
||||
hooks:
|
||||
- id: clang-format
|
||||
types_or: [c, c++]
|
||||
|
||||
@@ -700,6 +700,7 @@ esphome/components/veml7700/* @latonita
|
||||
esphome/components/version/* @esphome/core
|
||||
esphome/components/vevor_dtx/* @Bofr5555
|
||||
esphome/components/vl53l1x/* @will-tm
|
||||
esphome/components/vnc/* @MindFreeze
|
||||
esphome/components/voice_assistant/* @jesserockz @kahrendt
|
||||
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
|
||||
esphome/components/watchdog/* @oarcher
|
||||
|
||||
@@ -111,6 +111,8 @@ static const LogString *input_to_str(uint8_t input) {
|
||||
return LOG_STR("VDD");
|
||||
case NRF_SAADC_INPUT_VDDHDIV5:
|
||||
return LOG_STR("VDDHDIV5");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return LOG_STR("undefined input");
|
||||
}
|
||||
|
||||
@@ -261,6 +261,8 @@ void AirtonClimate::parse_state_frame_(const uint8_t *frame) {
|
||||
case AIRTON_FAN_AUTO:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this->target_temperature = (frame[3] & 0b00001111) + AIRTON_TEMP_MIN;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from collections.abc import Callable
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
@@ -27,6 +29,7 @@ from esphome.const import (
|
||||
CONF_CAPTURE_RESPONSE,
|
||||
CONF_DATA,
|
||||
CONF_DATA_TEMPLATE,
|
||||
CONF_DEFAULT,
|
||||
CONF_DELAY,
|
||||
CONF_ENCRYPTION,
|
||||
CONF_EVENT,
|
||||
@@ -136,6 +139,7 @@ SERVICE_ARG_FALLBACK_TYPES: dict[str, MockObj] = {
|
||||
CONF_BATCH_DELAY = "batch_delay"
|
||||
CONF_CUSTOM_SERVICES = "custom_services"
|
||||
CONF_EXAMPLE = "example"
|
||||
CONF_REQUIRED = "required"
|
||||
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
|
||||
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
|
||||
CONF_LISTEN_BACKLOG = "listen_backlog"
|
||||
@@ -247,12 +251,66 @@ def _validate_supports_response(value: Any) -> str:
|
||||
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
|
||||
|
||||
|
||||
VARIABLE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_TYPE): cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
|
||||
cv.Optional(CONF_DESCRIPTION): cv.string_strict,
|
||||
cv.Optional(CONF_EXAMPLE): cv.string_strict,
|
||||
}
|
||||
def _not_boolean(value: Any) -> Any:
|
||||
# bool is an int subclass, and "true" cannot be parsed as a number by the client
|
||||
if isinstance(value, bool):
|
||||
raise cv.Invalid("Expected a number, got a boolean")
|
||||
return value
|
||||
|
||||
|
||||
def _finite(value: float) -> float:
|
||||
if not math.isfinite(value):
|
||||
raise cv.Invalid("Expected a finite number")
|
||||
return value
|
||||
|
||||
|
||||
def _no_nul(value: str) -> str:
|
||||
# The default is a C string in flash, so a NUL would cut it short
|
||||
if "\0" in value:
|
||||
raise cv.Invalid("A default cannot contain a NUL character")
|
||||
return value
|
||||
|
||||
|
||||
# The wire carries the default as text; the client parses it into ExecuteServiceArgument's
|
||||
# types, so the values are kept within what int32 and float hold
|
||||
FLOAT32_MAX = 3.4028235e38
|
||||
_DEFAULT_VALUE_VALIDATORS: dict[str, Callable[[Any], Any]] = {
|
||||
"bool": cv.boolean,
|
||||
"int": cv.All(_not_boolean, cv.int_range(min=-(2**31), max=2**31 - 1)),
|
||||
"float": cv.All(
|
||||
_not_boolean,
|
||||
cv.float_,
|
||||
_finite,
|
||||
cv.float_range(min=-FLOAT32_MAX, max=FLOAT32_MAX),
|
||||
),
|
||||
"string": cv.All(cv.string_strict, _no_nul),
|
||||
}
|
||||
assert _DEFAULT_VALUE_VALIDATORS.keys() == _SERVICE_ARG_SCALAR_TYPES.keys()
|
||||
|
||||
|
||||
def _validate_variable_optionality(value: ConfigType) -> ConfigType:
|
||||
"""Cross-validate required/default against the variable type."""
|
||||
var_type = value[CONF_TYPE]
|
||||
if _is_optional_var(value) and var_type not in _DEFAULT_VALUE_VALIDATORS:
|
||||
raise cv.Invalid("Array variables cannot be optional")
|
||||
if CONF_DEFAULT in value:
|
||||
if value.get(CONF_REQUIRED) is True:
|
||||
raise cv.Invalid("A variable with a default cannot set required: true")
|
||||
value[CONF_DEFAULT] = _DEFAULT_VALUE_VALIDATORS[var_type](value[CONF_DEFAULT])
|
||||
return value
|
||||
|
||||
|
||||
VARIABLE_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_TYPE): cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
|
||||
cv.Optional(CONF_DESCRIPTION): cv.string_strict,
|
||||
cv.Optional(CONF_EXAMPLE): cv.string_strict,
|
||||
cv.Optional(CONF_REQUIRED): cv.boolean,
|
||||
cv.Optional(CONF_DEFAULT): cv.valid,
|
||||
}
|
||||
),
|
||||
_validate_variable_optionality,
|
||||
)
|
||||
|
||||
# Accepts the plain `name: type` shorthand or the full mapping form
|
||||
@@ -464,7 +522,28 @@ def _has_action_metadata(actions: list[ConfigType]) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _action_strings(conf: ConfigType, has_metadata: bool) -> list[str | None]:
|
||||
def _is_optional_var(var_: ConfigType) -> bool:
|
||||
return CONF_DEFAULT in var_ or var_.get(CONF_REQUIRED) is False
|
||||
|
||||
|
||||
def _has_optional_args(actions: list[ConfigType]) -> bool:
|
||||
return any(
|
||||
_is_optional_var(var_)
|
||||
for conf in actions
|
||||
for var_ in conf[CONF_VARIABLES].values()
|
||||
)
|
||||
|
||||
|
||||
def _default_to_wire(value: Any) -> str:
|
||||
"""Serialize a validated default for the wire; clients coerce by arg type."""
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
return str(value)
|
||||
|
||||
|
||||
def _action_strings(
|
||||
conf: ConfigType, has_metadata: bool, has_optional: bool
|
||||
) -> list[str | None]:
|
||||
"""Strings of one action in the table order UserServiceStatic (user_services.h) expects."""
|
||||
# An empty description or example is treated as unset
|
||||
strings: list[str | None] = [conf[CONF_ACTION]]
|
||||
@@ -477,6 +556,15 @@ def _action_strings(conf: ConfigType, has_metadata: bool) -> list[str | None]:
|
||||
var_.get(CONF_DESCRIPTION) or None,
|
||||
var_.get(CONF_EXAMPLE) or None,
|
||||
]
|
||||
if has_optional:
|
||||
# The default slot doubles as the optional flag: nullptr for a required variable,
|
||||
# "" for an optional one without a default (an empty string default is the same)
|
||||
if not _is_optional_var(var_):
|
||||
strings.append(None)
|
||||
elif (default := var_.get(CONF_DEFAULT)) is None:
|
||||
strings.append("")
|
||||
else:
|
||||
strings.append(_default_to_wire(default))
|
||||
return strings
|
||||
|
||||
|
||||
@@ -485,9 +573,7 @@ def _validate_wizard(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
_validate_wizard,
|
||||
)
|
||||
FINAL_VALIDATE_SCHEMA = _validate_wizard
|
||||
|
||||
|
||||
def _add_action_strings(index: int, strings: list[str | None]) -> MockObj:
|
||||
@@ -546,6 +632,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
has_metadata = _has_action_metadata(actions)
|
||||
if has_metadata:
|
||||
cg.add_define("USE_API_USER_DEFINED_ACTION_METADATA")
|
||||
has_optional = _has_optional_args(actions)
|
||||
if has_optional:
|
||||
cg.add_define("USE_API_USER_DEFINED_ACTION_OPTIONAL_ARGS")
|
||||
# Collect all triggers first, then register all at once with initializer_list
|
||||
triggers: list[cg.MockObj] = []
|
||||
for index, conf in enumerate(actions):
|
||||
@@ -589,7 +678,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
native = SERVICE_ARG_NATIVE_TYPES[var_type]
|
||||
service_template_args.append(native)
|
||||
func_args.append((native, name))
|
||||
strings = _action_strings(conf, has_metadata)
|
||||
strings = _action_strings(conf, has_metadata, has_optional)
|
||||
table = _add_action_strings(index, strings)
|
||||
# Template args: supports_response mode, then user service arg types
|
||||
templ = cg.TemplateArguments(supports_response, *service_template_args)
|
||||
|
||||
@@ -608,6 +608,7 @@ message ListEntitiesCoverResponse {
|
||||
bool supports_stop = 12;
|
||||
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
||||
bool supports_stop_tilt = 14;
|
||||
bool supports_toggle = 15;
|
||||
}
|
||||
|
||||
// Deprecated in API version 1.1
|
||||
@@ -670,6 +671,7 @@ message CoverCommandRequest {
|
||||
bool stop = 8;
|
||||
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
|
||||
bool stop_tilt = 10;
|
||||
bool toggle = 11;
|
||||
}
|
||||
|
||||
// ==================== FAN ====================
|
||||
@@ -1172,6 +1174,10 @@ message ListEntitiesServicesArgument {
|
||||
ServiceArgType type = 2;
|
||||
string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
|
||||
string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
|
||||
// Advisory: ExecuteServiceRequest still requires every argument
|
||||
bool optional = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_OPTIONAL_ARGS"];
|
||||
// Always text: bool "true"/"false", numbers parseable as int/float; empty means no default
|
||||
string default_value = 6 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_OPTIONAL_ARGS", (progmem) = true];
|
||||
}
|
||||
message ListEntitiesServicesResponse {
|
||||
option (id) = 41;
|
||||
@@ -1783,6 +1789,8 @@ message MediaPlayerSupportedFormat {
|
||||
uint32 num_channels = 3;
|
||||
MediaPlayerFormatPurpose purpose = 4;
|
||||
uint32 sample_bytes = 5;
|
||||
// Constant bitrate in kbps for lossy formats; 0 means no preference
|
||||
uint32 bitrate = 6;
|
||||
}
|
||||
message ListEntitiesMediaPlayerResponse {
|
||||
option (id) = 63;
|
||||
|
||||
@@ -555,6 +555,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
|
||||
msg.supports_tilt = traits.get_supports_tilt();
|
||||
msg.supports_stop = traits.get_supports_stop();
|
||||
msg.supports_stop_tilt = traits.get_supports_stop_tilt();
|
||||
msg.supports_toggle = traits.get_supports_toggle();
|
||||
return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
|
||||
@@ -567,6 +568,8 @@ void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
|
||||
call.set_command_stop();
|
||||
if (msg.stop_tilt)
|
||||
call.set_command_stop_tilt();
|
||||
if (msg.toggle)
|
||||
call.set_command_toggle();
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
@@ -1128,6 +1131,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
|
||||
media_format.num_channels = supported_format.num_channels;
|
||||
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
|
||||
media_format.sample_bytes = supported_format.sample_bytes;
|
||||
media_format.bitrate = supported_format.bitrate;
|
||||
}
|
||||
return fill_and_encode_entity_info(media_player, msg, conn, remaining_size);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ void HelloRequest::decode_field(void *self, uint32_t tag, const uint8_t *data, p
|
||||
msg.outgoing_connection_target = value.as_bool();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *HelloResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -51,6 +53,8 @@ void DisconnectRequest::decode_field(void *self, uint32_t tag, const uint8_t *da
|
||||
case proto_tag(1, WIRE_TYPE_VARINT):
|
||||
msg.reason = static_cast<enums::DisconnectReason>(value.as_varint());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *DisconnectRequest::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -396,6 +400,8 @@ void WizardInputSetRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
case proto_tag(2, WIRE_TYPE_LENGTH_DELIMITED):
|
||||
msg.entity_id = StringRef(value.data(), value.size());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -478,6 +484,7 @@ uint8_t *ListEntitiesCoverResponse::encode_msg(const void *self, uint8_t *__rest
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, msg.device_id);
|
||||
#endif
|
||||
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 14, msg.supports_stop_tilt);
|
||||
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 15, msg.supports_toggle);
|
||||
return pos;
|
||||
}
|
||||
uint32_t ListEntitiesCoverResponse::calc_size_msg(const void *self) {
|
||||
@@ -500,6 +507,7 @@ uint32_t ListEntitiesCoverResponse::calc_size_msg(const void *self) {
|
||||
size += ProtoSize::calc_uint32(1, msg.device_id);
|
||||
#endif
|
||||
size += ProtoSize::calc_bool(1, msg.supports_stop_tilt);
|
||||
size += ProtoSize::calc_bool(1, msg.supports_toggle);
|
||||
return size;
|
||||
}
|
||||
uint8_t *CoverStateResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -559,6 +567,11 @@ void CoverCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
|
||||
case proto_tag(10, WIRE_TYPE_VARINT):
|
||||
msg.stop_tilt = value.as_bool();
|
||||
break;
|
||||
case proto_tag(11, WIRE_TYPE_VARINT):
|
||||
msg.toggle = value.as_bool();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -679,6 +692,8 @@ void FanCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *da
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -885,6 +900,8 @@ void LightCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1028,6 +1045,8 @@ void SwitchCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1098,6 +1117,8 @@ void SubscribeLogsRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.dump_config = value.as_bool();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
@@ -1127,6 +1148,8 @@ void NoiseEncryptionSetKeyRequest::decode_field(void *self, uint32_t tag, const
|
||||
msg.key = value.data();
|
||||
msg.key_len = value.size();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *NoiseEncryptionSetKeyResponse::encode_msg(const void *self,
|
||||
@@ -1233,6 +1256,8 @@ void HomeassistantActionResponse::decode_field(void *self, uint32_t tag, const u
|
||||
msg.response_data_len = value.size();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1267,6 +1292,8 @@ void HomeAssistantStateResponse::decode_field(void *self, uint32_t tag, const ui
|
||||
case proto_tag(3, WIRE_TYPE_LENGTH_DELIMITED):
|
||||
msg.attribute = StringRef(value.data(), value.size());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1292,6 +1319,8 @@ void DSTRule::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_
|
||||
case proto_tag(6, WIRE_TYPE_VARINT):
|
||||
msg.day_of_week = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void ParsedTimezone::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
|
||||
@@ -1310,6 +1339,8 @@ void ParsedTimezone::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
case proto_tag(4, WIRE_TYPE_LENGTH_DELIMITED):
|
||||
value.decode_to_message(msg.dst_end);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void GetTimeResponse::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
|
||||
@@ -1323,6 +1354,8 @@ void GetTimeResponse::decode_field(void *self, uint32_t tag, const uint8_t *data
|
||||
value.decode_to_message(msg.parsed_timezone);
|
||||
msg.has_parsed_timezone = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
@@ -1336,6 +1369,12 @@ uint8_t *ListEntitiesServicesArgument::encode_msg(const void *self,
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.example);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_OPTIONAL_ARGS
|
||||
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.optional);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_OPTIONAL_ARGS
|
||||
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.default_value);
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
@@ -1349,6 +1388,12 @@ uint32_t ListEntitiesServicesArgument::calc_size_msg(const void *self) {
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
size += ProtoSize::calc_length(1, msg.example.size());
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_OPTIONAL_ARGS
|
||||
size += ProtoSize::calc_bool(1, msg.optional);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_OPTIONAL_ARGS
|
||||
size += ProtoSize::calc_length(1, msg.default_value.size());
|
||||
#endif
|
||||
return size;
|
||||
}
|
||||
@@ -1413,6 +1458,8 @@ void ExecuteServiceArgument::decode_field(void *self, uint32_t tag, const uint8_
|
||||
case proto_tag(9, WIRE_TYPE_LENGTH_DELIMITED):
|
||||
msg.string_array.push_back(value.as_string());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
|
||||
@@ -1447,6 +1494,8 @@ void ExecuteServiceRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
msg.return_response = value.as_bool();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) {
|
||||
@@ -1541,6 +1590,8 @@ void CameraImageRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.stream = value.as_bool();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1777,6 +1828,8 @@ void ClimateCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1905,6 +1958,8 @@ void WaterHeaterCommandRequest::decode_field(void *self, uint32_t tag, const uin
|
||||
case proto_tag(8, WIRE_TYPE_FIXED32):
|
||||
msg.target_temperature_high = value.as_float();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1996,6 +2051,8 @@ void NumberCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2075,6 +2132,8 @@ void SelectCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2177,6 +2236,8 @@ void SirenCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2260,6 +2321,8 @@ void LockCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2309,6 +2372,8 @@ void ButtonCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2320,6 +2385,7 @@ uint8_t *MediaPlayerSupportedFormat::encode_msg(const void *self, uint8_t *__res
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.num_channels);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(msg.purpose));
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.sample_bytes);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.bitrate);
|
||||
return pos;
|
||||
}
|
||||
uint32_t MediaPlayerSupportedFormat::calc_size_msg(const void *self) {
|
||||
@@ -2330,6 +2396,7 @@ uint32_t MediaPlayerSupportedFormat::calc_size_msg(const void *self) {
|
||||
size += ProtoSize::calc_uint32(1, msg.num_channels);
|
||||
size += msg.purpose ? 2 : 0;
|
||||
size += ProtoSize::calc_uint32(1, msg.sample_bytes);
|
||||
size += ProtoSize::calc_uint32(1, msg.bitrate);
|
||||
return size;
|
||||
}
|
||||
uint8_t *ListEntitiesMediaPlayerResponse::encode_msg(const void *self,
|
||||
@@ -2436,6 +2503,8 @@ void MediaPlayerCommandRequest::decode_field(void *self, uint32_t tag, const uin
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2448,6 +2517,8 @@ void SubscribeBluetoothLEAdvertisementsRequest::decode_field(void *self, uint32_
|
||||
case proto_tag(1, WIRE_TYPE_VARINT):
|
||||
msg.flags = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
@@ -2506,6 +2577,8 @@ void BluetoothDeviceRequest::decode_field(void *self, uint32_t tag, const uint8_
|
||||
case proto_tag(4, WIRE_TYPE_VARINT):
|
||||
msg.address_type = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *BluetoothDeviceConnectionResponse::encode_msg(const void *self,
|
||||
@@ -2534,6 +2607,8 @@ void BluetoothGATTGetServicesRequest::decode_field(void *self, uint32_t tag, con
|
||||
case proto_tag(1, WIRE_TYPE_VARINT):
|
||||
msg.address = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *BluetoothGATTDescriptor::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -2660,6 +2735,8 @@ void BluetoothGATTReadRequest::decode_field(void *self, uint32_t tag, const uint
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.handle = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *BluetoothGATTReadResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -2695,6 +2772,8 @@ void BluetoothGATTWriteRequest::decode_field(void *self, uint32_t tag, const uin
|
||||
msg.data = value.data();
|
||||
msg.data_len = value.size();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void BluetoothGATTReadDescriptorRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
@@ -2708,6 +2787,8 @@ void BluetoothGATTReadDescriptorRequest::decode_field(void *self, uint32_t tag,
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.handle = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void BluetoothGATTWriteDescriptorRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
@@ -2725,6 +2806,8 @@ void BluetoothGATTWriteDescriptorRequest::decode_field(void *self, uint32_t tag,
|
||||
msg.data = value.data();
|
||||
msg.data_len = value.size();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void BluetoothGATTNotifyRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
@@ -2741,6 +2824,8 @@ void BluetoothGATTNotifyRequest::decode_field(void *self, uint32_t tag, const ui
|
||||
case proto_tag(3, WIRE_TYPE_VARINT):
|
||||
msg.enable = value.as_bool();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *BluetoothGATTNotifyDataResponse::encode_msg(const void *self,
|
||||
@@ -2898,6 +2983,8 @@ void BluetoothScannerSetModeRequest::decode_field(void *self, uint32_t tag, cons
|
||||
case proto_tag(1, WIRE_TYPE_VARINT):
|
||||
msg.mode = static_cast<enums::BluetoothScannerMode>(value.as_varint());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2913,6 +3000,8 @@ void SubscribeVoiceAssistantRequest::decode_field(void *self, uint32_t tag, cons
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.flags = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *VoiceAssistantAudioSettings::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -2961,6 +3050,8 @@ void VoiceAssistantResponse::decode_field(void *self, uint32_t tag, const uint8_
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.error = value.as_bool();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void VoiceAssistantEventData::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
|
||||
@@ -2973,6 +3064,8 @@ void VoiceAssistantEventData::decode_field(void *self, uint32_t tag, const uint8
|
||||
case proto_tag(2, WIRE_TYPE_LENGTH_DELIMITED):
|
||||
msg.value = StringRef(value.data(), value.size());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void VoiceAssistantEventResponse::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
@@ -2987,6 +3080,8 @@ void VoiceAssistantEventResponse::decode_field(void *self, uint32_t tag, const u
|
||||
msg.data.emplace_back();
|
||||
value.decode_to_message(msg.data.back());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void VoiceAssistantAudio::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
|
||||
@@ -3004,6 +3099,8 @@ void VoiceAssistantAudio::decode_field(void *self, uint32_t tag, const uint8_t *
|
||||
msg.data2 = value.data();
|
||||
msg.data2_len = value.size();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *VoiceAssistantAudio::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -3044,6 +3141,8 @@ void VoiceAssistantTimerEventResponse::decode_field(void *self, uint32_t tag, co
|
||||
case proto_tag(6, WIRE_TYPE_VARINT):
|
||||
msg.is_active = value.as_bool();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void VoiceAssistantAnnounceRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
@@ -3063,6 +3162,8 @@ void VoiceAssistantAnnounceRequest::decode_field(void *self, uint32_t tag, const
|
||||
case proto_tag(4, WIRE_TYPE_VARINT):
|
||||
msg.start_conversation = value.as_bool();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *VoiceAssistantAnnounceFinished::encode_msg(const void *self,
|
||||
@@ -3124,6 +3225,8 @@ void VoiceAssistantExternalWakeWord::decode_field(void *self, uint32_t tag, cons
|
||||
case proto_tag(7, WIRE_TYPE_LENGTH_DELIMITED):
|
||||
msg.url = StringRef(value.data(), value.size());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void VoiceAssistantConfigurationRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
@@ -3135,6 +3238,8 @@ void VoiceAssistantConfigurationRequest::decode_field(void *self, uint32_t tag,
|
||||
msg.external_wake_words.emplace_back();
|
||||
value.decode_to_message(msg.external_wake_words.back());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *VoiceAssistantConfigurationResponse::encode_msg(const void *self,
|
||||
@@ -3173,6 +3278,8 @@ void VoiceAssistantSetConfiguration::decode_field(void *self, uint32_t tag, cons
|
||||
case proto_tag(1, WIRE_TYPE_LENGTH_DELIMITED):
|
||||
msg.active_wake_words.push_back(value.as_string());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3254,6 +3361,8 @@ void AlarmControlPanelCommandRequest::decode_field(void *self, uint32_t tag, con
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3333,6 +3442,8 @@ void TextCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3414,6 +3525,8 @@ void DateCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3495,6 +3608,8 @@ void TimeCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3644,6 +3759,8 @@ void ValveCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3718,6 +3835,8 @@ void DateTimeCommandRequest::decode_field(void *self, uint32_t tag, const uint8_
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3807,6 +3926,8 @@ void UpdateCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3819,6 +3940,8 @@ void ZWaveProxyFrame::decode_field(void *self, uint32_t tag, const uint8_t *data
|
||||
msg.data = value.data();
|
||||
msg.data_len = value.size();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
@@ -3845,6 +3968,8 @@ void ZWaveProxyRequest::decode_field(void *self, uint32_t tag, const uint8_t *da
|
||||
msg.data = value.data();
|
||||
msg.data_len = value.size();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *ZWaveProxyRequest::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -3940,6 +4065,8 @@ void InfraredRFTransmitRawTimingsRequest::decode_field(void *self, uint32_t tag,
|
||||
case proto_tag(6, WIRE_TYPE_VARINT):
|
||||
msg.modulation = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
@@ -4056,6 +4183,8 @@ void SerialProxyConfigureRequest::decode_field(void *self, uint32_t tag, const u
|
||||
case proto_tag(6, WIRE_TYPE_VARINT):
|
||||
msg.data_size = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
@@ -4084,6 +4213,8 @@ void SerialProxyWriteRequest::decode_field(void *self, uint32_t tag, const uint8
|
||||
msg.data = value.data();
|
||||
msg.data_len = value.size();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void SerialProxySetModemPinsRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
@@ -4097,6 +4228,8 @@ void SerialProxySetModemPinsRequest::decode_field(void *self, uint32_t tag, cons
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.line_states = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void SerialProxyGetModemPinsRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
@@ -4107,6 +4240,8 @@ void SerialProxyGetModemPinsRequest::decode_field(void *self, uint32_t tag, cons
|
||||
case proto_tag(1, WIRE_TYPE_VARINT):
|
||||
msg.instance = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *SerialProxyGetModemPinsResponse::encode_msg(const void *self,
|
||||
@@ -4135,6 +4270,8 @@ void SerialProxyRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.type = static_cast<enums::SerialProxyRequestType>(value.as_varint());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *SerialProxyRequestResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -4165,6 +4302,8 @@ void SerialProxySetModeRequest::decode_field(void *self, uint32_t tag, const uin
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.mode = static_cast<enums::SerialProxyMode>(value.as_varint());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *UsbDeviceDescriptor::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -4233,6 +4372,8 @@ void BluetoothSetConnectionParamsRequest::decode_field(void *self, uint32_t tag,
|
||||
case proto_tag(5, WIRE_TYPE_VARINT):
|
||||
msg.timeout = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *BluetoothSetConnectionParamsResponse::encode_msg(const void *self,
|
||||
|
||||
@@ -911,7 +911,7 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage {
|
||||
class ListEntitiesCoverResponse final : public InfoResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 13;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 59;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 61;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("list_entities_cover_response"); }
|
||||
#endif
|
||||
@@ -921,6 +921,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage {
|
||||
ProgmemStringRef device_class{};
|
||||
bool supports_stop{false};
|
||||
bool supports_stop_tilt{false};
|
||||
bool supports_toggle{false};
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
@@ -958,7 +959,7 @@ class CoverStateResponse final : public StateResponseProtoMessage {
|
||||
class CoverCommandRequest final : public CommandProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 30;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 27;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 29;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("cover_command_request"); }
|
||||
#endif
|
||||
@@ -968,6 +969,7 @@ class CoverCommandRequest final : public CommandProtoMessage {
|
||||
float tilt{0.0f};
|
||||
bool stop{false};
|
||||
bool stop_tilt{false};
|
||||
bool toggle{false};
|
||||
void decode(const uint8_t *buffer, size_t length) {
|
||||
ProtoDecodableMessage::decode_fields(this, buffer, length, &decode_field);
|
||||
}
|
||||
@@ -1591,6 +1593,12 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
ProgmemStringRef example{};
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_OPTIONAL_ARGS
|
||||
bool optional{false};
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_OPTIONAL_ARGS
|
||||
ProgmemStringRef default_value{};
|
||||
#endif
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
@@ -2261,6 +2269,7 @@ class MediaPlayerSupportedFormat final : public ProtoMessage {
|
||||
uint32_t num_channels{0};
|
||||
enums::MediaPlayerFormatPurpose purpose{};
|
||||
uint32_t sample_bytes{0};
|
||||
uint32_t bitrate{0};
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
|
||||
@@ -1246,6 +1246,7 @@ const char *ListEntitiesCoverResponse::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
|
||||
#endif
|
||||
dump_field(out, ESPHOME_PSTR("supports_stop_tilt"), this->supports_stop_tilt);
|
||||
dump_field(out, ESPHOME_PSTR("supports_toggle"), this->supports_toggle);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *CoverStateResponse::dump_to(DumpBuffer &out) const {
|
||||
@@ -1271,6 +1272,7 @@ const char *CoverCommandRequest::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
|
||||
#endif
|
||||
dump_field(out, ESPHOME_PSTR("stop_tilt"), this->stop_tilt);
|
||||
dump_field(out, ESPHOME_PSTR("toggle"), this->toggle);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
@@ -1641,6 +1643,12 @@ const char *ListEntitiesServicesArgument::dump_to(DumpBuffer &out) const {
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("example"), this->example);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_OPTIONAL_ARGS
|
||||
dump_field(out, ESPHOME_PSTR("optional"), this->optional);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_OPTIONAL_ARGS
|
||||
dump_progmem_string_field(out, ESPHOME_PSTR("default_value"), this->default_value);
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
@@ -2097,6 +2105,7 @@ const char *MediaPlayerSupportedFormat::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("num_channels"), this->num_channels);
|
||||
dump_field(out, ESPHOME_PSTR("purpose"), static_cast<enums::MediaPlayerFormatPurpose>(this->purpose));
|
||||
dump_field(out, ESPHOME_PSTR("sample_bytes"), this->sample_bytes);
|
||||
dump_field(out, ESPHOME_PSTR("bitrate"), this->bitrate);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ListEntitiesMediaPlayerResponse::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -301,7 +301,7 @@ constexpr uint32_t VARINT_MAX_1_BYTE = 1 << 7; // 128
|
||||
constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384
|
||||
|
||||
/// Generated encode body: writes the fields at pos, returns the cursor past them.
|
||||
using ProtoEncodeFn = uint8_t *(*) (const void *, uint8_t *PROTO_ENCODE_DEBUG_PARAM);
|
||||
using ProtoEncodeFn = uint8_t *(*)(const void *, uint8_t *PROTO_ENCODE_DEBUG_PARAM);
|
||||
|
||||
/// Static encode helpers for the generated encode bodies. Each takes the write cursor by value and
|
||||
/// returns it advanced, so outlined calls at -Os chain through the return register instead of a
|
||||
|
||||
@@ -31,6 +31,13 @@ ListEntitiesServicesResponse UserServiceStatic::encode_list_service_response_(
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
arg.description = this->str_(base + 1);
|
||||
arg.example = this->str_(base + 2);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_OPTIONAL_ARGS
|
||||
// The default slot doubles as the optional flag: nullptr is required, "" optional without a default
|
||||
const char *d = progmem_read_ptr(&this->strings_[base + USER_ACTION_ARG_BASE_STRINGS]);
|
||||
arg.optional = d != nullptr;
|
||||
if (d != nullptr)
|
||||
arg.default_value = ProgmemStringRef(d, ESPHOME_strlen_P(d));
|
||||
#endif
|
||||
}
|
||||
return msg;
|
||||
|
||||
@@ -40,13 +40,20 @@ template<typename T> enums::ServiceArgType to_service_arg_type();
|
||||
// All strings live in one PROGMEM pointer table emitted by codegen (see _action_strings in
|
||||
// __init__.py), so each service costs a single pointer of RAM. Layout: the action name, then
|
||||
// each argument name; with USE_API_USER_DEFINED_ACTION_METADATA the action description follows
|
||||
// the name and every argument is (name, description, example). Unset metadata is nullptr.
|
||||
// the name and every argument is (name, description, example); with
|
||||
// USE_API_USER_DEFINED_ACTION_OPTIONAL_ARGS every argument ends with its default: nullptr for a
|
||||
// required argument, "" for an optional one without a default. Unset is nullptr.
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
static constexpr size_t USER_ACTION_HEADER_STRINGS = 2;
|
||||
static constexpr size_t USER_ACTION_ARG_STRINGS = 3;
|
||||
static constexpr size_t USER_ACTION_ARG_BASE_STRINGS = 3;
|
||||
#else
|
||||
static constexpr size_t USER_ACTION_HEADER_STRINGS = 1;
|
||||
static constexpr size_t USER_ACTION_ARG_STRINGS = 1;
|
||||
static constexpr size_t USER_ACTION_ARG_BASE_STRINGS = 1;
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_OPTIONAL_ARGS
|
||||
static constexpr size_t USER_ACTION_ARG_STRINGS = USER_ACTION_ARG_BASE_STRINGS + 1;
|
||||
#else
|
||||
static constexpr size_t USER_ACTION_ARG_STRINGS = USER_ACTION_ARG_BASE_STRINGS;
|
||||
#endif
|
||||
class UserServiceStatic : public UserServiceDescriptor {
|
||||
public:
|
||||
|
||||
@@ -168,6 +168,8 @@ bool BalluClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
case BALLU_AUTO:
|
||||
this->mode = climate::CLIMATE_MODE_AUTO;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -111,10 +111,9 @@ static const uint8_t BL0906_USR_WRPROT = 0x9E;
|
||||
// Reset Register
|
||||
static const uint8_t BL0906_SOFT_RESET = 0x9F;
|
||||
|
||||
const uint8_t BL0906_INIT[2][6] = {
|
||||
// Reset to default
|
||||
{BL0906_WRITE_COMMAND, BL0906_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x52},
|
||||
// Enable User Operation Write
|
||||
{BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x55, 0x55, 0x00, 0xB7}};
|
||||
const uint8_t BL0906_INIT[2][6] = {// Reset to default
|
||||
{BL0906_WRITE_COMMAND, BL0906_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x52},
|
||||
// Enable User Operation Write
|
||||
{BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x55, 0x55, 0x00, 0xB7}};
|
||||
|
||||
} // namespace esphome::bl0906
|
||||
|
||||
@@ -268,6 +268,7 @@ int BluedroidGattClient::discover_services() {
|
||||
case SearchState::REPORT_PENDING:
|
||||
return 0; // One completion is already owed to this claimant.
|
||||
case SearchState::NONE:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
int err = this->check_and_log_error_("esp_ble_gattc_search_service",
|
||||
|
||||
@@ -22,8 +22,8 @@ class BluetoothProxy;
|
||||
|
||||
namespace esphome::bluetooth_connection {
|
||||
|
||||
using ClientState = ble_device_base::ClientState;
|
||||
using ConnectionType = ble_device_base::ConnectionType;
|
||||
using ble_device_base::ClientState;
|
||||
using ble_device_base::ConnectionType;
|
||||
|
||||
/// A refused GATT reply owed to the current subscriber. Payload-free only:
|
||||
/// these rebuild from address + handle + error, so a retry costs no buffered
|
||||
|
||||
@@ -30,8 +30,8 @@ using bluetooth_connection::INIT_SENDING_SERVICES;
|
||||
using bluetooth_connection::SERVICES_DONE_PENDING;
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
using BluetoothConnection = bluetooth_connection::BluetoothConnection;
|
||||
using ClientState = ble_device_base::ClientState;
|
||||
using bluetooth_connection::BluetoothConnection;
|
||||
using ble_device_base::ClientState;
|
||||
#endif
|
||||
|
||||
// Legacy versions:
|
||||
|
||||
@@ -123,7 +123,7 @@ void BMI160Component::internal_setup_(int stage) {
|
||||
|
||||
bool BMI160Component::setup_stage_(int stage) {
|
||||
switch (stage) {
|
||||
case 0:
|
||||
case 0: {
|
||||
uint8_t chipid;
|
||||
if (!this->read_byte(BMI160_REGISTER_CHIPID, &chipid) || (chipid != 0b11010001)) {
|
||||
return false;
|
||||
@@ -138,7 +138,7 @@ bool BMI160Component::setup_stage_(int stage) {
|
||||
// min 5ms, 10ms
|
||||
this->set_timeout(10, [this]() { this->internal_setup_(1); });
|
||||
break;
|
||||
|
||||
}
|
||||
case 1:
|
||||
ESP_LOGV(TAG, " Bringing gyroscope out of sleep");
|
||||
if (!this->write_byte(BMI160_REGISTER_CMD, (uint8_t) Cmd::GYRO_SET_PMU_MODE | (uint8_t) GyroPmuMode::NORMAL)) {
|
||||
@@ -149,7 +149,7 @@ bool BMI160Component::setup_stage_(int stage) {
|
||||
this->set_timeout(GYRO_WAKEUP_TIMEOUT_MS, [this]() { this->internal_setup_(2); });
|
||||
break;
|
||||
|
||||
case 2:
|
||||
case 2: {
|
||||
ESP_LOGV(TAG, " Setting up Gyro Config");
|
||||
uint8_t gyro_config = (uint8_t) GyroBandwidth::OSR4 | (uint8_t) GyroOuputDataRate::HZ_25;
|
||||
ESP_LOGV(TAG, " Output gyro_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(gyro_config));
|
||||
@@ -178,6 +178,10 @@ bool BMI160Component::setup_stage_(int stage) {
|
||||
}
|
||||
|
||||
this->setup_complete_ = true;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ class CC1101Component final : public Component,
|
||||
|
||||
// GDO pin for packet reception
|
||||
InternalGPIOPin *gdo0_pin_{nullptr};
|
||||
static void IRAM_ATTR gpio_intr(CC1101Component *arg);
|
||||
static void gpio_intr(CC1101Component *arg);
|
||||
|
||||
// Packet handling
|
||||
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
|
||||
|
||||
@@ -175,6 +175,8 @@ uint32_t CSE7761Component::coefficient_by_unit_(uint32_t unit) {
|
||||
case POWER_PAC:
|
||||
coeff = this->data_.coefficient[POWER_PAC] * scale;
|
||||
return coeff ? 0x80000000 / coeff : 0;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -156,6 +156,8 @@ static const char *regout0_to_str(uint32_t value) {
|
||||
return "3.0V";
|
||||
case (UICR_REGOUT0_VOUT_3V3):
|
||||
return "3.3V";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return "???V";
|
||||
}
|
||||
@@ -337,6 +339,8 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
return "QFxx - 6x6 48-pin QFN";
|
||||
case 0x2005:
|
||||
return "CKxx - 3.544 x 3.607 WLCSP";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return "Unspecified";
|
||||
};
|
||||
|
||||
@@ -143,6 +143,8 @@ bool DelonghiClimate::parse_state_frame_(const uint8_t frame[]) {
|
||||
case DELONGHI_FAN_AUTO:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this->publish_state();
|
||||
return true;
|
||||
|
||||
@@ -250,6 +250,8 @@ void DFPlayer::loop() {
|
||||
ESP_LOGE(TAG, "Entered into sleep mode");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x41:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cinttypes>
|
||||
#include <utility>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
@@ -26,7 +27,7 @@ class DutyTimeSensor final : public sensor::Sensor, public PollingComponent {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void set_sensor(binary_sensor::BinarySensor *sensor);
|
||||
#endif
|
||||
void set_lambda(std::function<bool()> &&func) { this->func_ = func; }
|
||||
void set_lambda(std::function<bool()> &&func) { this->func_ = std::move(func); }
|
||||
void set_last_duty_time_sensor(sensor::Sensor *sensor) { this->last_duty_time_sensor_ = sensor; }
|
||||
void set_restore(bool restore) { this->restore_ = restore; }
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#include "epaper_goodisplay_3c.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
void EPaperGoodDisplay3C::power_on() {}
|
||||
|
||||
void EPaperGoodDisplay3C::refresh_screen(bool partial) {
|
||||
EPaperWeAct3C::refresh_screen(partial);
|
||||
this->next_delay_ = REFRESH_START_MS;
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "epaper_weact_3c.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
/**
|
||||
* Good Display 3-color e-paper displays (SSD1680 controller), e.g. GDEY029Z95.
|
||||
*
|
||||
* Driven as EPaperWeAct3C, except that there is no separate power-on: the full refresh (0xF7) powers the panel
|
||||
* up itself. Busy then only goes high a little after the refresh starts, so the next state waits before
|
||||
* checking it.
|
||||
*/
|
||||
class EPaperGoodDisplay3C : public EPaperWeAct3C {
|
||||
public:
|
||||
using EPaperWeAct3C::EPaperWeAct3C;
|
||||
|
||||
protected:
|
||||
// Time from master activation (0x20) until busy can be relied on
|
||||
static constexpr uint16_t REFRESH_START_MS = 50;
|
||||
|
||||
// With EPaperWeAct3C's power-on (0x22 0xF8) first, this panel's refresh ends almost at once and the image
|
||||
// does not change.
|
||||
void power_on() override;
|
||||
void refresh_screen(bool partial) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -4,11 +4,17 @@ Supported models:
|
||||
- weact-2.13in-3c: 122x250 pixels (2.13" display)
|
||||
- weact-2.9in-3c: 128x296 pixels (2.9" display)
|
||||
- weact-4.2in-3c: 400x300 pixels (4.2" display)
|
||||
- goodisplay-gdey029z95-2.9: 128x296 pixels (Good Display GDEY029Z95 2.9" display)
|
||||
|
||||
These displays use SSD1680 or SSD1683 controller and require a specific initialization
|
||||
sequence. The DRV_OUT_CTL command is calculated from the display height.
|
||||
The Good Display panel is driven as the WeAct ones, except that it must not get the
|
||||
separate power-on before the refresh, so it has its own driver class, EPaperGoodDisplay3C.
|
||||
"""
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BUSY_PIN, CONF_RESET_PIN
|
||||
|
||||
from . import EpaperModel
|
||||
|
||||
|
||||
@@ -17,8 +23,10 @@ class WeActBWR(EpaperModel):
|
||||
|
||||
source_files = ("epaper_weact_3c.cpp",)
|
||||
|
||||
def __init__(self, name, **defaults):
|
||||
super().__init__(name, "EPaperWeAct3C", **defaults)
|
||||
def __init__(
|
||||
self, name: str, class_name: str = "EPaperWeAct3C", **defaults: object
|
||||
) -> None:
|
||||
super().__init__(name, class_name, **defaults)
|
||||
|
||||
def get_init_sequence(self, config):
|
||||
"""Generate initialization sequence for WeAct BWR displays.
|
||||
@@ -75,3 +83,27 @@ weact_4p2in3c = WeActBWR(
|
||||
data_rate="10MHz",
|
||||
minimum_update_interval="10s",
|
||||
)
|
||||
|
||||
|
||||
class GoodDisplayBWR(WeActBWR):
|
||||
"""Good Display Black/White/Red displays using SSD1680 controller."""
|
||||
|
||||
source_files = ("epaper_weact_3c.cpp", "epaper_goodisplay_3c.cpp")
|
||||
|
||||
def __init__(self, name: str, **defaults: object) -> None:
|
||||
super().__init__(name, "EPaperGoodDisplay3C", **defaults)
|
||||
|
||||
def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required:
|
||||
# Only a hardware reset wakes the panel from deep sleep, and refreshes are timed by busy
|
||||
if name in (CONF_RESET_PIN, CONF_BUSY_PIN):
|
||||
return cv.Required(name)
|
||||
return super().option(name, fallback)
|
||||
|
||||
|
||||
# Model: Good Display GDEY029Z95 2.9" BWR - 128x296 pixels, SSD1680 controller
|
||||
GoodDisplayBWR(
|
||||
"goodisplay-gdey029z95-2.9",
|
||||
width=128,
|
||||
height=296,
|
||||
minimum_update_interval="10s",
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
namespace esphome::esp32_ble {
|
||||
|
||||
using ble_device_base::UUID_STR_LEN;
|
||||
using ESPBTUUID = ble_device_base::ESPBTUUID;
|
||||
using ble_device_base::ESPBTUUID;
|
||||
|
||||
} // namespace esphome::esp32_ble
|
||||
|
||||
|
||||
@@ -33,18 +33,18 @@ namespace esphome::esp32_ble_tracker {
|
||||
|
||||
using namespace esp32_ble;
|
||||
|
||||
using adv_data_t = ble_device_base::adv_data_t;
|
||||
using ble_device_base::adv_data_t;
|
||||
|
||||
#ifdef USE_ESP32_BLE_UUID
|
||||
using ServiceData = ble_device_base::ServiceData;
|
||||
using ble_device_base::ServiceData;
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
// The advertisement device types are owned by the platform-neutral
|
||||
// ble_device_base layer; re-exported here (esp32 only) for backward
|
||||
// compatibility. ESPBTDevice::parse_scan_rst() (esp32-only) adapts BLEScanResult.
|
||||
using ESPBLEiBeacon = ble_device_base::ESPBLEiBeacon;
|
||||
using ESPBTDevice = ble_device_base::ESPBTDevice;
|
||||
using ble_device_base::ESPBLEiBeacon;
|
||||
using ble_device_base::ESPBTDevice;
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
|
||||
class ESP32BLETracker;
|
||||
@@ -82,12 +82,12 @@ struct ClientStateCounts {
|
||||
|
||||
// The client connection state types are owned by the platform-neutral
|
||||
// ble_device_base layer; re-exported here for backward compatibility.
|
||||
using ClientState = ble_device_base::ClientState;
|
||||
using ConnectionType = ble_device_base::ConnectionType;
|
||||
using ble_device_base::ClientState;
|
||||
using ble_device_base::ConnectionType;
|
||||
using ble_device_base::client_state_to_string;
|
||||
|
||||
// Neutral scanner lifecycle re-exported for backward compatibility.
|
||||
using ScannerState = ble_device_base::ScannerState;
|
||||
using ble_device_base::ScannerState;
|
||||
|
||||
/// Base class for BLE GATT clients that connect to remote devices.
|
||||
///
|
||||
|
||||
@@ -46,6 +46,7 @@ from esphome.const import (
|
||||
CONF_TYPE,
|
||||
CONF_USE_ADDRESS,
|
||||
CONF_VALUE,
|
||||
CONF_WIFI,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
Platform,
|
||||
@@ -562,6 +563,25 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _final_validate_wifi(config: ConfigType) -> None:
|
||||
"""A wifi station competes with ethernet for the primary interface, so both must be in network: priority:."""
|
||||
full_config = fv.full_config.get()
|
||||
if (wifi_config := full_config.get(CONF_WIFI)) is None:
|
||||
return
|
||||
|
||||
from esphome.components.wifi import wifi_stays_ap_only
|
||||
|
||||
if wifi_stays_ap_only(wifi_config, full_config):
|
||||
return # an access point does not claim the primary interface
|
||||
|
||||
priority_ifaces = get_priority_interfaces_from_full_config(full_config)
|
||||
if missing := [i for i in (DOMAIN, CONF_WIFI) if i not in priority_ifaces]:
|
||||
raise cv.Invalid(
|
||||
"When ethernet and a wifi station are used together, 'network: priority:' "
|
||||
f"must list both interfaces; missing: {', '.join(missing)}"
|
||||
)
|
||||
|
||||
|
||||
def _final_validate_spi(config: ConfigType) -> None:
|
||||
if not CORE.is_esp32:
|
||||
return # SPI interface validation is ESP32-only
|
||||
@@ -846,22 +866,7 @@ def _final_validate_rmii_pins(config: ConfigType) -> None:
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
"""Final validation for Ethernet component."""
|
||||
# Allow ethernet + wifi coexistence only when both are declared in network: priority:.
|
||||
if "wifi" in fv.full_config.get():
|
||||
priority_ifaces = get_priority_interfaces_from_full_config(fv.full_config.get())
|
||||
missing = [i for i in ("ethernet", "wifi") if i not in priority_ifaces]
|
||||
if missing and priority_ifaces:
|
||||
# A priority list exists but is incomplete: point at what to add.
|
||||
raise cv.Invalid(
|
||||
"When ethernet and wifi are used together, 'network: priority:' must "
|
||||
f"list both interfaces; missing: {', '.join(missing)}"
|
||||
)
|
||||
if missing:
|
||||
raise cv.Invalid(
|
||||
"Component ethernet cannot be used together with component wifi "
|
||||
"unless both are listed under 'network: priority:'"
|
||||
)
|
||||
|
||||
_final_validate_wifi(config)
|
||||
_final_validate_spi(config)
|
||||
_final_validate_rmii_pins(config)
|
||||
|
||||
|
||||
@@ -175,6 +175,8 @@ void EzoPMP::read_command_result_() {
|
||||
third_parameter_buffer[position_in_parameter_buffer] = current_char;
|
||||
third_parameter_buffer[position_in_parameter_buffer + 1] = '\0';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
position_in_parameter_buffer++;
|
||||
}
|
||||
|
||||
@@ -185,10 +185,6 @@ _PRESET_MODES_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
# speed_count_ is a uint16_t in FanTraits
|
||||
validate_speed_count = cv.int_range(min=1, max=65535)
|
||||
|
||||
|
||||
def validate_preset_modes(value):
|
||||
# Check against defined schema
|
||||
value = _PRESET_MODES_SCHEMA(value)
|
||||
|
||||
@@ -17,24 +17,24 @@ class FanTraits {
|
||||
public:
|
||||
FanTraits() = default;
|
||||
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
|
||||
: flags_{oscillation, speed, direction, 0}, speed_count_(speed_count) {}
|
||||
: oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {}
|
||||
|
||||
/// Return if this fan supports oscillation.
|
||||
bool supports_oscillation() const { return this->flags_.oscillation; }
|
||||
bool supports_oscillation() const { return this->oscillation_; }
|
||||
/// Set whether this fan supports oscillation.
|
||||
void set_oscillation(bool oscillation) { this->flags_.oscillation = oscillation; }
|
||||
void set_oscillation(bool oscillation) { this->oscillation_ = oscillation; }
|
||||
/// Return if this fan supports speed modes.
|
||||
bool supports_speed() const { return this->flags_.speed; }
|
||||
bool supports_speed() const { return this->speed_; }
|
||||
/// Set whether this fan supports speed levels.
|
||||
void set_speed(bool speed) { this->flags_.speed = speed; }
|
||||
void set_speed(bool speed) { this->speed_ = speed; }
|
||||
/// Return how many speed levels the fan has
|
||||
int supported_speed_count() const { return this->speed_count_; }
|
||||
/// Set how many speed levels this fan has.
|
||||
void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; }
|
||||
/// Return if this fan supports changing direction
|
||||
bool supports_direction() const { return this->flags_.direction; }
|
||||
bool supports_direction() const { return this->direction_; }
|
||||
/// Set whether this fan supports changing direction
|
||||
void set_direction(bool direction) { this->flags_.direction = direction; }
|
||||
void set_direction(bool direction) { this->direction_ = direction; }
|
||||
/// Empty when the fan has no preset modes. Set them on the Fan entity.
|
||||
const FanPresetModes &supported_preset_modes() const;
|
||||
|
||||
@@ -60,14 +60,10 @@ class FanTraits {
|
||||
/// Set the preset modes pointer (only Fan::wire_preset_modes_() should call this).
|
||||
void set_supported_preset_modes_(const FanPresetModes *preset_modes) { this->preset_modes_ = preset_modes; }
|
||||
|
||||
// Bit-packed flags to save memory (1 byte instead of 3)
|
||||
struct FanTraitFlags {
|
||||
uint8_t oscillation : 1;
|
||||
uint8_t speed : 1;
|
||||
uint8_t direction : 1;
|
||||
uint8_t reserved : 5;
|
||||
} flags_{};
|
||||
uint16_t speed_count_{}; // validators cap configs at 65535
|
||||
bool oscillation_{false};
|
||||
bool speed_{false};
|
||||
bool direction_{false};
|
||||
int speed_count_{};
|
||||
const FanPresetModes *preset_modes_{nullptr}; // owned by the Fan entity
|
||||
};
|
||||
static_assert(std::is_trivially_copyable_v<FanTraits>, "FanTraits is returned by value from get_traits()");
|
||||
|
||||
@@ -194,6 +194,8 @@ uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) {
|
||||
ESP_LOGE(TAG, "Could not find fingerprint features");
|
||||
this->finger_scan_invalid_callback_.call();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return send_result;
|
||||
}
|
||||
|
||||
@@ -318,20 +318,21 @@ void HaierClimateBase::process_protocol_reset() {
|
||||
|
||||
bool HaierClimateBase::prepare_pending_action() {
|
||||
if (this->action_request_.has_value()) {
|
||||
switch (this->action_request_.value().action) {
|
||||
PendingAction &request = this->action_request_.value();
|
||||
switch (request.action) {
|
||||
case ActionRequest::SEND_CUSTOM_COMMAND:
|
||||
return true;
|
||||
case ActionRequest::TURN_POWER_ON:
|
||||
this->action_request_.value().message = this->get_power_message(true);
|
||||
request.message = this->get_power_message(true);
|
||||
return true;
|
||||
case ActionRequest::TURN_POWER_OFF:
|
||||
this->action_request_.value().message = this->get_power_message(false);
|
||||
request.message = this->get_power_message(false);
|
||||
return true;
|
||||
case ActionRequest::TOGGLE_POWER:
|
||||
this->action_request_.value().message = this->get_power_message(this->mode == ClimateMode::CLIMATE_MODE_OFF);
|
||||
request.message = this->get_power_message(this->mode == ClimateMode::CLIMATE_MODE_OFF);
|
||||
return true;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_.value().action);
|
||||
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) request.action);
|
||||
this->action_request_.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1071,6 +1071,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
|
||||
case (uint8_t) hon_protocol::FanMode::FAN_HIGH:
|
||||
this->fan_mode = CLIMATE_FAN_HIGH;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
should_publish = should_publish || (!old_fan_mode.has_value()) ||
|
||||
(old_fan_mode.value_or(CLIMATE_FAN_ON) != this->fan_mode.value_or(CLIMATE_FAN_ON));
|
||||
@@ -1144,6 +1146,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
|
||||
case (uint8_t) hon_protocol::ConditioningMode::AUTO:
|
||||
this->mode = CLIMATE_MODE_HEAT_COOL;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
should_publish = should_publish || (old_mode != this->mode);
|
||||
|
||||
@@ -33,7 +33,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
|
||||
DECAY_MODE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): fan.validate_speed_count,
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
|
||||
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
|
||||
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ void decode_mitsubishi_heavy_zmp(const uint8_t frame[MITSUBISHI_HEAVY_FRAME_LENG
|
||||
case MITSUBISHI_HEAVY_ZMP_ECONO:
|
||||
climate.preset = climate::CLIMATE_PRESET_ECO;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
uint8_t swing_h = frame[BYTE_SWING_H] & HORIZONTAL_SWING_MASK;
|
||||
|
||||
@@ -260,6 +260,8 @@ bool HitachiClimate::parse_mode_(const uint8_t remote_state[]) {
|
||||
case HITACHI_AC344_MODE_FAN:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
@@ -294,6 +296,8 @@ bool HitachiClimate::parse_fan_(const uint8_t remote_state[]) {
|
||||
case HITACHI_AC344_FAN_AUTO:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -255,6 +255,8 @@ bool HitachiClimate::parse_mode_(const uint8_t remote_state[]) {
|
||||
case HITACHI_AC424_MODE_FAN:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
@@ -289,6 +291,8 @@ bool HitachiClimate::parse_fan_(const uint8_t remote_state[]) {
|
||||
case HITACHI_AC424_FAN_AUTO:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ size_t ImprovBase::get_formatted_next_url_(char *buffer, size_t buffer_size) {
|
||||
void ImprovBase::add_next_url_(improv::RpcResponseBuilder &builder, size_t max_len) {
|
||||
// The builder rejects strings above 254 bytes, so anything longer than this
|
||||
// buffer could never be sent anyway
|
||||
char url_buffer[256];
|
||||
char url_buffer[256] = {};
|
||||
size_t len = this->get_formatted_next_url_(url_buffer, sizeof(url_buffer));
|
||||
if (len == 0) {
|
||||
return;
|
||||
|
||||
@@ -61,6 +61,8 @@ void Kuntze::on_read_holding_registers(uint16_t start_address, std::span<const u
|
||||
if (this->oci_sensor_ != nullptr)
|
||||
this->oci_sensor_->publish_state(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -407,12 +407,10 @@ void LD2410Component::handle_periodic_data_() {
|
||||
}
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
std::function<void(void)> set_number_value(number::Number *n, float value) {
|
||||
void set_number_value(number::Number *n, float value) {
|
||||
if (n != nullptr && (!n->has_state() || n->state != value)) {
|
||||
n->state = value;
|
||||
return [n, value]() { n->publish_state(value); };
|
||||
n->publish_state(value);
|
||||
}
|
||||
return []() {};
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -560,29 +558,24 @@ bool LD2410Component::handle_ack_data_() {
|
||||
Moving distance range: 13th byte
|
||||
Still distance range: 14th byte
|
||||
*/
|
||||
std::vector<std::function<void(void)>> updates;
|
||||
updates.push_back(set_number_value(this->max_move_distance_gate_number_, this->buffer_data_[12]));
|
||||
updates.push_back(set_number_value(this->max_still_distance_gate_number_, this->buffer_data_[13]));
|
||||
set_number_value(this->max_move_distance_gate_number_, this->buffer_data_[12]);
|
||||
set_number_value(this->max_still_distance_gate_number_, this->buffer_data_[13]);
|
||||
/*
|
||||
Moving Sensitivities: 15~23th bytes
|
||||
*/
|
||||
for (std::vector<number::Number *>::size_type i = 0; i != this->gate_move_threshold_numbers_.size(); i++) {
|
||||
updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[14 + i]));
|
||||
for (size_t i = 0; i < TOTAL_GATES; i++) {
|
||||
set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[14 + i]);
|
||||
}
|
||||
/*
|
||||
Still Sensitivities: 24~32th bytes
|
||||
*/
|
||||
for (std::vector<number::Number *>::size_type i = 0; i != this->gate_still_threshold_numbers_.size(); i++) {
|
||||
updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[23 + i]));
|
||||
for (size_t i = 0; i < TOTAL_GATES; i++) {
|
||||
set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[23 + i]);
|
||||
}
|
||||
/*
|
||||
None Duration: 33~34th bytes
|
||||
*/
|
||||
updates.push_back(
|
||||
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[33], this->buffer_data_[32])));
|
||||
for (auto &update : updates) {
|
||||
update();
|
||||
}
|
||||
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[33], this->buffer_data_[32]));
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "light_color_values.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -133,6 +134,8 @@ class LightCall {
|
||||
LightCall &set_effect(const std::string &effect) { return this->set_effect(effect.data(), effect.size()); }
|
||||
/// Set the effect of the light by its name (const char * overload to resolve ambiguity).
|
||||
LightCall &set_effect(const char *effect) { return this->set_effect(effect, strlen(effect)); }
|
||||
/// Set the effect of the light by its name (StringRef overload to resolve ambiguity, zero-copy).
|
||||
LightCall &set_effect(StringRef effect) { return this->set_effect(effect.c_str(), effect.size()); }
|
||||
/// Set the effect of the light by its name and length (zero-copy from API).
|
||||
LightCall &set_effect(const char *effect, size_t len);
|
||||
/// Set the effect of the light by its internal index number (only for internal use).
|
||||
|
||||
@@ -218,6 +218,8 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) {
|
||||
case 10:
|
||||
buf[i] = j;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -38,7 +38,7 @@ class MCP23016 final : public Component, public i2c::I2CDevice, public gpio_expa
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(MCP23016 *arg);
|
||||
static void gpio_intr(MCP23016 *arg);
|
||||
// Virtual methods from CachedGpioExpander
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
|
||||
@@ -10,7 +10,7 @@ enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANG
|
||||
|
||||
// Not a template member: GCC 10 (ESP8266) ignores section attributes on template instantiations, leaving the
|
||||
// ISR in flash.
|
||||
void IRAM_ATTR mcp23xxx_gpio_intr(Component *arg);
|
||||
void mcp23xxx_gpio_intr(Component *arg);
|
||||
|
||||
template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander::CachedGpioExpander<uint8_t, N> {
|
||||
public:
|
||||
|
||||
@@ -81,6 +81,8 @@ void Mcp4461Component::initialize_terminal_disabled(Mcp4461WiperIdx wiper, char
|
||||
case 'w':
|
||||
this->reg_[wiper_idx].terminal_w = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from collections.abc import Callable
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio
|
||||
from esphome.components.const import CONF_BITRATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ENTITY_CATEGORY,
|
||||
@@ -55,6 +56,30 @@ FORMAT_MAPPING = {
|
||||
}
|
||||
|
||||
|
||||
# MP3 constant bitrates in kbps; the encoder rounds anything else to the nearest one
|
||||
MP3_BITRATES = (
|
||||
8,
|
||||
16,
|
||||
24,
|
||||
32,
|
||||
40,
|
||||
48,
|
||||
56,
|
||||
64,
|
||||
80,
|
||||
96,
|
||||
112,
|
||||
128,
|
||||
144, # MPEG-2 only, at 16, 22.05 and 24 kHz
|
||||
160,
|
||||
192,
|
||||
224,
|
||||
256,
|
||||
320,
|
||||
)
|
||||
validate_mp3_bitrate = cv.one_of(*MP3_BITRATES, int=True)
|
||||
|
||||
|
||||
def build_supported_format_struct(
|
||||
format_config: ConfigType, purpose: MockObj
|
||||
) -> cg.StructInitializer:
|
||||
@@ -75,6 +100,10 @@ def build_supported_format_struct(
|
||||
if format_config[CONF_FORMAT] != "MP3":
|
||||
args.append(("sample_bytes", 2))
|
||||
|
||||
# Omitted means no preference, and the client keeps encoding as it always has.
|
||||
if bitrate := format_config.get(CONF_BITRATE):
|
||||
args.append(("bitrate", bitrate))
|
||||
|
||||
return cg.StructInitializer(*args)
|
||||
|
||||
|
||||
@@ -91,6 +120,14 @@ def validate_preferred_format(
|
||||
inherit_property_from(CONF_NUM_CHANNELS, audio_device_key)(config)
|
||||
inherit_property_from(CONF_SAMPLE_RATE, audio_device_key)(config)
|
||||
|
||||
# A constant bitrate is only meaningful for the one lossy format whose
|
||||
# encoder Home Assistant can be told to fix
|
||||
if CONF_BITRATE in config and config.get(CONF_FORMAT) != "MP3":
|
||||
raise cv.Invalid(
|
||||
f"{CONF_BITRATE} is only supported for the MP3 format",
|
||||
path=[CONF_BITRATE],
|
||||
)
|
||||
|
||||
# Opus only supports 48 kHz
|
||||
if config.get(CONF_FORMAT) == "OPUS" and config.get(CONF_SAMPLE_RATE) != 48000:
|
||||
raise cv.Invalid("Opus only supports a sample rate of 48000 Hz")
|
||||
|
||||
@@ -77,6 +77,7 @@ struct MediaPlayerSupportedFormat {
|
||||
uint32_t num_channels;
|
||||
MediaPlayerFormatPurpose purpose;
|
||||
uint32_t sample_bytes;
|
||||
uint32_t bitrate;
|
||||
};
|
||||
|
||||
// Base features always reported for all media players
|
||||
|
||||
@@ -244,9 +244,9 @@ class MipiSpi : public display::Display,
|
||||
char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)];
|
||||
// Don't spam the log after setup
|
||||
if (this->init_sequence_len_ == 0) {
|
||||
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
|
||||
esph_log_v(TAG, "Command %02X, length %zu, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
|
||||
} else {
|
||||
esph_log_d(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
|
||||
esph_log_d(TAG, "Command %02X, length %zu, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
|
||||
}
|
||||
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
|
||||
this->enable();
|
||||
|
||||
@@ -52,6 +52,7 @@ ST7796.extend(
|
||||
height=480,
|
||||
offset_width=49,
|
||||
draw_rounding=1,
|
||||
data_rate="40MHz",
|
||||
cs_pin=39,
|
||||
reset_pin=47,
|
||||
dc_pin=9,
|
||||
|
||||
@@ -652,7 +652,7 @@ ModbusCommandItem ModbusCommandItem::create_custom_command(
|
||||
ESP_LOGI(TAG, "Custom Command sent");
|
||||
};
|
||||
} else {
|
||||
cmd.on_data_func = handler;
|
||||
cmd.on_data_func = std::move(handler);
|
||||
}
|
||||
cmd.payload.set(values.data(), values.size());
|
||||
|
||||
@@ -669,7 +669,7 @@ ModbusCommandItem ModbusCommandItem::create_custom_command(
|
||||
ESP_LOGI(TAG, "Custom Command sent");
|
||||
};
|
||||
} else {
|
||||
cmd.on_data_func = handler;
|
||||
cmd.on_data_func = std::move(handler);
|
||||
}
|
||||
uint8_t *p = cmd.payload.init(values.size() * 2);
|
||||
for (auto v : values) {
|
||||
|
||||
@@ -28,12 +28,12 @@ std::string get_random_ha_tag_ndef() {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)];
|
||||
}
|
||||
uri += "-";
|
||||
uri += '-';
|
||||
for (int j = 0; j < 3; j++) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)];
|
||||
}
|
||||
uri += "-";
|
||||
uri += '-';
|
||||
}
|
||||
for (int i = 0; i < 12; i++) {
|
||||
uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)];
|
||||
|
||||
@@ -254,6 +254,8 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
case IRNoblexMode::IR_NOBLEX_MODE_HEAT:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class PCA6416AComponent final : public Component,
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PCA6416AComponent *arg);
|
||||
static void gpio_intr(PCA6416AComponent *arg);
|
||||
// Virtual methods from CachedGpioExpander
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
|
||||
@@ -29,7 +29,7 @@ class PCA9505Component final : public Component,
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PCA9505Component *arg);
|
||||
static void gpio_intr(PCA9505Component *arg);
|
||||
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
|
||||
@@ -38,7 +38,7 @@ class PCA9554Component final : public Component,
|
||||
#endif
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PCA9554Component *arg);
|
||||
static void gpio_intr(PCA9554Component *arg);
|
||||
|
||||
bool read_inputs_();
|
||||
bool write_register_(uint8_t reg, uint16_t value) { return this->write_address_(reg * this->reg_width_, value); }
|
||||
|
||||
@@ -33,7 +33,7 @@ class PCF8574Component final : public Component,
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PCF8574Component *arg);
|
||||
static void gpio_intr(PCF8574Component *arg);
|
||||
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
|
||||
@@ -25,7 +25,7 @@ class PI4IOE5V6408Component final : public Component,
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PI4IOE5V6408Component *arg);
|
||||
static void gpio_intr(PI4IOE5V6408Component *arg);
|
||||
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
|
||||
@@ -656,6 +656,8 @@ void Pipsolar::handle_qpiws_(const char *message) {
|
||||
this->publish_binary_sensor_(enabled, this->warning_battery_equalization_);
|
||||
value_warnings_present |= enabled.value_or(false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class QMC5883LComponent final : public PollingComponent, public i2c::I2CDevice {
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(QMC5883LComponent *arg);
|
||||
static void gpio_intr(QMC5883LComponent *arg);
|
||||
void read_sensor_();
|
||||
|
||||
QMC5883LDatarate datarate_{QMC5883L_DATARATE_10_HZ};
|
||||
|
||||
@@ -147,7 +147,7 @@ void RFBridgeComponent::finish_bucket_frame_() {
|
||||
buckets--;
|
||||
}
|
||||
if ((i < 3) || (buckets % 2) || (i == at - 1)) {
|
||||
str += " ";
|
||||
str += ' ';
|
||||
}
|
||||
}
|
||||
ESP_LOGI(TAG, "Received RFBridge Bucket: %s", str.c_str());
|
||||
|
||||
@@ -39,13 +39,16 @@ from esphome.const import (
|
||||
CONF_REFERENCE_TEMPERATURE,
|
||||
CONF_SEND_EVERY,
|
||||
CONF_SEND_FIRST_AT,
|
||||
CONF_SENSOR_ID,
|
||||
CONF_STATE_CLASS,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TEMPERATURE_OFFSET,
|
||||
CONF_TIMEOUT,
|
||||
CONF_TO,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_USE_FAHRENHEIT,
|
||||
CONF_VALUE,
|
||||
CONF_WEB_SERVER,
|
||||
CONF_WINDOW_SIZE,
|
||||
@@ -310,6 +313,7 @@ OrFilter = sensor_ns.class_("OrFilter", Filter)
|
||||
CalibrateLinearFilter = sensor_ns.class_("CalibrateLinearFilter", Filter)
|
||||
ToNTCResistanceFilter = sensor_ns.class_("ToNTCResistanceFilter", Filter)
|
||||
ToNTCTemperatureFilter = sensor_ns.class_("ToNTCTemperatureFilter", Filter)
|
||||
RHCorrectionFilter = sensor_ns.class_("RHCorrectionFilter", Filter)
|
||||
CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter)
|
||||
SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter)
|
||||
ClampFilter = sensor_ns.class_("ClampFilter", Filter)
|
||||
@@ -1281,6 +1285,23 @@ async def calibrate_ntc_temperature_filter_to_code(config, filter_id):
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"rh_correction",
|
||||
RHCorrectionFilter,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_TEMPERATURE_OFFSET): cv.templatable(cv.float_),
|
||||
cv.Required(CONF_SENSOR_ID): cv.use_id(Sensor),
|
||||
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
)
|
||||
async def rh_correction_filter_to_code(config: ConfigType, filter_id: ID) -> MockObj:
|
||||
sensor = await cg.get_variable(config[CONF_SENSOR_ID])
|
||||
offset = await cg.templatable(config[CONF_TEMPERATURE_OFFSET], [], cg.float_)
|
||||
return cg.new_Pvariable(filter_id, offset, sensor, config[CONF_USE_FAHRENHEIT])
|
||||
|
||||
|
||||
def _mean(xs):
|
||||
return sum(xs) / len(xs)
|
||||
|
||||
|
||||
@@ -493,6 +493,48 @@ optional<float> ToNTCTemperatureFilter::new_value(float value) {
|
||||
return temp;
|
||||
}
|
||||
|
||||
// RHCorrectionFilter
|
||||
static constexpr float MAGNUS_B = 17.67f, MAGNUS_C = 243.5f;
|
||||
|
||||
static bool rh_correction_temp_in_band(float t) { return t >= -40.0f && t <= 50.0f; }
|
||||
|
||||
optional<float> RHCorrectionFilter::new_value(float value) {
|
||||
if (!this->temperature_sensor_->has_state()) {
|
||||
if (!this->warned_) {
|
||||
ESP_LOGW(TAG, "RHCorrectionFilter(%p): no temperature yet, passing humidity through uncorrected", this);
|
||||
this->warned_ = true;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
float offset = this->temperature_offset_.value();
|
||||
float tc = this->temperature_sensor_->get_state();
|
||||
if (this->use_fahrenheit_) {
|
||||
tc = fahrenheit_to_celsius(tc);
|
||||
offset /= 1.8f;
|
||||
}
|
||||
const float tnc = tc - offset;
|
||||
// A NaN temperature or offset is a faulted input: keep the last corrected value
|
||||
if (std::isnan(tnc)) {
|
||||
if (!this->warned_) {
|
||||
ESP_LOGW(TAG, "RHCorrectionFilter(%p): temperature or offset is NaN, dropping humidity updates", this);
|
||||
this->warned_ = true;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (!rh_correction_temp_in_band(tc) || !rh_correction_temp_in_band(tnc)) {
|
||||
if (!this->warned_) {
|
||||
ESP_LOGW(TAG, "RHCorrectionFilter(%p): Temperature out of range -40..50 °C. Tc=%f Tnc=%f", this, tc, tnc);
|
||||
this->warned_ = true;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
this->warned_ = false;
|
||||
const float rh =
|
||||
clamp(value * std::exp(MAGNUS_B * MAGNUS_C * (tnc - tc) / ((tnc + MAGNUS_C) * (tc + MAGNUS_C))), 0.0f, 100.0f);
|
||||
ESP_LOGVV(TAG, "RHCorrectionFilter(%p)::new_value(%f) -> %f", this, value, rh);
|
||||
return rh;
|
||||
}
|
||||
|
||||
// StreamingFilter (base class)
|
||||
StreamingFilter::StreamingFilter(uint16_t window_size, uint16_t send_first_at)
|
||||
: window_size_(window_size), send_first_at_(send_first_at) {}
|
||||
|
||||
@@ -647,6 +647,26 @@ class ToNTCTemperatureFilter : public Filter {
|
||||
double c_;
|
||||
};
|
||||
|
||||
/** RH Temperature correction filter
|
||||
*
|
||||
* Allows for Relative Humidity Correction when a temperature offset is applied to the temperature sensor.
|
||||
* Uses the Magnus formula, rh = rh_nc * exp(b*(t_nc/(c+t_nc) - t/(c+t))) with b = 17.67, c = 243.5 °C;
|
||||
* see https://en.wikipedia.org/wiki/Dew_point
|
||||
*/
|
||||
class RHCorrectionFilter : public Filter {
|
||||
public:
|
||||
RHCorrectionFilter(TemplatableFn<float> offset, const Sensor *temperature_sensor, bool use_fahrenheit)
|
||||
: temperature_offset_(offset), temperature_sensor_(temperature_sensor), use_fahrenheit_(use_fahrenheit) {}
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
TemplatableFn<float> temperature_offset_;
|
||||
const Sensor *temperature_sensor_;
|
||||
bool use_fahrenheit_;
|
||||
bool warned_{false};
|
||||
};
|
||||
|
||||
/** Base class for streaming filters (batch windows where window_size == send_every).
|
||||
*
|
||||
* When window_size equals send_every, we don't need a sliding window.
|
||||
|
||||
@@ -331,7 +331,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
|
||||
this->state_ = STATE_RECEIVED_SMS;
|
||||
} else {
|
||||
if (!this->message_.empty())
|
||||
this->message_ += "\n";
|
||||
this->message_ += '\n';
|
||||
this->message_ += message;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -108,7 +108,7 @@ bool check_sml_data(const bytes &buffer) {
|
||||
|
||||
uint16_t crc_received = (buffer.at(buffer.size() - 2) << 8) | buffer.at(buffer.size() - 1);
|
||||
uint16_t crc_calculated = crc16(buffer.data() + 8, buffer.size() - 10, 0x6e23, 0x8408, true, true);
|
||||
crc_calculated = (crc_calculated >> 8) | (crc_calculated << 8);
|
||||
crc_calculated = byteswap(crc_calculated);
|
||||
if (crc_received == crc_calculated) {
|
||||
ESP_LOGV(TAG, "Checksum verification successful with CRC16/X25.");
|
||||
return true;
|
||||
|
||||
@@ -48,6 +48,8 @@ void SmlTextSensor::publish_val(const ObisInfo &obis_info) {
|
||||
publish_state(reinterpret_cast<const char *>(obis_info.value.begin()), obis_info.value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from esphome.components import (
|
||||
)
|
||||
from esphome.components.const import (
|
||||
CONF_ANNOUNCEMENT_VOLUME,
|
||||
CONF_BITRATE,
|
||||
CONF_VOLUME_INCREMENT,
|
||||
CONF_VOLUME_INITIAL,
|
||||
CONF_VOLUME_MAX,
|
||||
@@ -107,6 +108,7 @@ PIPELINE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_FORMAT, default="FLAC"): cv.enum(audio.AUDIO_FILE_TYPE_ENUM),
|
||||
cv.Optional(CONF_SAMPLE_RATE): cv.int_range(min=1),
|
||||
cv.Optional(CONF_NUM_CHANNELS): cv.int_range(1, 2),
|
||||
cv.Optional(CONF_BITRATE): media_player.validate_mp3_bitrate,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import audio, media_player, media_source, speaker
|
||||
from esphome.components.const import (
|
||||
CONF_ANNOUNCEMENT_VOLUME,
|
||||
CONF_BITRATE,
|
||||
CONF_SOURCES,
|
||||
CONF_VOLUME_INCREMENT,
|
||||
CONF_VOLUME_INITIAL,
|
||||
@@ -78,6 +79,7 @@ PIPELINE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_FORMAT, default="FLAC"): cv.enum(audio.AUDIO_FILE_TYPE_ENUM),
|
||||
cv.Optional(CONF_SAMPLE_RATE): cv.int_range(min=1),
|
||||
cv.Optional(CONF_NUM_CHANNELS): cv.int_range(1, 2),
|
||||
cv.Optional(CONF_BITRATE): media_player.validate_mp3_bitrate,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_SPEED): cv.invalid(
|
||||
"Configuring individual speeds is deprecated."
|
||||
),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): fan.validate_speed_count,
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
|
||||
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <map>
|
||||
#include <utility>
|
||||
@@ -224,8 +225,7 @@ class SPIDelegate {
|
||||
// write 16 bits
|
||||
virtual void write16(uint16_t data) {
|
||||
if (this->bit_order_ == BIT_ORDER_MSB_FIRST) {
|
||||
uint16_t buffer;
|
||||
buffer = (data >> 8) | (data << 8);
|
||||
uint16_t buffer = byteswap(data);
|
||||
this->write_array(reinterpret_cast<const uint8_t *>(&buffer), 2);
|
||||
} else {
|
||||
this->write_array(reinterpret_cast<const uint8_t *>(&data), 2);
|
||||
|
||||
@@ -106,7 +106,7 @@ class ST25R : public PollingComponent, public nfc::Nfcc {
|
||||
void finish_scan_();
|
||||
void finalize_scan_();
|
||||
void remove_tag_();
|
||||
static void IRAM_ATTR isr(ST25R *arg);
|
||||
static void isr(ST25R *arg);
|
||||
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
InternalGPIOPin *irq_pin_{nullptr};
|
||||
|
||||
@@ -107,7 +107,7 @@ class SX126x final : public Component,
|
||||
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(SX126x *arg);
|
||||
static void gpio_intr(SX126x *arg);
|
||||
void configure_fsk_ook_();
|
||||
void configure_lora_();
|
||||
void set_packet_params_(uint8_t payload_length);
|
||||
|
||||
@@ -91,7 +91,7 @@ class SX127x final : public Component,
|
||||
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(SX127x *arg);
|
||||
static void gpio_intr(SX127x *arg);
|
||||
void configure_fsk_ook_();
|
||||
void configure_lora_();
|
||||
void set_mode_(uint8_t modulation, uint8_t mode);
|
||||
|
||||
@@ -58,7 +58,7 @@ class TCA8418Component : public key_provider::KeyProvider, public Component, pub
|
||||
#endif
|
||||
|
||||
/// Wakes the loop so queued events are read. Runs in interrupt context.
|
||||
static void IRAM_ATTR interrupt_handler(TCA8418Component *arg);
|
||||
static void interrupt_handler(TCA8418Component *arg);
|
||||
|
||||
protected:
|
||||
/// Write the matrix / individual-input pin configuration. Returns false on I2C failure.
|
||||
|
||||
@@ -27,7 +27,7 @@ class TCA9555Component final : public Component,
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(TCA9555Component *arg);
|
||||
static void gpio_intr(TCA9555Component *arg);
|
||||
// Virtual methods from GpioExpander base class — names come from base
|
||||
bool digital_read_hw(uint8_t pin) override; // NOLINT(readability-identifier-naming)
|
||||
bool digital_read_cache(uint8_t pin) override; // NOLINT(readability-identifier-naming)
|
||||
|
||||
@@ -191,6 +191,8 @@ bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
case TCL112_AUTO:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto temp = TCL112_TEMP_MAX - remote_state[7];
|
||||
|
||||
@@ -39,6 +39,8 @@ void TEM3200Component::setup() {
|
||||
ESP_LOGE(TAG, "STALE data. Data has not been updated since last fetch");
|
||||
this->status_set_warning();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +129,8 @@ void TEM3200Component::update() {
|
||||
ESP_LOGE(TAG, "Warning: STALE data. Data has not been updated since last fetch");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
float temperature = convert_temperature(raw_temperature);
|
||||
|
||||
@@ -19,7 +19,7 @@ CONFIG_SCHEMA = (
|
||||
{
|
||||
cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean,
|
||||
cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean,
|
||||
cv.Optional(CONF_SPEED_COUNT): fan.validate_speed_count,
|
||||
cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1),
|
||||
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
"""VNC (RFB) client that shows a remote screen on a display."""
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, network, socket
|
||||
from esphome.components.const import (
|
||||
BYTE_ORDER_LITTLE,
|
||||
CONF_HOST,
|
||||
CONF_RECONNECT_INTERVAL,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_DISPLAY_ID,
|
||||
CONF_ID,
|
||||
CONF_ON_CONNECT,
|
||||
CONF_ON_DISCONNECT,
|
||||
CONF_PLATFORM,
|
||||
CONF_PORT,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@MindFreeze"]
|
||||
DEPENDENCIES = ["display", "network"]
|
||||
AUTO_LOAD = ["socket"]
|
||||
DOMAIN = "vnc"
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_MAX_UPDATE_DELAY = "max_update_delay"
|
||||
CONF_UPDATE_DELAY = "update_delay"
|
||||
|
||||
vnc_ns = cg.esphome_ns.namespace("vnc")
|
||||
VncClient = vnc_ns.class_("VncClient", cg.Component)
|
||||
|
||||
# Drivers that cannot take another component's pixels yet.
|
||||
UNSUPPORTED_DISPLAYS = dict.fromkeys(
|
||||
("mipi_dsi", "mipi_rgb", "mipi_spi"), "it is not supported by vnc yet"
|
||||
) | {
|
||||
"waveshare_epaper": "it draws every colour except pure black as ink; use epaper_spi"
|
||||
}
|
||||
|
||||
|
||||
def _register_writer(config: ConfigType) -> ConfigType:
|
||||
display.register_external_writer(config[CONF_DISPLAY_ID], DOMAIN)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(VncClient),
|
||||
cv.GenerateID(CONF_DISPLAY_ID): cv.use_id(display.Display),
|
||||
cv.Required(CONF_HOST): socket.ipv4_host,
|
||||
cv.Optional(CONF_PORT, default=5900): cv.port,
|
||||
cv.Optional(CONF_RECONNECT_INTERVAL, default="5s"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(min=cv.TimePeriod(seconds=1)),
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_UPDATE_DELAY, default="200ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_MAX_UPDATE_DELAY, default="1s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation({}),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
_register_writer,
|
||||
socket.consume_sockets(1, DOMAIN),
|
||||
network.require_ipv4(DOMAIN),
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
full_config = fv.full_config.get()
|
||||
disp_id = config[CONF_DISPLAY_ID]
|
||||
disp_conf = fv.get_declaring_config(disp_id)
|
||||
|
||||
if (platform := disp_conf.get(CONF_PLATFORM)) in UNSUPPORTED_DISPLAYS:
|
||||
raise cv.Invalid(
|
||||
f"Display '{disp_id}' uses the {platform} platform: "
|
||||
f"{UNSUPPORTED_DISPLAYS[platform]}",
|
||||
path=[CONF_DISPLAY_ID],
|
||||
)
|
||||
if (
|
||||
display.get_display_metadata(disp_id).has_writer
|
||||
or disp_conf.get(CONF_AUTO_CLEAR_ENABLED) is True
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"vnc draws the whole of display '{disp_id}'; remove lambda, pages, "
|
||||
"auto_clear_enabled and show_test_card from it",
|
||||
path=[CONF_DISPLAY_ID],
|
||||
)
|
||||
if len(writers := display.get_external_writers(disp_id)) > 1:
|
||||
raise cv.Invalid(
|
||||
f"Display '{disp_id}' is drawn by more than one component: {', '.join(writers)}",
|
||||
path=[CONF_DISPLAY_ID],
|
||||
)
|
||||
lvgl_configs = full_config.get("lvgl") or []
|
||||
if isinstance(lvgl_configs, dict):
|
||||
lvgl_configs = [lvgl_configs]
|
||||
for lvgl_config in lvgl_configs:
|
||||
if any(d.id == disp_id.id for d in lvgl_config.get("displays", [])):
|
||||
raise cv.Invalid(
|
||||
f"Display '{disp_id}' is also used by LVGL", path=[CONF_DISPLAY_ID]
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(CONF_ON_CONNECT, "add_on_connect_callback"),
|
||||
automation.CallbackAutomation(CONF_ON_DISCONNECT, "add_on_disconnect_callback"),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
disp = await cg.get_variable(config[CONF_DISPLAY_ID])
|
||||
# Ask the server for the display's own byte order, so block-copying drivers skip the per-pixel path
|
||||
meta = display.get_display_metadata(config[CONF_DISPLAY_ID])
|
||||
var = cg.new_Pvariable(config[CONF_ID], disp, meta.byte_order != BYTE_ORDER_LITTLE)
|
||||
await cg.register_component(var, config)
|
||||
socket.require_tcp_client_link()
|
||||
cg.add(var.set_host(config[CONF_HOST]))
|
||||
cg.add(var.set_port(config[CONF_PORT]))
|
||||
cg.add(var.set_reconnect_interval(config[CONF_RECONNECT_INTERVAL]))
|
||||
cg.add(var.set_update_delay(config[CONF_UPDATE_DELAY]))
|
||||
cg.add(var.set_max_update_delay(config[CONF_MAX_UPDATE_DELAY]))
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
VNC_ID_SCHEMA = automation.maybe_simple_id(
|
||||
{cv.GenerateID(): cv.use_id(VncClient)},
|
||||
)
|
||||
|
||||
automation.register_apply_condition("vnc.is_connected", VNC_ID_SCHEMA, "is_connected()")
|
||||
@@ -0,0 +1,702 @@
|
||||
#include "vnc_client.h"
|
||||
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::vnc {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "vnc");
|
||||
|
||||
// Below the 50 ms blocking warning, and the same slice it8951 uses for its transfers.
|
||||
static constexpr uint32_t LOOP_BUDGET_MS = 20;
|
||||
static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000;
|
||||
static constexpr uint32_t STABLE_SESSION_MS = 60000;
|
||||
static constexpr uint32_t MAX_BACKOFF_MS = 60000;
|
||||
static constexpr uint32_t CONFIG_RETRY_MS = 300000;
|
||||
static constexpr uint32_t HEARTBEAT_MS = 60000;
|
||||
static constexpr uint32_t MIN_REQUEST_SPACING_MS = 50;
|
||||
// The longest the first refresh waits for a display with a loop to park once after boot.
|
||||
static constexpr uint32_t DISPLAY_START_MS = 5000;
|
||||
// Longer than any driver's own busy timeout: a panel that stays busy this long is stuck
|
||||
static constexpr uint32_t DISPLAY_BUSY_TIMEOUT_MS = 120000;
|
||||
static constexpr uint16_t MAX_CURSOR_SIDE = 1024;
|
||||
static constexpr uint32_t MAX_TEXT_LEN = 63;
|
||||
|
||||
static constexpr uint8_t SECURITY_NONE = 1;
|
||||
static constexpr uint8_t SECURITY_VNC_AUTH = 2;
|
||||
|
||||
static constexpr int32_t ENCODING_RAW = 0;
|
||||
static constexpr int32_t ENCODING_RICH_CURSOR = -239;
|
||||
|
||||
static constexpr uint8_t MSG_FRAMEBUFFER_UPDATE = 0;
|
||||
static constexpr uint8_t MSG_SET_COLOUR_MAP = 1;
|
||||
static constexpr uint8_t MSG_BELL = 2;
|
||||
static constexpr uint8_t MSG_SERVER_CUT_TEXT = 3;
|
||||
|
||||
static inline uint16_t read_be16(const uint8_t *p) { return encode_uint16(p[0], p[1]); }
|
||||
static inline uint32_t read_be32(const uint8_t *p) { return encode_uint32(p[0], p[1], p[2], p[3]); }
|
||||
static inline void write_be16(uint8_t *p, uint16_t v) {
|
||||
p[0] = v >> 8;
|
||||
p[1] = v;
|
||||
}
|
||||
static inline void write_be32(uint8_t *p, uint32_t v) {
|
||||
p[0] = v >> 24;
|
||||
p[1] = v >> 16;
|
||||
p[2] = v >> 8;
|
||||
p[3] = v;
|
||||
}
|
||||
|
||||
static int parse_three_digits(const uint8_t *p) {
|
||||
int value = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (p[i] < '0' || p[i] > '9') {
|
||||
return -1;
|
||||
}
|
||||
value = value * 10 + (p[i] - '0');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void VncClient::setup() {
|
||||
this->setup_ms_ = App.get_loop_component_start_time();
|
||||
this->link_.begin(TAG);
|
||||
}
|
||||
|
||||
void VncClient::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"VNC:\n"
|
||||
" Server: %s:%u\n"
|
||||
" Reconnect Interval: %" PRIu32 "ms\n"
|
||||
" Update Delay: %" PRIu32 "ms\n"
|
||||
" Max Update Delay: %" PRIu32 "ms",
|
||||
this->link_.host(), this->link_.port(), this->link_.reconnect_interval(), this->update_delay_ms_,
|
||||
this->max_update_delay_ms_);
|
||||
}
|
||||
|
||||
void VncClient::on_shutdown() { this->link_.close(); }
|
||||
|
||||
void VncClient::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
this->pass_start_ms_ = millis();
|
||||
if (this->display_->is_failed()) {
|
||||
this->link_.close(); // a connect still in flight would otherwise keep its socket for good
|
||||
if (this->state_ != VncState::VNC_STATE_IDLE) {
|
||||
this->session_end_(now, VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Display failed"));
|
||||
}
|
||||
this->status_set_error(LOG_STR("Display failed"));
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
// A display that parks its loop when idle is busy while that loop runs. Displays that never
|
||||
// park (no loop, or one that always runs) update synchronously, so they are never busy.
|
||||
if (this->display_->is_idle()) {
|
||||
this->display_parks_ = true;
|
||||
this->display_busy_ = false;
|
||||
} else {
|
||||
this->display_busy_ = this->display_parks_;
|
||||
}
|
||||
// A stuck panel, or another owner of the display's loop, would otherwise freeze the screen silently
|
||||
if (!this->display_busy_) {
|
||||
this->busy_since_ms_ = now;
|
||||
if (this->busy_warned_) {
|
||||
this->busy_warned_ = false;
|
||||
// The connection path owns the warning while no session runs
|
||||
if (this->state_ == VncState::VNC_STATE_RUNNING) {
|
||||
this->status_clear_warning();
|
||||
}
|
||||
}
|
||||
} else if (!this->busy_warned_ && now - this->busy_since_ms_ >= DISPLAY_BUSY_TIMEOUT_MS) {
|
||||
this->busy_warned_ = true;
|
||||
this->status_set_warning(LOG_STR("Display busy too long"));
|
||||
}
|
||||
|
||||
if (!this->link_.connected()) {
|
||||
if (this->state_ != VncState::VNC_STATE_IDLE) {
|
||||
this->session_lost_(now);
|
||||
}
|
||||
this->connect_step_(now);
|
||||
if (!this->link_.connected()) {
|
||||
return;
|
||||
}
|
||||
this->start_handshake_(now);
|
||||
}
|
||||
if (this->state_ == VncState::VNC_STATE_HANDSHAKE && now - this->session_start_ms_ >= HANDSHAKE_TIMEOUT_MS) {
|
||||
this->fail_(this->lost_failure_(), LOG_STR("Handshake timed out"));
|
||||
return;
|
||||
}
|
||||
|
||||
const bool blocked = this->in_update_ && this->display_busy_;
|
||||
// A display still starting up (it8951) can go from its start-up straight into a refresh without parking.
|
||||
const bool display_starting =
|
||||
!this->display_parks_ && this->display_->has_overridden_loop() && now - this->setup_ms_ < DISPLAY_START_MS;
|
||||
// Wait until the screen stops changing, so an animation becomes one refresh showing where it ended.
|
||||
if (this->refresh_pending_ && !this->in_update_ && !this->display_busy_ && !display_starting &&
|
||||
(now - this->last_change_ms_ >= this->update_delay_ms_ ||
|
||||
now - this->first_change_ms_ >= this->max_update_delay_ms_)) {
|
||||
// Synchronous displays flush inside update(), so this pass does nothing else.
|
||||
this->refresh_pending_ = false;
|
||||
this->display_->update();
|
||||
this->display_busy_ = this->display_parks_ && !this->display_->is_idle();
|
||||
} else if (!blocked && (this->rx_pending_ || this->link_.ready())) {
|
||||
this->service_rx_(now);
|
||||
}
|
||||
if (this->state_ == VncState::VNC_STATE_RUNNING) {
|
||||
// Mid-frame there is nothing to send, so a vanished server would never fail a write.
|
||||
if (this->in_update_ && !blocked && !this->rx_pending_ && now - this->last_rx_ms_ >= HEARTBEAT_MS) {
|
||||
this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Server stopped sending"));
|
||||
return;
|
||||
}
|
||||
this->pace_(now);
|
||||
}
|
||||
this->link_.flush_tx();
|
||||
if (this->rx_pending_ && !(this->in_update_ && this->display_busy_)) {
|
||||
this->high_freq_.start();
|
||||
} else {
|
||||
this->high_freq_.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::connect_step_(uint32_t now) {
|
||||
const bool up = network::is_connected();
|
||||
if (up && !this->network_was_up_) {
|
||||
this->backoff_ms_ = 0;
|
||||
this->hold_ms_ = 0;
|
||||
}
|
||||
this->network_was_up_ = up;
|
||||
if (!up) {
|
||||
return;
|
||||
}
|
||||
if (!this->link_.connecting() && (now - this->hold_start_ms_ < this->hold_ms_ || this->link_.in_backoff())) {
|
||||
return;
|
||||
}
|
||||
this->link_.poll();
|
||||
// DNS failures, refused connects and connect timeouts all leave no socket and start the link's backoff.
|
||||
if (!this->link_.connected() && !this->link_.connecting() && this->link_.in_backoff()) {
|
||||
this->session_end_(now, VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Cannot reach the server"));
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::reset_session_() {
|
||||
this->rx_pos_ = 0;
|
||||
this->rx_len_ = 0;
|
||||
this->rx_pending_ = false;
|
||||
this->in_update_ = false;
|
||||
this->request_outstanding_ = false;
|
||||
this->refresh_pending_ = false;
|
||||
this->frame_drawn_ = false;
|
||||
this->step_ = VncStep::VNC_STEP_VERSION;
|
||||
this->reason_failure_ = VncFailure::VNC_FAILURE_TRANSIENT;
|
||||
this->high_freq_.stop();
|
||||
}
|
||||
|
||||
void VncClient::start_handshake_(uint32_t now) {
|
||||
this->reset_session_();
|
||||
this->state_ = VncState::VNC_STATE_HANDSHAKE;
|
||||
this->session_start_ms_ = now;
|
||||
this->last_rx_ms_ = now;
|
||||
}
|
||||
|
||||
VncFailure VncClient::lost_failure_() const {
|
||||
if (this->state_ == VncState::VNC_STATE_HANDSHAKE) {
|
||||
if (this->step_ == VncStep::VNC_STEP_REASON_LENGTH || this->step_ == VncStep::VNC_STEP_REASON) {
|
||||
return this->reason_failure_;
|
||||
}
|
||||
}
|
||||
return VncFailure::VNC_FAILURE_TRANSIENT;
|
||||
}
|
||||
|
||||
void VncClient::session_lost_(uint32_t now) {
|
||||
this->session_end_(now, this->lost_failure_(), LOG_STR("Connection lost"));
|
||||
}
|
||||
|
||||
VncParse VncClient::fail_(VncFailure failure, const LogString *reason) {
|
||||
this->link_.close();
|
||||
this->link_.note_attempt();
|
||||
this->session_end_(App.get_loop_component_start_time(), failure, reason);
|
||||
return VncParse::VNC_PARSE_STOP;
|
||||
}
|
||||
|
||||
void VncClient::session_end_(uint32_t now, VncFailure failure, const LogString *reason) {
|
||||
const bool was_running = this->state_ == VncState::VNC_STATE_RUNNING;
|
||||
this->reset_session_();
|
||||
this->state_ = VncState::VNC_STATE_IDLE;
|
||||
this->hold_start_ms_ = now;
|
||||
if (was_running) {
|
||||
const uint32_t lasted = now - this->session_start_ms_;
|
||||
ESP_LOGW(TAG, "%s after %" PRIu32 "s", LOG_STR_ARG(reason), lasted / 1000);
|
||||
this->disconnect_callback_.call();
|
||||
this->status_set_warning();
|
||||
if (lasted >= STABLE_SESSION_MS) {
|
||||
this->backoff_ms_ = 0;
|
||||
this->hold_ms_ = 0;
|
||||
return;
|
||||
}
|
||||
failure = VncFailure::VNC_FAILURE_TRANSIENT;
|
||||
}
|
||||
switch (failure) {
|
||||
case VncFailure::VNC_FAILURE_CONFIG:
|
||||
this->hold_ms_ = CONFIG_RETRY_MS;
|
||||
ESP_LOGE(TAG, "%s; retrying in %" PRIu32 "s", LOG_STR_ARG(reason), this->hold_ms_ / 1000);
|
||||
this->status_set_error(reason);
|
||||
return;
|
||||
default: {
|
||||
const uint32_t base = this->link_.reconnect_interval();
|
||||
this->backoff_ms_ =
|
||||
this->backoff_ms_ == 0 ? base : std::min(this->backoff_ms_ * 2, std::max(MAX_BACKOFF_MS, base));
|
||||
this->hold_ms_ = this->backoff_ms_;
|
||||
if (!was_running) {
|
||||
ESP_LOGW(TAG, "%s; retrying in %" PRIu32 "s", LOG_STR_ARG(reason), this->hold_ms_ / 1000);
|
||||
}
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool VncClient::budget_exhausted_() const { return millis() - this->pass_start_ms_ >= LOOP_BUDGET_MS; }
|
||||
|
||||
void VncClient::service_rx_(uint32_t now) {
|
||||
for (;;) {
|
||||
VncParse result = this->parse_();
|
||||
if (result == VncParse::VNC_PARSE_STOP) {
|
||||
return;
|
||||
}
|
||||
if (result == VncParse::VNC_PARSE_PAUSE) {
|
||||
this->rx_pending_ = true;
|
||||
return;
|
||||
}
|
||||
// Less than one item is left, so the buffer always has room after compacting.
|
||||
if (this->rx_pos_ != 0) {
|
||||
this->rx_len_ -= this->rx_pos_;
|
||||
std::memmove(this->rx_, this->rx_ + this->rx_pos_, this->rx_len_);
|
||||
this->rx_pos_ = 0;
|
||||
}
|
||||
if (this->budget_exhausted_()) {
|
||||
this->rx_pending_ = true;
|
||||
return;
|
||||
}
|
||||
ssize_t count = this->link_.read(this->rx_ + this->rx_len_, RX_BUFFER_SIZE - this->rx_len_);
|
||||
if (count < 0) {
|
||||
this->session_lost_(now);
|
||||
return;
|
||||
}
|
||||
if (count == 0) {
|
||||
this->rx_pending_ = false;
|
||||
return;
|
||||
}
|
||||
this->rx_len_ += static_cast<uint16_t>(count);
|
||||
this->rx_total_ += count;
|
||||
this->last_rx_ms_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::skip_(uint32_t count, VncStep next) {
|
||||
this->skip_left_ = count;
|
||||
this->after_skip_ = next;
|
||||
this->step_ = VncStep::VNC_STEP_SKIP;
|
||||
}
|
||||
|
||||
bool VncClient::send_client_init_() {
|
||||
const uint8_t shared = 1;
|
||||
return this->send_(&shared, 1);
|
||||
}
|
||||
|
||||
VncParse VncClient::parse_() {
|
||||
for (;;) {
|
||||
const uint8_t *p = this->rx_ + this->rx_pos_;
|
||||
const size_t avail = this->rx_len_ - this->rx_pos_;
|
||||
switch (this->step_) {
|
||||
case VncStep::VNC_STEP_VERSION: {
|
||||
if (avail < 12) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
const int major = parse_three_digits(p + 4);
|
||||
const int minor = parse_three_digits(p + 8);
|
||||
if (std::memcmp(p, "RFB ", 4) != 0 || p[7] != '.' || p[11] != '\n' || major < 0 || minor < 0) {
|
||||
return this->fail_(VncFailure::VNC_FAILURE_CONFIG, LOG_STR("Not a VNC server"));
|
||||
}
|
||||
if (major < 3) {
|
||||
return this->fail_(VncFailure::VNC_FAILURE_CONFIG, LOG_STR("Unsupported RFB version"));
|
||||
}
|
||||
this->minor_ = (major > 3 || minor >= 8) ? 8 : (minor == 7 ? 7 : 3);
|
||||
this->rx_pos_ += 12;
|
||||
uint8_t reply[12] = {'R', 'F', 'B', ' ', '0', '0', '3', '.', '0', '0', '8', '\n'};
|
||||
reply[10] = '0' + this->minor_;
|
||||
if (!this->send_(reply, sizeof(reply))) {
|
||||
return this->fail_send_();
|
||||
}
|
||||
this->step_ = this->minor_ == 3 ? VncStep::VNC_STEP_SECURITY_TYPE_33 : VncStep::VNC_STEP_SECURITY_COUNT;
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SECURITY_COUNT: {
|
||||
if (avail < 1) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
this->sec_count_ = p[0];
|
||||
this->rx_pos_ += 1;
|
||||
if (this->sec_count_ == 0) {
|
||||
// A refusal before authentication, such as a lockout caused by another client.
|
||||
this->step_ = VncStep::VNC_STEP_REASON_LENGTH;
|
||||
} else {
|
||||
this->step_ = VncStep::VNC_STEP_SECURITY_LIST;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SECURITY_LIST: {
|
||||
if (avail < this->sec_count_) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
uint8_t chosen = 0;
|
||||
bool wants_password = false;
|
||||
for (uint8_t i = 0; i < this->sec_count_; i++) {
|
||||
if (p[i] == SECURITY_NONE) {
|
||||
chosen = p[i];
|
||||
break;
|
||||
}
|
||||
wants_password |= p[i] == SECURITY_VNC_AUTH;
|
||||
}
|
||||
this->rx_pos_ += this->sec_count_;
|
||||
if (chosen == 0) {
|
||||
return this->fail_(VncFailure::VNC_FAILURE_CONFIG, wants_password ? LOG_STR("The server requires a password")
|
||||
: LOG_STR("No supported security type"));
|
||||
}
|
||||
if (!this->send_(&chosen, 1)) {
|
||||
return this->fail_send_();
|
||||
}
|
||||
if (this->minor_ == 8) {
|
||||
this->step_ = VncStep::VNC_STEP_SECURITY_RESULT;
|
||||
} else {
|
||||
if (!this->send_client_init_()) {
|
||||
return this->fail_send_();
|
||||
}
|
||||
this->step_ = VncStep::VNC_STEP_SERVER_INIT;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SECURITY_TYPE_33: {
|
||||
if (avail < 4) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
const uint32_t type = read_be32(p);
|
||||
this->rx_pos_ += 4;
|
||||
if (type == 0) {
|
||||
this->step_ = VncStep::VNC_STEP_REASON_LENGTH;
|
||||
} else if (type == SECURITY_NONE) {
|
||||
if (!this->send_client_init_()) {
|
||||
return this->fail_send_();
|
||||
}
|
||||
this->step_ = VncStep::VNC_STEP_SERVER_INIT;
|
||||
} else {
|
||||
return this->fail_(VncFailure::VNC_FAILURE_CONFIG, type == SECURITY_VNC_AUTH
|
||||
? LOG_STR("The server requires a password")
|
||||
: LOG_STR("No supported security type"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SECURITY_RESULT: {
|
||||
if (avail < 4) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
const uint32_t result = read_be32(p);
|
||||
this->rx_pos_ += 4;
|
||||
if (result == 0) {
|
||||
if (!this->send_client_init_()) {
|
||||
return this->fail_send_();
|
||||
}
|
||||
this->step_ = VncStep::VNC_STEP_SERVER_INIT;
|
||||
break;
|
||||
}
|
||||
this->reason_failure_ = VncFailure::VNC_FAILURE_CONFIG;
|
||||
this->step_ = VncStep::VNC_STEP_REASON_LENGTH;
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_REASON_LENGTH: {
|
||||
if (avail < 4) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
this->text_len_ = static_cast<uint8_t>(std::min(read_be32(p), MAX_TEXT_LEN));
|
||||
this->rx_pos_ += 4;
|
||||
this->step_ = VncStep::VNC_STEP_REASON;
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_REASON: {
|
||||
if (avail < this->text_len_) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
ESP_LOGE(TAG, "Server: %.*s", static_cast<int>(this->text_len_), reinterpret_cast<const char *>(p));
|
||||
return this->fail_(this->reason_failure_, LOG_STR("Connection refused"));
|
||||
}
|
||||
case VncStep::VNC_STEP_SERVER_INIT: {
|
||||
if (avail < 24) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
this->fb_w_ = read_be16(p);
|
||||
this->fb_h_ = read_be16(p + 2);
|
||||
// Reused for the length of the name that follows.
|
||||
this->skip_left_ = read_be32(p + 20);
|
||||
this->rx_pos_ += 24;
|
||||
if (this->fb_w_ == 0 || this->fb_h_ == 0) {
|
||||
return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("The server screen is empty"));
|
||||
}
|
||||
this->text_len_ = static_cast<uint8_t>(std::min(this->skip_left_, MAX_TEXT_LEN));
|
||||
this->step_ = VncStep::VNC_STEP_SERVER_NAME;
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SERVER_NAME: {
|
||||
if (avail < this->text_len_) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
ESP_LOGI(TAG, "Server '%.*s', %ux%u, RFB 3.%u", static_cast<int>(this->text_len_),
|
||||
reinterpret_cast<const char *>(p), this->fb_w_, this->fb_h_, this->minor_);
|
||||
this->rx_pos_ += this->text_len_;
|
||||
this->skip_(this->skip_left_ - this->text_len_, VncStep::VNC_STEP_SESSION_START);
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SESSION_START:
|
||||
if (!this->start_session_(App.get_loop_component_start_time())) {
|
||||
return this->fail_send_();
|
||||
}
|
||||
this->step_ = VncStep::VNC_STEP_MESSAGE;
|
||||
break;
|
||||
case VncStep::VNC_STEP_MESSAGE: {
|
||||
if (avail < 1) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
switch (p[0]) {
|
||||
case MSG_FRAMEBUFFER_UPDATE:
|
||||
if (avail < 4) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
this->rects_left_ = read_be16(p + 2);
|
||||
this->rx_pos_ += 4;
|
||||
this->in_update_ = true;
|
||||
this->update_start_ms_ = millis();
|
||||
this->update_start_bytes_ = this->rx_total_ - (this->rx_len_ - this->rx_pos_);
|
||||
this->rect_done_();
|
||||
break;
|
||||
case MSG_SET_COLOUR_MAP:
|
||||
if (avail < 6) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
this->rx_pos_ += 6;
|
||||
this->skip_(6u * read_be16(p + 4), VncStep::VNC_STEP_MESSAGE);
|
||||
break;
|
||||
case MSG_BELL:
|
||||
this->rx_pos_ += 1;
|
||||
break;
|
||||
case MSG_SERVER_CUT_TEXT:
|
||||
if (avail < 8) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
this->rx_pos_ += 8;
|
||||
this->skip_(read_be32(p + 4), VncStep::VNC_STEP_MESSAGE);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown message type %u", p[0]);
|
||||
return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Protocol error"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_RECT: {
|
||||
if (this->display_busy_) {
|
||||
return VncParse::VNC_PARSE_PAUSE;
|
||||
}
|
||||
if (avail < 12) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
this->rect_x_ = read_be16(p);
|
||||
this->rect_y_ = read_be16(p + 2);
|
||||
this->rect_w_ = read_be16(p + 4);
|
||||
this->rect_h_ = read_be16(p + 6);
|
||||
const auto encoding = static_cast<int32_t>(read_be32(p + 8));
|
||||
this->rx_pos_ += 12;
|
||||
this->rects_left_--;
|
||||
if (encoding == ENCODING_RAW) {
|
||||
if (static_cast<uint32_t>(this->rect_x_) + this->rect_w_ > this->fb_w_ ||
|
||||
static_cast<uint32_t>(this->rect_y_) + this->rect_h_ > this->fb_h_) {
|
||||
ESP_LOGW(TAG, "Rectangle %ux%u at %u,%u is outside the %ux%u screen", this->rect_w_, this->rect_h_,
|
||||
this->rect_x_, this->rect_y_, this->fb_w_, this->fb_h_);
|
||||
return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Protocol error"));
|
||||
}
|
||||
if (this->rect_w_ == 0 || this->rect_h_ == 0) {
|
||||
this->rect_done_();
|
||||
} else {
|
||||
this->pos_x_ = 0;
|
||||
this->pos_y_ = 0;
|
||||
this->step_ = VncStep::VNC_STEP_RAW;
|
||||
}
|
||||
} else if (encoding == ENCODING_RICH_CURSOR) {
|
||||
if (this->rect_w_ > MAX_CURSOR_SIDE || this->rect_h_ > MAX_CURSOR_SIDE) {
|
||||
return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Protocol error"));
|
||||
}
|
||||
const uint32_t size = static_cast<uint32_t>(this->rect_w_) * this->rect_h_ * this->bpp_ +
|
||||
((this->rect_w_ + 7u) / 8u) * this->rect_h_;
|
||||
this->skip_(size, VncStep::VNC_STEP_RECT_DONE);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unsupported encoding %" PRId32, encoding);
|
||||
return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Protocol error"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_RECT_DONE:
|
||||
this->rect_done_();
|
||||
break;
|
||||
case VncStep::VNC_STEP_RAW: {
|
||||
if (this->display_busy_) {
|
||||
return VncParse::VNC_PARSE_PAUSE;
|
||||
}
|
||||
const size_t pixels = avail / this->bpp_;
|
||||
if (pixels == 0) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
if (this->budget_exhausted_()) {
|
||||
return VncParse::VNC_PARSE_PAUSE;
|
||||
}
|
||||
const auto count = static_cast<uint16_t>(std::min<size_t>(pixels, this->rect_w_ - this->pos_x_));
|
||||
this->draw_row_(this->rect_x_ + this->pos_x_, this->rect_y_ + this->pos_y_, count, p);
|
||||
this->rx_pos_ += count * this->bpp_;
|
||||
this->pos_x_ += count;
|
||||
if (this->pos_x_ == this->rect_w_) {
|
||||
this->pos_x_ = 0;
|
||||
if (++this->pos_y_ == this->rect_h_) {
|
||||
this->rect_done_();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SKIP: {
|
||||
const auto count = static_cast<uint32_t>(std::min<size_t>(avail, this->skip_left_));
|
||||
this->rx_pos_ += count;
|
||||
this->skip_left_ -= count;
|
||||
if (this->skip_left_ != 0) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
this->step_ = this->after_skip_;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::rect_done_() {
|
||||
if (this->rects_left_ == 0) {
|
||||
this->frame_done_();
|
||||
} else {
|
||||
this->step_ = VncStep::VNC_STEP_RECT;
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::frame_done_() {
|
||||
this->in_update_ = false;
|
||||
this->request_outstanding_ = false;
|
||||
this->step_ = VncStep::VNC_STEP_MESSAGE;
|
||||
if (this->frame_drawn_) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
this->frame_drawn_ = false;
|
||||
if (!this->refresh_pending_) {
|
||||
this->first_change_ms_ = now;
|
||||
}
|
||||
this->last_change_ms_ = now;
|
||||
this->refresh_pending_ = true;
|
||||
ESP_LOGD(TAG, "Update: %" PRIu32 " bytes in %" PRIu32 "ms",
|
||||
this->rx_total_ - (this->rx_len_ - this->rx_pos_) - this->update_start_bytes_,
|
||||
millis() - this->update_start_ms_);
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::draw_row_(uint16_t x, uint16_t y, uint16_t count, const uint8_t *pixels) {
|
||||
if (y >= this->vis_h_ || x >= this->vis_w_) {
|
||||
return;
|
||||
}
|
||||
const int visible = std::min<int>(count, this->vis_w_ - x);
|
||||
this->display_->draw_pixels_at(x, y, visible, 1, pixels, this->bitness_, this->big_endian_);
|
||||
this->frame_drawn_ = true;
|
||||
}
|
||||
|
||||
bool VncClient::start_session_(uint32_t now) {
|
||||
const bool rgb565 = this->display_->get_display_type() != display::DISPLAY_TYPE_BINARY;
|
||||
this->bpp_ = rgb565 ? 2 : 1;
|
||||
this->bitness_ = rgb565 ? display::COLOR_BITNESS_565 : display::COLOR_BITNESS_332;
|
||||
|
||||
const int disp_w = this->display_->get_width();
|
||||
const int disp_h = this->display_->get_height();
|
||||
this->vis_w_ = std::min<int>(this->fb_w_, disp_w);
|
||||
this->vis_h_ = std::min<int>(this->fb_h_, disp_h);
|
||||
if (this->fb_w_ != disp_w || this->fb_h_ != disp_h) {
|
||||
ESP_LOGW(TAG, "Server screen is %ux%u but the display is %dx%d; showing the top-left %ux%u", this->fb_w_,
|
||||
this->fb_h_, disp_w, disp_h, this->vis_w_, this->vis_h_);
|
||||
}
|
||||
|
||||
// SetPixelFormat: true colour, in the byte order the display takes
|
||||
uint8_t pixel_format[20] = {};
|
||||
pixel_format[4] = rgb565 ? 16 : 8;
|
||||
pixel_format[5] = rgb565 ? 16 : 8;
|
||||
pixel_format[6] = this->big_endian_;
|
||||
pixel_format[7] = 1;
|
||||
write_be16(pixel_format + 8, rgb565 ? 31 : 7);
|
||||
write_be16(pixel_format + 10, rgb565 ? 63 : 7);
|
||||
write_be16(pixel_format + 12, rgb565 ? 31 : 3);
|
||||
pixel_format[14] = rgb565 ? 11 : 5;
|
||||
pixel_format[15] = rgb565 ? 5 : 2;
|
||||
if (!this->send_(pixel_format, sizeof(pixel_format))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SetEncodings: TightVNC uses the first real encoding it supports for every rectangle.
|
||||
uint8_t encodings[12] = {2, 0, 0, 2};
|
||||
write_be32(encodings + 4, ENCODING_RAW);
|
||||
write_be32(encodings + 8, static_cast<uint32_t>(ENCODING_RICH_CURSOR));
|
||||
if (!this->send_(encodings, sizeof(encodings)) || !this->send_update_request_(false, now)) {
|
||||
return false;
|
||||
}
|
||||
this->state_ = VncState::VNC_STATE_RUNNING;
|
||||
this->session_start_ms_ = now;
|
||||
if (!this->busy_warned_) {
|
||||
this->status_clear_warning(); // a stuck display keeps its warning through a reconnect
|
||||
}
|
||||
this->status_clear_error();
|
||||
this->connect_callback_.call();
|
||||
return true;
|
||||
}
|
||||
|
||||
void VncClient::pace_(uint32_t now) {
|
||||
if (this->display_busy_ || this->in_update_ || now - this->last_request_ms_ < MIN_REQUEST_SPACING_MS) {
|
||||
return;
|
||||
}
|
||||
// Repeating a parked request costs nothing on the server and detects a dead link.
|
||||
if (!this->request_outstanding_ ||
|
||||
(now - this->last_rx_ms_ >= HEARTBEAT_MS && now - this->last_request_ms_ >= HEARTBEAT_MS)) {
|
||||
this->send_update_request_(true, now);
|
||||
}
|
||||
}
|
||||
|
||||
bool VncClient::send_(const uint8_t *data, size_t len) {
|
||||
if (this->link_.tx_free() < len) {
|
||||
this->link_.flush_tx();
|
||||
if (this->link_.tx_free() < len) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this->link_.queue(data, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VncClient::send_update_request_(bool incremental, uint32_t now) {
|
||||
uint8_t msg[10] = {3, static_cast<uint8_t>(incremental ? 1 : 0)};
|
||||
write_be16(msg + 6, this->vis_w_);
|
||||
write_be16(msg + 8, this->vis_h_);
|
||||
if (!this->send_(msg, sizeof(msg))) {
|
||||
return false;
|
||||
}
|
||||
this->request_outstanding_ = true;
|
||||
this->last_request_ms_ = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::vnc
|
||||
@@ -0,0 +1,162 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/socket/tcp_client_link.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::vnc {
|
||||
|
||||
enum class VncState : uint8_t {
|
||||
VNC_STATE_IDLE,
|
||||
VNC_STATE_HANDSHAKE,
|
||||
VNC_STATE_RUNNING,
|
||||
};
|
||||
|
||||
enum class VncStep : uint8_t {
|
||||
VNC_STEP_VERSION,
|
||||
VNC_STEP_SECURITY_COUNT,
|
||||
VNC_STEP_SECURITY_LIST,
|
||||
VNC_STEP_SECURITY_TYPE_33,
|
||||
VNC_STEP_SECURITY_RESULT,
|
||||
VNC_STEP_REASON_LENGTH,
|
||||
VNC_STEP_REASON,
|
||||
VNC_STEP_SERVER_INIT,
|
||||
VNC_STEP_SERVER_NAME,
|
||||
VNC_STEP_SESSION_START,
|
||||
VNC_STEP_MESSAGE,
|
||||
VNC_STEP_RECT,
|
||||
VNC_STEP_RECT_DONE,
|
||||
VNC_STEP_RAW,
|
||||
VNC_STEP_SKIP,
|
||||
};
|
||||
|
||||
enum class VncParse : uint8_t {
|
||||
VNC_PARSE_MORE, // the next step needs bytes that have not arrived
|
||||
VNC_PARSE_PAUSE, // out of time, or the display is busy; resume without new data
|
||||
VNC_PARSE_STOP, // the session ended
|
||||
};
|
||||
|
||||
enum class VncFailure : uint8_t {
|
||||
VNC_FAILURE_TRANSIENT,
|
||||
VNC_FAILURE_CONFIG,
|
||||
};
|
||||
|
||||
/// RFB (VNC) client that draws the remote framebuffer into a display.
|
||||
class VncClient : public Component {
|
||||
public:
|
||||
explicit VncClient(display::Display *display, bool big_endian = false) : display_(display), big_endian_(big_endian) {}
|
||||
|
||||
void set_host(const char *host) { this->link_.set_host(host); }
|
||||
void set_port(uint16_t port) { this->link_.set_port(port); }
|
||||
void set_reconnect_interval(uint32_t ms) { this->link_.set_reconnect_interval(ms); }
|
||||
void set_update_delay(uint32_t ms) { this->update_delay_ms_ = ms; }
|
||||
void set_max_update_delay(uint32_t ms) { this->max_update_delay_ms_ = ms; }
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void on_shutdown() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
bool is_connected() const { return this->state_ == VncState::VNC_STATE_RUNNING; }
|
||||
|
||||
template<typename F> void add_on_connect_callback(F &&callback) {
|
||||
this->connect_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
template<typename F> void add_on_disconnect_callback(F &&callback) {
|
||||
this->disconnect_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
protected:
|
||||
static constexpr size_t RX_BUFFER_SIZE = 512;
|
||||
|
||||
void connect_step_(uint32_t now);
|
||||
void start_handshake_(uint32_t now);
|
||||
void service_rx_(uint32_t now);
|
||||
VncParse parse_();
|
||||
void pace_(uint32_t now);
|
||||
bool start_session_(uint32_t now);
|
||||
bool send_client_init_();
|
||||
void frame_done_();
|
||||
void rect_done_();
|
||||
void draw_row_(uint16_t x, uint16_t y, uint16_t count, const uint8_t *pixels);
|
||||
void skip_(uint32_t count, VncStep next);
|
||||
bool budget_exhausted_() const;
|
||||
bool send_(const uint8_t *data, size_t len);
|
||||
bool send_update_request_(bool incremental, uint32_t now);
|
||||
VncParse fail_(VncFailure failure, const LogString *reason);
|
||||
VncParse fail_send_() { return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Send failed")); }
|
||||
VncFailure lost_failure_() const;
|
||||
void session_lost_(uint32_t now);
|
||||
void session_end_(uint32_t now, VncFailure failure, const LogString *reason);
|
||||
void reset_session_();
|
||||
|
||||
display::Display *display_;
|
||||
socket::TcpClientLink link_;
|
||||
LazyCallbackManager<void()> connect_callback_;
|
||||
LazyCallbackManager<void()> disconnect_callback_;
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
|
||||
uint32_t update_delay_ms_{200};
|
||||
uint32_t max_update_delay_ms_{1000};
|
||||
uint32_t backoff_ms_{0};
|
||||
uint32_t hold_ms_{0};
|
||||
uint32_t hold_start_ms_{0};
|
||||
uint32_t setup_ms_{0};
|
||||
uint32_t busy_since_ms_{0};
|
||||
uint32_t session_start_ms_{0};
|
||||
uint32_t update_start_ms_{0};
|
||||
// The first and the latest frame drawn since the display last refreshed.
|
||||
uint32_t first_change_ms_{0};
|
||||
uint32_t last_change_ms_{0};
|
||||
uint32_t rx_total_{0};
|
||||
uint32_t update_start_bytes_{0};
|
||||
uint32_t last_request_ms_{0};
|
||||
uint32_t last_rx_ms_{0};
|
||||
uint32_t pass_start_ms_{0};
|
||||
uint32_t skip_left_{0};
|
||||
|
||||
uint16_t rx_pos_{0};
|
||||
uint16_t rx_len_{0};
|
||||
uint16_t fb_w_{0};
|
||||
uint16_t fb_h_{0};
|
||||
uint16_t vis_w_{0};
|
||||
uint16_t vis_h_{0};
|
||||
uint16_t rects_left_{0};
|
||||
uint16_t rect_x_{0};
|
||||
uint16_t rect_y_{0};
|
||||
uint16_t rect_w_{0};
|
||||
uint16_t rect_h_{0};
|
||||
// Raw: column and row inside the rect.
|
||||
uint16_t pos_x_{0};
|
||||
uint16_t pos_y_{0};
|
||||
|
||||
uint8_t bpp_{2};
|
||||
uint8_t minor_{8};
|
||||
uint8_t sec_count_{0};
|
||||
uint8_t text_len_{0}; // clamped to MAX_TEXT_LEN
|
||||
display::ColorBitness bitness_{display::COLOR_BITNESS_565};
|
||||
VncState state_{VncState::VNC_STATE_IDLE};
|
||||
VncStep step_{VncStep::VNC_STEP_VERSION};
|
||||
VncStep after_skip_{VncStep::VNC_STEP_MESSAGE};
|
||||
VncFailure reason_failure_{VncFailure::VNC_FAILURE_TRANSIENT};
|
||||
|
||||
bool rx_pending_{false};
|
||||
bool in_update_{false};
|
||||
bool request_outstanding_{false};
|
||||
bool refresh_pending_{false};
|
||||
bool frame_drawn_{false};
|
||||
bool display_parks_{false};
|
||||
bool display_busy_{false};
|
||||
bool busy_warned_{false};
|
||||
bool network_was_up_{false};
|
||||
bool big_endian_; // the pixel order the display takes
|
||||
|
||||
uint8_t rx_[RX_BUFFER_SIZE];
|
||||
};
|
||||
|
||||
} // namespace esphome::vnc
|
||||
@@ -357,7 +357,7 @@ void VoiceAssistant::loop() {
|
||||
msg.start = true;
|
||||
msg.conversation_id = StringRef(this->conversation_id_);
|
||||
msg.flags = flags;
|
||||
msg.audio_settings = audio_settings;
|
||||
msg.audio_settings = std::move(audio_settings);
|
||||
msg.wake_word_phrase = StringRef(this->wake_word_);
|
||||
|
||||
// Reset media player state tracking
|
||||
|
||||
@@ -1043,7 +1043,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
|
||||
if (is_on) {
|
||||
parse_cstr_param_(
|
||||
request, ESPHOME_F("effect"), call,
|
||||
static_cast<light::LightCall &(light::LightCall::*) (const char *, size_t)>(&decltype(call)::set_effect));
|
||||
static_cast<light::LightCall &(light::LightCall::*)(const char *, size_t)>(&decltype(call)::set_effect));
|
||||
}
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
@@ -1404,7 +1404,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
|
||||
auto call = obj->make_call();
|
||||
parse_cstr_param_(
|
||||
request, ESPHOME_F("value"), call,
|
||||
static_cast<text::TextCall &(text::TextCall::*) (const char *, size_t)>(&decltype(call)::set_value));
|
||||
static_cast<text::TextCall &(text::TextCall::*)(const char *, size_t)>(&decltype(call)::set_value));
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1464,7 +1464,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
|
||||
auto call = obj->make_call();
|
||||
parse_cstr_param_(
|
||||
request, ESPHOME_F("option"), call,
|
||||
static_cast<select::SelectCall &(select::SelectCall::*) (const char *, size_t)>(&decltype(call)::set_option));
|
||||
static_cast<select::SelectCall &(select::SelectCall::*)(const char *, size_t)>(&decltype(call)::set_option));
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1522,26 +1522,26 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
|
||||
// Parse string mode parameters
|
||||
parse_cstr_param_(
|
||||
request, ESPHOME_F("mode"), call,
|
||||
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(&decltype(call)::set_mode));
|
||||
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(&decltype(call)::set_mode));
|
||||
parse_cstr_param_(request, ESPHOME_F("fan_mode"), call,
|
||||
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
|
||||
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(
|
||||
&decltype(call)::set_fan_mode));
|
||||
parse_cstr_param_(request, ESPHOME_F("swing_mode"), call,
|
||||
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
|
||||
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(
|
||||
&decltype(call)::set_swing_mode));
|
||||
parse_cstr_param_(request, ESPHOME_F("preset"), call,
|
||||
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
|
||||
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(
|
||||
&decltype(call)::set_preset));
|
||||
|
||||
// Parse temperature parameters
|
||||
// static_cast needed to disambiguate overloaded setters (float vs optional<float>)
|
||||
using ClimateCall = decltype(call);
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature_high"), call,
|
||||
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_high));
|
||||
static_cast<ClimateCall &(ClimateCall::*)(float)>(&ClimateCall::set_target_temperature_high));
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature_low"), call,
|
||||
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_low));
|
||||
static_cast<ClimateCall &(ClimateCall::*)(float)>(&ClimateCall::set_target_temperature_low));
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature"), call,
|
||||
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature));
|
||||
static_cast<ClimateCall &(ClimateCall::*)(float)>(&ClimateCall::set_target_temperature));
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1818,8 +1818,8 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
|
||||
auto call = obj->make_call();
|
||||
parse_cstr_param_(
|
||||
request, ESPHOME_F("code"), call,
|
||||
static_cast<alarm_control_panel::AlarmControlPanelCall &(
|
||||
alarm_control_panel::AlarmControlPanelCall::*) (const char *, size_t)>(&decltype(call)::set_code));
|
||||
static_cast<alarm_control_panel::AlarmControlPanelCall &(alarm_control_panel::AlarmControlPanelCall::*)(
|
||||
const char *, size_t)>(&decltype(call)::set_code));
|
||||
|
||||
static constexpr ActionEntry<alarm_control_panel::AlarmControlPanelCall> ACTIONS[] PROGMEM = {
|
||||
{"disarm", &alarm_control_panel::AlarmControlPanelCall::disarm},
|
||||
@@ -1893,7 +1893,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
|
||||
// Parse mode parameter
|
||||
parse_cstr_param_(
|
||||
request, ESPHOME_F("mode"), base_call,
|
||||
static_cast<water_heater::WaterHeaterCall &(water_heater::WaterHeaterCall::*) (const char *, size_t)>(
|
||||
static_cast<water_heater::WaterHeaterCall &(water_heater::WaterHeaterCall::*)(const char *, size_t)>(
|
||||
&water_heater::WaterHeaterCall::set_mode));
|
||||
|
||||
// Parse temperature parameters
|
||||
|
||||
@@ -450,9 +450,12 @@ uart::UARTFlushResult WeikaiChannel::flush() {
|
||||
|
||||
size_t WeikaiChannel::xfer_fifo_to_buffer_() {
|
||||
size_t total = 0;
|
||||
size_t to_transfer;
|
||||
size_t free;
|
||||
while ((to_transfer = this->rx_in_fifo_()) && (free = this->receive_buffer_.free())) {
|
||||
while (true) {
|
||||
size_t to_transfer = this->rx_in_fifo_();
|
||||
size_t free = this->receive_buffer_.free();
|
||||
if (to_transfer == 0 || free == 0) {
|
||||
break;
|
||||
}
|
||||
// while bytes in fifo and some room in the buffer we transfer
|
||||
if (to_transfer > XFER_MAX_SIZE)
|
||||
to_transfer = XFER_MAX_SIZE; // we can only do so much
|
||||
|
||||
@@ -309,6 +309,8 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
case WHIRLPOOL_AUTO:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ from esphome.const import (
|
||||
CONF_ENABLE_BTM,
|
||||
CONF_ENABLE_ON_BOOT,
|
||||
CONF_ENABLE_RRM,
|
||||
CONF_ETHERNET,
|
||||
CONF_FAST_CONNECT,
|
||||
CONF_GATEWAY,
|
||||
CONF_HIDDEN,
|
||||
@@ -431,8 +432,26 @@ def _apply_min_auth_mode_default(config):
|
||||
return config
|
||||
|
||||
|
||||
def wifi_has_sta(config: ConfigType) -> bool:
|
||||
"""Whether the validated config has at least one station network."""
|
||||
return bool(config.get(CONF_NETWORKS))
|
||||
|
||||
|
||||
# Components that save station credentials at run time
|
||||
_PROVISIONERS = ("captive_portal", "improv_ble", "improv_serial")
|
||||
|
||||
|
||||
def wifi_stays_ap_only(config: ConfigType, full_config: ConfigType) -> bool:
|
||||
"""Whether no station can appear at run time: no networks, no DPP, no provisioner."""
|
||||
return not (
|
||||
wifi_has_sta(config)
|
||||
or CONF_DPP in config
|
||||
or any(c in full_config for c in _PROVISIONERS)
|
||||
)
|
||||
|
||||
|
||||
def final_validate(config):
|
||||
has_sta = bool(config.get(CONF_NETWORKS, True))
|
||||
has_sta = wifi_has_sta(config)
|
||||
has_ap = CONF_AP in config
|
||||
has_dpp = CONF_DPP in config
|
||||
full_config = fv.full_config.get()
|
||||
@@ -447,9 +466,11 @@ def final_validate(config):
|
||||
|
||||
ap_is_wisp = has_ap and config[CONF_AP][CONF_AP_MODE] == WIFI_AP_MODE_WISP
|
||||
if ap_is_wisp:
|
||||
if not has_sta:
|
||||
# The clients are routed through the station, or through ethernet
|
||||
if not has_sta and CONF_ETHERNET not in full_config:
|
||||
raise cv.Invalid(
|
||||
"WISP mode requires at least one STA network (wifi.ssid or wifi.networks)"
|
||||
"WISP mode needs a station network (wifi.ssid or wifi.networks), "
|
||||
"or ethernet as the upstream interface"
|
||||
)
|
||||
if has_captive_portal:
|
||||
raise cv.Invalid(
|
||||
@@ -458,14 +479,27 @@ def final_validate(config):
|
||||
# Every client packet passes through the WiFi driver, none through the device's own sockets
|
||||
require_high_performance_wifi()
|
||||
if CONF_DNS_SERVERS not in config[CONF_AP]:
|
||||
# The access point passes the station's resolvers on, which a static address has to name
|
||||
for network in [config, *config.get(CONF_NETWORKS, [])]:
|
||||
# The access point passes the upstream's resolvers on, which a static address has to name
|
||||
upstreams = [
|
||||
([], config),
|
||||
*(
|
||||
([CONF_NETWORKS, i], network)
|
||||
for i, network in enumerate(config.get(CONF_NETWORKS, []))
|
||||
),
|
||||
(None, full_config.get(CONF_ETHERNET, {})),
|
||||
]
|
||||
for path, network in upstreams:
|
||||
static = network.get(CONF_MANUAL_IP)
|
||||
if static and static[CONF_DNS1].is_unspecified:
|
||||
if not static or not static[CONF_DNS1].is_unspecified:
|
||||
continue
|
||||
if path is None:
|
||||
raise cv.Invalid(
|
||||
"manual_ip needs dns1 in WISP mode unless ap.dns_servers is set",
|
||||
path=[CONF_MANUAL_IP, CONF_DNS1],
|
||||
"ethernet manual_ip needs dns1 in WISP mode unless ap.dns_servers is set"
|
||||
)
|
||||
raise cv.Invalid(
|
||||
"manual_ip needs dns1 in WISP mode unless ap.dns_servers is set",
|
||||
path=[*path, CONF_MANUAL_IP, CONF_DNS1],
|
||||
)
|
||||
|
||||
if has_ap and not has_captive_portal and not has_web_server and not ap_is_wisp:
|
||||
_LOGGER.warning(
|
||||
@@ -565,7 +599,7 @@ def _report_provisioning_credentials(config):
|
||||
"""
|
||||
from esphome.components import provisioning
|
||||
|
||||
if config.get(CONF_NETWORKS):
|
||||
if wifi_has_sta(config):
|
||||
provisioning.report_hardcoded_credentials("wifi")
|
||||
elif CONF_AP in config:
|
||||
# An access point with no station credentials: the AP shuts down when the
|
||||
|
||||
@@ -783,12 +783,9 @@ void WiFiComponent::start() {
|
||||
// Without fast_connect: go straight to scanning (or hidden mode if all networks are hidden)
|
||||
this->start_initial_connection_();
|
||||
#endif
|
||||
#ifdef USE_WIFI_AP
|
||||
} else if (this->has_ap()) {
|
||||
#if defined(USE_WIFI_AP) && !defined(USE_WIFI_AP_WISP)
|
||||
} else if (this->has_ap()) { // a bridge waits in loop() for the upstream's resolver
|
||||
this->setup_ap_config_();
|
||||
if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
|
||||
ESP_LOGV(TAG, "Setting Output Power Option failed");
|
||||
}
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
// Where the radio scans alongside the AP, the portal can list networks.
|
||||
if (captive_portal::global_captive_portal != nullptr && !WIFI_AP_EXCLUSIVE) {
|
||||
@@ -797,7 +794,7 @@ void WiFiComponent::start() {
|
||||
}
|
||||
#endif
|
||||
this->start_ap_portal_();
|
||||
#endif // USE_WIFI_AP
|
||||
#endif // USE_WIFI_AP && !USE_WIFI_AP_WISP
|
||||
}
|
||||
#ifdef USE_WIFI_DPP
|
||||
// Without networks there is nothing to try first, so listen for a configurator right away
|
||||
@@ -937,15 +934,7 @@ void WiFiComponent::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP_WISP
|
||||
// The bridge comes up once it has a resolver to offer; a start that failed is tried again after ap_timeout
|
||||
if (!this->ap_setup_ && this->ap_dns_known_() &&
|
||||
(this->ap_changed_ == 0 || now - this->ap_changed_ > this->ap_timeout_)) {
|
||||
ESP_LOGI(TAG, "Starting AP");
|
||||
this->ap_changed_ = now;
|
||||
this->setup_ap_config_();
|
||||
}
|
||||
#elif defined(USE_WIFI_AP)
|
||||
#if defined(USE_WIFI_AP) && !defined(USE_WIFI_AP_WISP)
|
||||
// Once the provisioning window has closed, don't bring up the fallback AP (or
|
||||
// the captive portal on it) - the device must stay unprovisionable until it is
|
||||
// power-cycled.
|
||||
@@ -975,7 +964,7 @@ void WiFiComponent::loop() {
|
||||
this->start_ap_portal_();
|
||||
}
|
||||
}
|
||||
#endif // USE_WIFI_AP
|
||||
#endif // USE_WIFI_AP && !USE_WIFI_AP_WISP
|
||||
|
||||
#ifdef USE_IMPROV_BLE
|
||||
if (improv_ble::global_improv_component != nullptr && !improv_ble::global_improv_component->is_active() &&
|
||||
@@ -1013,6 +1002,16 @@ void WiFiComponent::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP_WISP
|
||||
// The bridge comes up once it has a resolver to offer; a start that failed is tried again after ap_timeout
|
||||
if (!this->ap_setup_ && this->state_ != WIFI_COMPONENT_STATE_DISABLED && this->ap_dns_known_() &&
|
||||
(this->ap_changed_ == 0 || now - this->ap_changed_ > this->ap_timeout_)) {
|
||||
ESP_LOGI(TAG, "Starting AP");
|
||||
this->ap_changed_ = now;
|
||||
this->setup_ap_config_();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_WIFI_DPP
|
||||
// Without networks no cooldown starts the listen, so a failed or abandoned enrollment is listened for again
|
||||
// here, at most once per timeout
|
||||
@@ -1098,6 +1097,11 @@ void WiFiComponent::setup_ap_config_() {
|
||||
// Reset before the AP exists: afterwards only the platform's event path writes the counter
|
||||
this->ap_clients_ = 0;
|
||||
this->ap_setup_ = this->wifi_start_ap_(this->ap_);
|
||||
// The station path applied it in start(); without a station the radio only exists from here
|
||||
if (this->ap_setup_ && !this->has_sta() && !std::isnan(this->output_power_) &&
|
||||
!this->wifi_apply_output_power_(this->output_power_)) {
|
||||
ESP_LOGV(TAG, "Setting Output Power Option failed");
|
||||
}
|
||||
#if defined(USE_WIFI_AP_EXCLUSIVE) || defined(USE_WIFI_AP_WISP)
|
||||
// A failed start must not leave the radio in AP mode: on an exclusive radio it would refuse
|
||||
// the STA attempts until the next try, and a bridge would take clients it cannot serve.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#endif
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
@@ -813,13 +814,8 @@ class WiFiComponent final : public Component {
|
||||
}
|
||||
|
||||
bool all_networks_hidden_() const {
|
||||
if (this->sta_.empty())
|
||||
return false;
|
||||
for (const auto &ap : this->sta_) {
|
||||
if (!ap.get_hidden())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return !this->sta_.empty() &&
|
||||
std::all_of(this->sta_.begin(), this->sta_.end(), [](const WiFiAP &ap) { return ap.get_hidden(); });
|
||||
}
|
||||
|
||||
bool wifi_loop_();
|
||||
|
||||
@@ -62,7 +62,6 @@ static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid
|
||||
#ifdef USE_WIFI_AP
|
||||
static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
#endif // USE_WIFI_AP
|
||||
static bool s_sta_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -128,6 +127,10 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi
|
||||
#if USE_NETWORK_IPV6
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_GOT_IP6) {
|
||||
memcpy(&event.data.ip_got_ip6, event_data, sizeof(ip_event_got_ip6_t));
|
||||
#endif
|
||||
#if defined(USE_WIFI_AP_WISP) && !defined(USE_WIFI_AP_WISP_STATIC_DNS)
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_ETH_GOT_IP) { // NOLINT(bugprone-branch-clone)
|
||||
// no data: the bridge rereads the resolvers an ethernet upstream just received
|
||||
#endif
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { // NOLINT(bugprone-branch-clone)
|
||||
// no data
|
||||
@@ -1037,7 +1040,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
s_sta_started = true;
|
||||
// re-apply power save mode
|
||||
wifi_apply_power_save_();
|
||||
#ifdef SOC_WIFI_SUPPORT_5G
|
||||
@@ -1046,7 +1048,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
|
||||
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) {
|
||||
ESP_LOGV(TAG, "STA stop");
|
||||
s_sta_started = false;
|
||||
s_sta_connecting = false;
|
||||
|
||||
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) {
|
||||
@@ -1126,6 +1127,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
this->notify_disconnect_state_listeners_();
|
||||
#endif
|
||||
|
||||
#if defined(USE_WIFI_AP_WISP) && !defined(USE_WIFI_AP_WISP_STATIC_DNS)
|
||||
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_ETH_GOT_IP) {
|
||||
this->apply_ap_dns_();
|
||||
#endif
|
||||
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) {
|
||||
const auto &it = data->data.ip_got_ip;
|
||||
ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw));
|
||||
@@ -1400,10 +1405,6 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
|
||||
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
|
||||
esp_err_t err;
|
||||
|
||||
// enable AP
|
||||
if (!this->wifi_mode_({}, true))
|
||||
return false;
|
||||
|
||||
// Check if the AP interface is initialized before using it
|
||||
if (s_ap_netif == nullptr) {
|
||||
ESP_LOGW(TAG, "AP interface not initialized");
|
||||
@@ -1481,8 +1482,8 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
|
||||
#endif
|
||||
|
||||
err = esp_netif_dhcps_start(s_ap_netif);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
// Before the access point is up, its start event brings the server up with these settings
|
||||
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_IF_NOT_READY) {
|
||||
ESP_LOGE(TAG, "esp_netif_dhcps_start failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
@@ -1491,6 +1492,17 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
// The address and DHCP settings go in before the access point starts: its start event starts the DHCP
|
||||
// server on the event task, and settings written after that race it.
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
||||
#else
|
||||
if (!this->wifi_ap_ip_config_({})) {
|
||||
#endif
|
||||
ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
|
||||
return false;
|
||||
}
|
||||
|
||||
// enable AP
|
||||
if (!this->wifi_mode_({}, true))
|
||||
return false;
|
||||
@@ -1527,19 +1539,6 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
ESP_LOGE(TAG, "esp_wifi_set_config failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
||||
ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (!this->wifi_ap_ip_config_({})) {
|
||||
ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1684,7 +1683,7 @@ bool WiFiComponent::write_ap_dns_() {
|
||||
#ifndef USE_WIFI_AP_WISP_STATIC_DNS
|
||||
void WiFiComponent::apply_ap_dns_() {
|
||||
if (!this->ap_setup_)
|
||||
return; // loop() brings the bridge up once the station has a resolver, and its setup writes them
|
||||
return; // loop() brings the bridge up once the upstream has a resolver, and its setup writes them
|
||||
// The DHCP server holds what it offers, so there is nothing to cache; a failed update is noticed again with the
|
||||
// next lease. A resolver the station lost stays offered: set_dns_info rejects an empty address.
|
||||
bool changed = false;
|
||||
|
||||
@@ -166,8 +166,9 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
|
||||
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
|
||||
case CYW43_LINK_NONET:
|
||||
return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND;
|
||||
default:
|
||||
return WiFiSTAConnectStatus::IDLE;
|
||||
}
|
||||
return WiFiSTAConnectStatus::IDLE;
|
||||
}
|
||||
|
||||
int WiFiComponent::s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) {
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace esphome::xiaomi_hhccpot002 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "xiaomi_hhccpot002");
|
||||
|
||||
void XiaomiHHCCPOT002 ::dump_config() {
|
||||
void XiaomiHHCCPOT002::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Xiaomi HHCCPOT002");
|
||||
LOG_SENSOR(" ", "Moisture", this->moisture_);
|
||||
LOG_SENSOR(" ", "Conductivity", this->conductivity_);
|
||||
|
||||
@@ -115,7 +115,7 @@ int ZephyrPreferences::load_setting(const char *name, size_t len, settings_read_
|
||||
auto type = parse_hex<uint32_t>(name);
|
||||
if (!type.has_value()) {
|
||||
std::string full_name(ESPHOME_SETTINGS_KEY);
|
||||
full_name += "/";
|
||||
full_name += '/';
|
||||
full_name += name;
|
||||
// Delete unusable keys. Otherwise it will stay in flash forever.
|
||||
settings_delete(full_name.c_str());
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user