1
0
mirror of https://github.com/esphome/esphome.git synced 2026-03-22 19:26:44 +01:00

Merge pull request #2328 from esphome/bump-2021.9.0

2021.9.0
This commit is contained in:
Jesse Hills 2021-09-16 09:40:29 +12:00 committed by GitHub
commit 321504cf29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
212 changed files with 6593 additions and 7437 deletions

View File

@ -14,6 +14,8 @@ esphome/core/* @esphome/core
esphome/components/ac_dimmer/* @glmnet
esphome/components/adc/* @esphome/core
esphome/components/addressable_light/* @justfalter
esphome/components/airthings_ble/* @jeromelaban
esphome/components/airthings_wave_plus/* @jeromelaban
esphome/components/am43/* @buxtronix
esphome/components/am43/cover/* @buxtronix
esphome/components/animation/* @syndlex
@ -29,6 +31,7 @@ esphome/components/ble_client/* @buxtronix
esphome/components/bme680_bsec/* @trvrnrth
esphome/components/canbus/* @danielschramm @mvturnho
esphome/components/captive_portal/* @OttoWinter
esphome/components/ccs811/* @habbie
esphome/components/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet
esphome/components/color_temperature/* @jesserockz
@ -52,6 +55,8 @@ esphome/components/globals/* @esphome/core
esphome/components/gpio/* @esphome/core
esphome/components/gps/* @coogle
esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann
esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/homeassistant/* @OttoWinter
esphome/components/hrxl_maxsonar_wr/* @netmikey
@ -75,8 +80,7 @@ esphome/components/mcp23x17_base/* @jesserockz
esphome/components/mcp23xxx_base/* @jesserockz
esphome/components/mcp2515/* @danielschramm @mvturnho
esphome/components/mcp9808/* @k7hpn
esphome/components/midea_ac/* @dudanov
esphome/components/midea_dongle/* @dudanov
esphome/components/midea/* @dudanov
esphome/components/mitsubishi/* @RubyBailey
esphome/components/network/* @esphome/core
esphome/components/nextion/* @senexcrenshaw
@ -90,6 +94,7 @@ esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/pid/* @OttoWinter
esphome/components/pipsolar/* @andreashergert1984
esphome/components/pm1006/* @habbie
esphome/components/pmsa003i/* @sjtrny
esphome/components/pn532/* @OttoWinter @jesserockz
esphome/components/pn532_i2c/* @OttoWinter @jesserockz
@ -115,6 +120,7 @@ esphome/components/sht4x/* @sjtrny
esphome/components/shutdown/* @esphome/core
esphome/components/sim800l/* @glmnet
esphome/components/sm2135/* @BoukeHaarsma23
esphome/components/socket/* @esphome/core
esphome/components/spi/* @esphome/core
esphome/components/ssd1322_base/* @kbx81
esphome/components/ssd1322_spi/* @kbx81
@ -129,6 +135,7 @@ esphome/components/ssd1351_base/* @kbx81
esphome/components/ssd1351_spi/* @kbx81
esphome/components/st7735/* @SenexCrenshaw
esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155
esphome/components/substitutions/* @esphome/core
esphome/components/sun/* @OttoWinter
esphome/components/switch/* @esphome/core

View File

@ -24,7 +24,7 @@ TYPE_LINT = 'lint'
TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT]
BASE_VERSION = "3.6.0"
BASE_VERSION = "4.2.0"
parser = argparse.ArgumentParser()

View File

@ -256,7 +256,7 @@ def show_logs(config, args, port):
run_miniterm(config, port)
return 0
if get_port_type(port) == "NETWORK" and "api" in config:
from esphome.api.client import run_logs
from esphome.components.api.client import run_logs
return run_logs(config, port)
if get_port_type(port) == "MQTT" and "mqtt" in config:
@ -483,75 +483,9 @@ def parse_args(argv):
metavar=("key", "value"),
)
# Keep backward compatibility with the old command line format of
# esphome <config> <command>.
#
# Unfortunately this can't be done by adding another configuration argument to the
# main config parser, as argparse is greedy when parsing arguments, so in regular
# usage it'll eat the command as the configuration argument and error out out
# because it can't parse the configuration as a command.
#
# Instead, construct an ad-hoc parser for the old format that doesn't actually
# process the arguments, but parses them enough to let us figure out if the old
# format is used. In that case, swap the command and configuration in the arguments
# and continue on with the normal parser (after raising a deprecation warning).
#
# Disable argparse's built-in help option and add it manually to prevent this
# parser from printing the help messagefor the old format when invoked with -h.
compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False)
compat_parser.add_argument("-h", "--help")
compat_parser.add_argument("configuration", nargs="*")
compat_parser.add_argument(
"command",
choices=[
"config",
"compile",
"upload",
"logs",
"run",
"clean-mqtt",
"wizard",
"mqtt-fingerprint",
"version",
"clean",
"dashboard",
"vscode",
"update-all",
],
)
# on Python 3.9+ we can simply set exit_on_error=False in the constructor
def _raise(x):
raise argparse.ArgumentError(None, x)
compat_parser.error = _raise
deprecated_argv_suggestion = None
if ["dashboard", "config"] == argv[1:3] or ["version"] == argv[1:2]:
# this is most likely meant in new-style arg format. do not try compat parsing
pass
else:
try:
result, unparsed = compat_parser.parse_known_args(argv[1:])
last_option = len(argv) - len(unparsed) - 1 - len(result.configuration)
unparsed = [
"--device" if arg in ("--upload-port", "--serial-port") else arg
for arg in unparsed
]
argv = (
argv[0:last_option] + [result.command] + result.configuration + unparsed
)
deprecated_argv_suggestion = argv
except argparse.ArgumentError:
# This is not an old-style command line, so we don't have to do anything.
pass
# And continue on with regular parsing
parser = argparse.ArgumentParser(
description=f"ESPHome v{const.__version__}", parents=[options_parser]
)
parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion)
mqtt_options = argparse.ArgumentParser(add_help=False)
mqtt_options.add_argument("--topic", help="Manually set the MQTT topic.")
@ -701,7 +635,83 @@ def parse_args(argv):
"configuration", help="Your YAML configuration file directories.", nargs="+"
)
return parser.parse_args(argv[1:])
# Keep backward compatibility with the old command line format of
# esphome <config> <command>.
#
# Unfortunately this can't be done by adding another configuration argument to the
# main config parser, as argparse is greedy when parsing arguments, so in regular
# usage it'll eat the command as the configuration argument and error out out
# because it can't parse the configuration as a command.
#
# Instead, if parsing using the current format fails, construct an ad-hoc parser
# that doesn't actually process the arguments, but parses them enough to let us
# figure out if the old format is used. In that case, swap the command and
# configuration in the arguments and retry with the normal parser (and raise
# a deprecation warning).
arguments = argv[1:]
# On Python 3.9+ we can simply set exit_on_error=False in the constructor
def _raise(x):
raise argparse.ArgumentError(None, x)
# First, try new-style parsing, but don't exit in case of failure
try:
# duplicate parser so that we can use the original one to raise errors later on
current_parser = argparse.ArgumentParser(add_help=False, parents=[parser])
current_parser.set_defaults(deprecated_argv_suggestion=None)
current_parser.error = _raise
return current_parser.parse_args(arguments)
except argparse.ArgumentError:
pass
# Second, try compat parsing and rearrange the command-line if it succeeds
# Disable argparse's built-in help option and add it manually to prevent this
# parser from printing the help messagefor the old format when invoked with -h.
compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False)
compat_parser.add_argument("-h", "--help", action="store_true")
compat_parser.add_argument("configuration", nargs="*")
compat_parser.add_argument(
"command",
choices=[
"config",
"compile",
"upload",
"logs",
"run",
"clean-mqtt",
"wizard",
"mqtt-fingerprint",
"version",
"clean",
"dashboard",
"vscode",
"update-all",
],
)
try:
compat_parser.error = _raise
result, unparsed = compat_parser.parse_known_args(argv[1:])
last_option = len(arguments) - len(unparsed) - 1 - len(result.configuration)
unparsed = [
"--device" if arg in ("--upload-port", "--serial-port") else arg
for arg in unparsed
]
arguments = (
arguments[0:last_option]
+ [result.command]
+ result.configuration
+ unparsed
)
deprecated_argv_suggestion = arguments
except argparse.ArgumentError:
# old-style parsing failed, don't suggest any argument
deprecated_argv_suggestion = None
# Finally, run the new-style parser again with the possibly swapped arguments,
# and let it error out if the command is unparsable.
parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion)
return parser.parse_args(arguments)
def run_esphome(argv):
@ -715,7 +725,7 @@ def run_esphome(argv):
"and will be removed in the future. "
)
_LOGGER.warning("Please instead use:")
_LOGGER.warning(" esphome %s", " ".join(args.deprecated_argv_suggestion[1:]))
_LOGGER.warning(" esphome %s", " ".join(args.deprecated_argv_suggestion))
if sys.version_info < (3, 7, 0):
_LOGGER.error(

File diff suppressed because one or more lines are too long

View File

@ -1,518 +0,0 @@
from datetime import datetime
import functools
import logging
import socket
import threading
import time
# pylint: disable=unused-import
from typing import Optional # noqa
from google.protobuf import message # noqa
from esphome import const
import esphome.api.api_pb2 as pb
from esphome.const import CONF_PASSWORD, CONF_PORT
from esphome.core import EsphomeError
from esphome.helpers import resolve_ip_address, indent
from esphome.log import color, Fore
from esphome.util import safe_print
_LOGGER = logging.getLogger(__name__)
class APIConnectionError(EsphomeError):
pass
MESSAGE_TYPE_TO_PROTO = {
1: pb.HelloRequest,
2: pb.HelloResponse,
3: pb.ConnectRequest,
4: pb.ConnectResponse,
5: pb.DisconnectRequest,
6: pb.DisconnectResponse,
7: pb.PingRequest,
8: pb.PingResponse,
9: pb.DeviceInfoRequest,
10: pb.DeviceInfoResponse,
11: pb.ListEntitiesRequest,
12: pb.ListEntitiesBinarySensorResponse,
13: pb.ListEntitiesCoverResponse,
14: pb.ListEntitiesFanResponse,
15: pb.ListEntitiesLightResponse,
16: pb.ListEntitiesSensorResponse,
17: pb.ListEntitiesSwitchResponse,
18: pb.ListEntitiesTextSensorResponse,
19: pb.ListEntitiesDoneResponse,
20: pb.SubscribeStatesRequest,
21: pb.BinarySensorStateResponse,
22: pb.CoverStateResponse,
23: pb.FanStateResponse,
24: pb.LightStateResponse,
25: pb.SensorStateResponse,
26: pb.SwitchStateResponse,
27: pb.TextSensorStateResponse,
28: pb.SubscribeLogsRequest,
29: pb.SubscribeLogsResponse,
30: pb.CoverCommandRequest,
31: pb.FanCommandRequest,
32: pb.LightCommandRequest,
33: pb.SwitchCommandRequest,
34: pb.SubscribeServiceCallsRequest,
35: pb.ServiceCallResponse,
36: pb.GetTimeRequest,
37: pb.GetTimeResponse,
}
def _varuint_to_bytes(value):
if value <= 0x7F:
return bytes([value])
ret = bytes()
while value:
temp = value & 0x7F
value >>= 7
if value:
ret += bytes([temp | 0x80])
else:
ret += bytes([temp])
return ret
def _bytes_to_varuint(value):
result = 0
bitpos = 0
for val in value:
result |= (val & 0x7F) << bitpos
bitpos += 7
if (val & 0x80) == 0:
return result
return None
# pylint: disable=too-many-instance-attributes,not-callable
class APIClient(threading.Thread):
def __init__(self, address, port, password):
threading.Thread.__init__(self)
self._address = address # type: str
self._port = port # type: int
self._password = password # type: Optional[str]
self._socket = None # type: Optional[socket.socket]
self._socket_open_event = threading.Event()
self._socket_write_lock = threading.Lock()
self._connected = False
self._authenticated = False
self._message_handlers = []
self._keepalive = 5
self._ping_timer = None
self.on_disconnect = None
self.on_connect = None
self.on_login = None
self.auto_reconnect = False
self._running_event = threading.Event()
self._stop_event = threading.Event()
@property
def stopped(self):
return self._stop_event.is_set()
def _refresh_ping(self):
if self._ping_timer is not None:
self._ping_timer.cancel()
self._ping_timer = None
def func():
self._ping_timer = None
if self._connected:
try:
self.ping()
except APIConnectionError as err:
self._fatal_error(err)
else:
self._refresh_ping()
self._ping_timer = threading.Timer(self._keepalive, func)
self._ping_timer.start()
def _cancel_ping(self):
if self._ping_timer is not None:
self._ping_timer.cancel()
self._ping_timer = None
def _close_socket(self):
self._cancel_ping()
if self._socket is not None:
self._socket.close()
self._socket = None
self._socket_open_event.clear()
self._connected = False
self._authenticated = False
self._message_handlers = []
def stop(self, force=False):
if self.stopped:
raise ValueError
if self._connected and not force:
try:
self.disconnect()
except APIConnectionError:
pass
self._close_socket()
self._stop_event.set()
if not force:
self.join()
def connect(self):
if not self._running_event.wait(0.1):
raise APIConnectionError("You need to call start() first!")
if self._connected:
self.disconnect(on_disconnect=False)
try:
ip = resolve_ip_address(self._address)
except EsphomeError as err:
_LOGGER.warning(
"Error resolving IP address of %s. Is it connected to WiFi?",
self._address,
)
_LOGGER.warning(
"(If this error persists, please set a static IP address: "
"https://esphome.io/components/wifi.html#manual-ips)"
)
raise APIConnectionError(err) from err
_LOGGER.info("Connecting to %s:%s (%s)", self._address, self._port, ip)
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._socket.settimeout(10.0)
self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
try:
self._socket.connect((ip, self._port))
except OSError as err:
err = APIConnectionError(f"Error connecting to {ip}: {err}")
self._fatal_error(err)
raise err
self._socket.settimeout(0.1)
self._socket_open_event.set()
hello = pb.HelloRequest()
hello.client_info = f"ESPHome v{const.__version__}"
try:
resp = self._send_message_await_response(hello, pb.HelloResponse)
except APIConnectionError as err:
self._fatal_error(err)
raise err
_LOGGER.debug(
"Successfully connected to %s ('%s' API=%s.%s)",
self._address,
resp.server_info,
resp.api_version_major,
resp.api_version_minor,
)
self._connected = True
self._refresh_ping()
if self.on_connect is not None:
self.on_connect()
def _check_connected(self):
if not self._connected:
err = APIConnectionError("Must be connected!")
self._fatal_error(err)
raise err
def login(self):
self._check_connected()
if self._authenticated:
raise APIConnectionError("Already logged in!")
connect = pb.ConnectRequest()
if self._password is not None:
connect.password = self._password
resp = self._send_message_await_response(connect, pb.ConnectResponse)
if resp.invalid_password:
raise APIConnectionError("Invalid password!")
self._authenticated = True
if self.on_login is not None:
self.on_login()
def _fatal_error(self, err):
was_connected = self._connected
self._close_socket()
if was_connected and self.on_disconnect is not None:
self.on_disconnect(err)
def _write(self, data): # type: (bytes) -> None
if self._socket is None:
raise APIConnectionError("Socket closed")
# _LOGGER.debug("Write: %s", format_bytes(data))
with self._socket_write_lock:
try:
self._socket.sendall(data)
except OSError as err:
err = APIConnectionError(f"Error while writing data: {err}")
self._fatal_error(err)
raise err
def _send_message(self, msg):
# type: (message.Message) -> None
for message_type, klass in MESSAGE_TYPE_TO_PROTO.items():
if isinstance(msg, klass):
break
else:
raise ValueError
encoded = msg.SerializeToString()
_LOGGER.debug("Sending %s:\n%s", type(msg), indent(str(msg)))
req = bytes([0])
req += _varuint_to_bytes(len(encoded))
req += _varuint_to_bytes(message_type)
req += encoded
self._write(req)
def _send_message_await_response_complex(
self, send_msg, do_append, do_stop, timeout=5
):
event = threading.Event()
responses = []
def on_message(resp):
if do_append(resp):
responses.append(resp)
if do_stop(resp):
event.set()
self._message_handlers.append(on_message)
self._send_message(send_msg)
ret = event.wait(timeout)
try:
self._message_handlers.remove(on_message)
except ValueError:
pass
if not ret:
raise APIConnectionError("Timeout while waiting for message response!")
return responses
def _send_message_await_response(self, send_msg, response_type, timeout=5):
def is_response(msg):
return isinstance(msg, response_type)
return self._send_message_await_response_complex(
send_msg, is_response, is_response, timeout
)[0]
def device_info(self):
self._check_connected()
return self._send_message_await_response(
pb.DeviceInfoRequest(), pb.DeviceInfoResponse
)
def ping(self):
self._check_connected()
return self._send_message_await_response(pb.PingRequest(), pb.PingResponse)
def disconnect(self, on_disconnect=True):
self._check_connected()
try:
self._send_message_await_response(
pb.DisconnectRequest(), pb.DisconnectResponse
)
except APIConnectionError:
pass
self._close_socket()
if self.on_disconnect is not None and on_disconnect:
self.on_disconnect(None)
def _check_authenticated(self):
if not self._authenticated:
raise APIConnectionError("Must login first!")
def subscribe_logs(self, on_log, log_level=7, dump_config=False):
self._check_authenticated()
def on_msg(msg):
if isinstance(msg, pb.SubscribeLogsResponse):
on_log(msg)
self._message_handlers.append(on_msg)
req = pb.SubscribeLogsRequest(dump_config=dump_config)
req.level = log_level
self._send_message(req)
def _recv(self, amount):
ret = bytes()
if amount == 0:
return ret
while len(ret) < amount:
if self.stopped:
raise APIConnectionError("Stopped!")
if not self._socket_open_event.is_set():
raise APIConnectionError("No socket!")
try:
val = self._socket.recv(amount - len(ret))
except AttributeError as err:
raise APIConnectionError("Socket was closed") from err
except socket.timeout:
continue
except OSError as err:
raise APIConnectionError(f"Error while receiving data: {err}") from err
ret += val
return ret
def _recv_varint(self):
raw = bytes()
while not raw or raw[-1] & 0x80:
raw += self._recv(1)
return _bytes_to_varuint(raw)
def _run_once(self):
if not self._socket_open_event.wait(0.1):
return
# Preamble
if self._recv(1)[0] != 0x00:
raise APIConnectionError("Invalid preamble")
length = self._recv_varint()
msg_type = self._recv_varint()
raw_msg = self._recv(length)
if msg_type not in MESSAGE_TYPE_TO_PROTO:
_LOGGER.debug("Skipping message type %s", msg_type)
return
msg = MESSAGE_TYPE_TO_PROTO[msg_type]()
msg.ParseFromString(raw_msg)
_LOGGER.debug("Got message: %s:\n%s", type(msg), indent(str(msg)))
for msg_handler in self._message_handlers[:]:
msg_handler(msg)
self._handle_internal_messages(msg)
def run(self):
self._running_event.set()
while not self.stopped:
try:
self._run_once()
except APIConnectionError as err:
if self.stopped:
break
if self._connected:
_LOGGER.error("Error while reading incoming messages: %s", err)
self._fatal_error(err)
self._running_event.clear()
def _handle_internal_messages(self, msg):
if isinstance(msg, pb.DisconnectRequest):
self._send_message(pb.DisconnectResponse())
if self._socket is not None:
self._socket.close()
self._socket = None
self._connected = False
if self.on_disconnect is not None:
self.on_disconnect(None)
elif isinstance(msg, pb.PingRequest):
self._send_message(pb.PingResponse())
elif isinstance(msg, pb.GetTimeRequest):
resp = pb.GetTimeResponse()
resp.epoch_seconds = int(time.time())
self._send_message(resp)
def run_logs(config, address):
conf = config["api"]
port = conf[CONF_PORT]
password = conf[CONF_PASSWORD]
_LOGGER.info("Starting log output from %s using esphome API", address)
cli = APIClient(address, port, password)
stopping = False
retry_timer = []
has_connects = []
def try_connect(err, tries=0):
if stopping:
return
if err:
_LOGGER.warning("Disconnected from API: %s", err)
while retry_timer:
retry_timer.pop(0).cancel()
error = None
try:
cli.connect()
cli.login()
except APIConnectionError as err2: # noqa
error = err2
if error is None:
_LOGGER.info("Successfully connected to %s", address)
return
wait_time = int(min(1.5 ** min(tries, 100), 30))
if not has_connects:
_LOGGER.warning(
"Initial connection failed. The ESP might not be connected "
"to WiFi yet (%s). Re-Trying in %s seconds",
error,
wait_time,
)
else:
_LOGGER.warning(
"Couldn't connect to API (%s). Trying to reconnect in %s seconds",
error,
wait_time,
)
timer = threading.Timer(
wait_time, functools.partial(try_connect, None, tries + 1)
)
timer.start()
retry_timer.append(timer)
def on_log(msg):
time_ = datetime.now().time().strftime("[%H:%M:%S]")
text = msg.message
if msg.send_failed:
text = color(
Fore.WHITE,
"(Message skipped because it was too big to fit in "
"TCP buffer - This is only cosmetic)",
)
safe_print(time_ + text)
def on_login():
try:
cli.subscribe_logs(on_log, dump_config=not has_connects)
has_connects.append(True)
except APIConnectionError:
cli.disconnect()
cli.on_disconnect = try_connect
cli.on_login = on_login
cli.start()
try:
try_connect(None)
while True:
time.sleep(1)
except KeyboardInterrupt:
stopping = True
cli.stop(True)
while retry_timer:
retry_timer.pop(0).cancel()
return 0

View File

@ -55,6 +55,7 @@ ESP8266_BOARD_PINS = {
"espectro": {"LED": 15, "BUTTON": 2},
"espino": {"LED": 2, "LED_RED": 2, "LED_GREEN": 4, "LED_BLUE": 5, "BUTTON": 0},
"espinotee": {"LED": 16},
"espmxdevkit": {},
"espresso_lite_v1": {"LED": 16},
"espresso_lite_v2": {"LED": 2},
"gen4iod": {},
@ -105,6 +106,10 @@ ESP8266_BOARD_PINS = {
},
"phoenix_v1": {"LED": 16},
"phoenix_v2": {"LED": 2},
"sonoff_basic": {},
"sonoff_s20": {},
"sonoff_sv": {},
"sonoff_th": {},
"sparkfunBlynk": "thing",
"thing": {"LED": 5, "SDA": 2, "SCL": 14},
"thingdev": "thing",
@ -166,6 +171,7 @@ ESP8266_FLASH_SIZES = {
"espectro": FLASH_SIZE_4_MB,
"espino": FLASH_SIZE_4_MB,
"espinotee": FLASH_SIZE_4_MB,
"espmxdevkit": FLASH_SIZE_1_MB,
"espresso_lite_v1": FLASH_SIZE_4_MB,
"espresso_lite_v2": FLASH_SIZE_4_MB,
"gen4iod": FLASH_SIZE_512_KB,
@ -178,6 +184,10 @@ ESP8266_FLASH_SIZES = {
"oak": FLASH_SIZE_4_MB,
"phoenix_v1": FLASH_SIZE_4_MB,
"phoenix_v2": FLASH_SIZE_4_MB,
"sonoff_basic": FLASH_SIZE_1_MB,
"sonoff_s20": FLASH_SIZE_1_MB,
"sonoff_sv": FLASH_SIZE_1_MB,
"sonoff_th": FLASH_SIZE_1_MB,
"sparkfunBlynk": FLASH_SIZE_4_MB,
"thing": FLASH_SIZE_512_KB,
"thingdev": FLASH_SIZE_512_KB,
@ -291,6 +301,7 @@ ESP32_BOARD_PINS = {
"SW2": 2,
"SW3": 0,
},
"az-delivery-devkit-v4": {},
"bpi-bit": {
"BUTTON_A": 35,
"BUTTON_B": 27,
@ -320,6 +331,8 @@ ESP32_BOARD_PINS = {
"RGB_LED": 4,
"TEMPERATURE_SENSOR": 34,
},
"briki_abc_esp32": {},
"briki_mbc-wb_esp32": {},
"d-duino-32": {
"D1": 5,
"D10": 1,
@ -380,11 +393,58 @@ ESP32_BOARD_PINS = {
"esp32cam": {},
"esp32dev": {},
"esp32doit-devkit-v1": {"LED": 2},
"esp32doit-espduino": {"TX0": 1, "RX0": 3, "CMD": 11, "CLK": 6, "SD0": 7, "SD1": 8},
"esp32thing": {"BUTTON": 0, "LED": 5, "SS": 2},
"esp32thing_plus": {
"SDA": 23,
"SCL": 22,
"SS": 33,
"MOSI": 18,
"MISO": 19,
"SCK": 5,
"A0": 26,
"A1": 25,
"A2": 34,
"A3": 39,
"A4": 36,
"A5": 4,
"A6": 14,
"A7": 32,
"A8": 15,
"A9": 33,
"A10": 27,
"A11": 12,
"A12": 13,
},
"esp32vn-iot-uno": {},
"espea32": {"BUTTON": 0, "LED": 5},
"espectro32": {"LED": 15, "SD_SS": 33},
"espino32": {"BUTTON": 0, "LED": 16},
"etboard": {
"LED_BUILTIN": 5,
"TX": 34,
"RX": 35,
"SS": 29,
"MOSI": 37,
"MISO": 31,
"SCK": 30,
"A0": 36,
"A1": 39,
"A2": 32,
"A3": 33,
"A4": 34,
"A5": 35,
"A6": 25,
"A7": 26,
"D2": 27,
"D3": 14,
"D4": 12,
"D5": 13,
"D6": 15,
"D7": 16,
"D8": 17,
"D9": 4,
},
"featheresp32": {
"A0": 26,
"A1": 25,
@ -434,6 +494,18 @@ ESP32_BOARD_PINS = {
"SW4": 21,
},
"frogboard": {},
"healtypi4": {
"KEY_BUILTIN": 17,
"ADS1292_DRDY_PIN": 26,
"ADS1292_CS_PIN": 13,
"ADS1292_START_PIN": 14,
"ADS1292_PWDN_PIN": 27,
"AFE4490_CS_PIN": 21,
"AFE4490_DRDY_PIN": 39,
"AFE4490_PWDN_PIN": 4,
"PUSH_BUTTON": 17,
"SLIDE_SWITCH": 16,
},
"heltec_wifi_kit_32": {
"A1": 37,
"A2": 38,
@ -444,6 +516,7 @@ ESP32_BOARD_PINS = {
"SDA_OLED": 4,
"Vext": 21,
},
"heltec_wifi_kit_32_v2": "heltec_wifi_kit_32",
"heltec_wifi_lora_32": {
"BUTTON": 0,
"DIO0": 26,
@ -489,8 +562,68 @@ ESP32_BOARD_PINS = {
"SS": 18,
"Vext": 21,
},
"heltec_wireless_stick_lite": {
"LED_BUILTIN": 25,
"KEY_BUILTIN": 0,
"SS": 18,
"MOSI": 27,
"MISO": 19,
"SCK": 5,
"Vext": 21,
"LED": 25,
"RST_LoRa": 14,
"DIO0": 26,
"DIO1": 35,
"DIO2": 34,
},
"honeylemon": {
"LED_BUILTIN": 2,
"BUILTIN_KEY": 0,
},
"hornbill32dev": {"BUTTON": 0, "LED": 13},
"hornbill32minima": {"SS": 2},
"imbrios-logsens-v1p1": {
"LED_BUILTIN": 33,
"UART2_TX": 17,
"UART2_RX": 16,
"UART2_RTS": 4,
"CAN_TX": 17,
"CAN_RX": 16,
"CAN_TXDE": 4,
"SS": 15,
"MOSI": 13,
"MISO": 12,
"SCK": 14,
"SPI_SS1": 23,
"BUZZER_CTRL": 19,
"SD_CARD_DETECT": 35,
"SW2_BUILDIN": 0,
"SW3_BUILDIN": 36,
"SW4_BUILDIN": 34,
"LED1_BUILDIN": 32,
"LED2_BUILDIN": 33,
},
"inex_openkb": {
"LED_BUILTIN": 16,
"LDR_PIN": 36,
"SW1": 16,
"SW2": 14,
"BT_LED": 17,
"WIFI_LED": 2,
"NTP_LED": 15,
"IOT_LED": 12,
"BUZZER": 13,
"INPUT1": 32,
"INPUT2": 33,
"INPUT3": 34,
"INPUT4": 35,
"OUTPUT1": 26,
"OUTPUT2": 27,
"SDA0": 21,
"SCL0": 22,
"SDA1": 4,
"SCL1": 5,
},
"intorobot": {
"A1": 39,
"A2": 35,
@ -528,6 +661,40 @@ ESP32_BOARD_PINS = {
"iotaap_magnolia": {},
"iotbusio": {},
"iotbusproteus": {},
"kits-edu": {},
"labplus_mpython": {
"SDA": 23,
"SCL": 22,
"P0": 33,
"P1": 32,
"P2": 35,
"P3": 34,
"P4": 39,
"P5": 0,
"P6": 16,
"P7": 17,
"P8": 26,
"P9": 25,
"P10": 36,
"P11": 2,
"P13": 18,
"P14": 19,
"P15": 21,
"P16": 5,
"P19": 22,
"P20": 23,
"P": 27,
"Y": 14,
"T": 12,
"H": 13,
"O": 15,
"N": 4,
"BTN_A": 0,
"BTN_B": 2,
"SOUND": 36,
"LIGHT": 39,
"BUZZER": 16,
},
"lolin32": {"LED": 5},
"lolin32_lite": {"LED": 22},
"lolin_d32": {"LED": 5, "_VBAT": 35},
@ -554,6 +721,16 @@ ESP32_BOARD_PINS = {
"SDA": 12,
"SS": 18,
},
"m5stack-atom": {
"SDA": 26,
"SCL": 32,
"ADC1": 35,
"ADC2": 36,
"SS": 19,
"MOSI": 33,
"MISO": 23,
"SCK": 22,
},
"m5stack-core-esp32": {
"ADC1": 35,
"ADC2": 36,
@ -580,6 +757,26 @@ ESP32_BOARD_PINS = {
"RXD2": 16,
"TXD2": 17,
},
"m5stack-core2": {
"SDA": 32,
"SCL": 33,
"SS": 5,
"MOSI": 23,
"MISO": 38,
"SCK": 18,
"ADC1": 35,
"ADC2": 36,
},
"m5stack-coreink": {
"SDA": 32,
"SCL": 33,
"SS": 9,
"MOSI": 23,
"MISO": 34,
"SCK": 18,
"ADC1": 35,
"ADC2": 36,
},
"m5stack-fire": {
"ADC1": 35,
"ADC2": 36,
@ -630,6 +827,17 @@ ESP32_BOARD_PINS = {
"RXD2": 16,
"TXD2": 17,
},
"m5stack-timer-cam": {
"LED_BUILTIN": 2,
"SDA": 4,
"SCL": 13,
"SS": 5,
"MOSI": 23,
"MISO": 19,
"SCK": 18,
"ADC1": 35,
"ADC2": 36,
},
"m5stick-c": {
"ADC1": 35,
"ADC2": 36,
@ -664,6 +872,17 @@ ESP32_BOARD_PINS = {
"RIGHT_PUTTON": 34,
"YELLOW_LED": 18,
},
"mgbot-iotik32a": {
"LED_BUILTIN": 4,
"TX2": 17,
"RX2": 16,
},
"mgbot-iotik32b": {
"LED_BUILTIN": 18,
"IR": 27,
"TX2": 17,
"RX2": 16,
},
"mhetesp32devkit": {"LED": 2},
"mhetesp32minikit": {"LED": 2},
"microduino-core-esp32": {
@ -740,6 +959,7 @@ ESP32_BOARD_PINS = {
},
"node32s": {},
"nodemcu-32s": {"BUTTON": 0, "LED": 2},
"nscreen-32": {},
"odroid_esp32": {"ADC1": 35, "ADC2": 36, "LED": 2, "SCL": 4, "SDA": 15, "SS": 22},
"onehorse32dev": {"A1": 37, "A2": 38, "BUTTON": 0, "LED": 5},
"oroca_edubot": {
@ -766,6 +986,10 @@ ESP32_BOARD_PINS = {
"VBAT": 35,
},
"pico32": {},
"piranha_esp32": {
"LED_BUILTIN": 2,
"KEY_BUILTIN": 0,
},
"pocket_32": {"LED": 16},
"pycom_gpy": {
"A1": 37,
@ -778,7 +1002,14 @@ ESP32_BOARD_PINS = {
"SDA": 12,
"SS": 17,
},
"qchip": "heltec_wifi_kit_32",
"quantum": {},
"s_odi_ultra": {
"LED_BUILTIN": 2,
"LED_BUILTINB": 4,
},
"sensesiot_weizen": {},
"sg-o_airMon": {},
"sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16},
"tinypico": {},
"ttgo-lora32-v1": {
@ -790,6 +1021,26 @@ ESP32_BOARD_PINS = {
"SCK": 5,
"SS": 18,
},
"ttgo-lora32-v2": {
"LED_BUILTIN": 22,
"KEY_BUILTIN": 0,
"SS": 18,
"MOSI": 27,
"MISO": 19,
"SCK": 5,
"A1": 37,
"A2": 38,
},
"ttgo-lora32-v21": {
"LED_BUILTIN": 25,
"KEY_BUILTIN": 0,
"SS": 18,
"MOSI": 27,
"MISO": 19,
"SCK": 5,
"A1": 37,
"A2": 38,
},
"ttgo-t-beam": {"BUTTON": 39, "LED": 14, "MOSI": 27, "SCK": 5, "SS": 18},
"ttgo-t-watch": {"BUTTON": 36, "MISO": 2, "MOSI": 15, "SCK": 14, "SS": 13},
"ttgo-t1": {"LED": 22, "MISO": 2, "MOSI": 15, "SCK": 14, "SCL": 23, "SS": 13},
@ -855,6 +1106,32 @@ ESP32_BOARD_PINS = {
"T5": 5,
"T6": 4,
},
"wifiduino32": {
"LED_BUILTIN": 2,
"KEY_BUILTIN": 0,
"SDA": 5,
"SCL": 16,
"A0": 27,
"A1": 14,
"A2": 12,
"A3": 35,
"A4": 13,
"A5": 4,
"D0": 3,
"D1": 1,
"D2": 17,
"D3": 15,
"D4": 32,
"D5": 33,
"D6": 25,
"D7": 26,
"D8": 23,
"D9": 22,
"D10": 21,
"D11": 19,
"D12": 18,
"D13": 2,
},
"xinabox_cw02": {"LED": 27},
}

View File

@ -44,6 +44,7 @@ void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) {
for (int led = it.size(); led-- > 0;) {
it[led].set(Color::BLACK);
}
it.schedule_show();
}
void AdalightLightEffect::apply(light::AddressableLight &it, const Color &current_color) {
@ -133,6 +134,7 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL
it[led].set(Color(led_data[0], led_data[1], led_data[2], white));
}
it.schedule_show();
return CONSUMED;
}

View File

@ -0,0 +1,23 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import esp32_ble_tracker
from esphome.const import CONF_ID
DEPENDENCIES = ["esp32_ble_tracker"]
CODEOWNERS = ["@jeromelaban"]
airthings_ble_ns = cg.esphome_ns.namespace("airthings_ble")
AirthingsListener = airthings_ble_ns.class_(
"AirthingsListener", esp32_ble_tracker.ESPBTDeviceListener
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(AirthingsListener),
}
).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield esp32_ble_tracker.register_ble_device(var, config)

View File

@ -0,0 +1,33 @@
#include "airthings_listener.h"
#include "esphome/core/log.h"
#ifdef ARDUINO_ARCH_ESP32
namespace esphome {
namespace airthings_ble {
static const char *TAG = "airthings_ble";
bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
for (auto &it : device.get_manufacturer_datas()) {
if (it.uuid == esp32_ble_tracker::ESPBTUUID::from_uint32(0x0334)) {
if (it.data.size() < 4)
continue;
uint32_t sn = it.data[0];
sn |= ((uint32_t) it.data[1] << 8);
sn |= ((uint32_t) it.data[2] << 16);
sn |= ((uint32_t) it.data[3] << 24);
ESP_LOGD(TAG, "Found AirThings device Serial:%u (MAC: %s)", sn, device.address_str().c_str());
return true;
}
}
return false;
}
} // namespace airthings_ble
} // namespace esphome
#endif

View File

@ -0,0 +1,20 @@
#pragma once
#ifdef ARDUINO_ARCH_ESP32
#include "esphome/core/component.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include <BLEDevice.h>
namespace esphome {
namespace airthings_ble {
class AirthingsListener : public esp32_ble_tracker::ESPBTDeviceListener {
public:
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
};
} // namespace airthings_ble
} // namespace esphome
#endif

View File

@ -0,0 +1 @@
CODEOWNERS = ["@jeromelaban"]

View File

@ -0,0 +1,142 @@
#include "airthings_wave_plus.h"
#ifdef ARDUINO_ARCH_ESP32
namespace esphome {
namespace airthings_wave_plus {
void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
switch (event) {
case ESP_GATTC_OPEN_EVT: {
if (param->open.status == ESP_GATT_OK) {
ESP_LOGI(TAG, "Connected successfully!");
}
break;
}
case ESP_GATTC_DISCONNECT_EVT: {
ESP_LOGW(TAG, "Disconnected!");
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
this->handle = 0;
auto chr = this->parent()->get_characteristic(service_uuid, sensors_data_characteristic_uuid);
if (chr == nullptr) {
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid.to_string().c_str(),
sensors_data_characteristic_uuid.to_string().c_str());
break;
}
this->handle = chr->handle;
this->node_state = espbt::ClientState::Established;
request_read_values_();
break;
}
case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.conn_id != this->parent()->conn_id)
break;
if (param->read.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
break;
}
if (param->read.handle == this->handle) {
read_sensors_(param->read.value, param->read.value_len);
}
break;
}
default:
break;
}
}
void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
auto value = (WavePlusReadings *) raw_value;
if (sizeof(WavePlusReadings) <= value_len) {
ESP_LOGD(TAG, "version = %d", value->version);
if (value->version == 1) {
ESP_LOGD(TAG, "ambient light = %d", value->ambientLight);
this->humidity_sensor_->publish_state(value->humidity / 2.0f);
if (is_valid_radon_value_(value->radon)) {
this->radon_sensor_->publish_state(value->radon);
}
if (is_valid_radon_value_(value->radon_lt)) {
this->radon_long_term_sensor_->publish_state(value->radon_lt);
}
this->temperature_sensor_->publish_state(value->temperature / 100.0f);
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
if (is_valid_co2_value_(value->co2)) {
this->co2_sensor_->publish_state(value->co2);
}
if (is_valid_voc_value_(value->voc)) {
this->tvoc_sensor_->publish_state(value->voc);
}
// This instance must not stay connected
// so other clients can connect to it (e.g. the
// mobile app).
parent()->set_enabled(false);
} else {
ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version);
}
}
}
bool AirthingsWavePlus::is_valid_radon_value_(short radon) { return 0 <= radon && radon <= 16383; }
bool AirthingsWavePlus::is_valid_voc_value_(short voc) { return 0 <= voc && voc <= 16383; }
bool AirthingsWavePlus::is_valid_co2_value_(short co2) { return 0 <= co2 && co2 <= 16383; }
void AirthingsWavePlus::loop() {}
void AirthingsWavePlus::update() {
if (this->node_state != espbt::ClientState::Established) {
if (!parent()->enabled) {
ESP_LOGW(TAG, "Reconnecting to device");
parent()->set_enabled(true);
parent()->connect();
} else {
ESP_LOGW(TAG, "Connection in progress");
}
}
}
void AirthingsWavePlus::request_read_values_() {
auto status =
esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status);
}
}
void AirthingsWavePlus::dump_config() {
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
LOG_SENSOR(" ", "Radon", this->radon_sensor_);
LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
}
AirthingsWavePlus::AirthingsWavePlus() : PollingComponent(10000) {
auto service_bt = *BLEUUID::fromString(std::string("b42e1c08-ade7-11e4-89d3-123b93f75cba")).getNative();
auto characteristic_bt = *BLEUUID::fromString(std::string("b42e2a68-ade7-11e4-89d3-123b93f75cba")).getNative();
service_uuid = espbt::ESPBTUUID::from_uuid(service_bt);
sensors_data_characteristic_uuid = espbt::ESPBTUUID::from_uuid(characteristic_bt);
}
void AirthingsWavePlus::setup() {}
} // namespace airthings_wave_plus
} // namespace esphome
#endif // ARDUINO_ARCH_ESP32

View File

@ -0,0 +1,79 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <iterator>
#ifdef ARDUINO_ARCH_ESP32
#include <esp_gattc_api.h>
#include <BLEDevice.h>
using namespace esphome::ble_client;
namespace esphome {
namespace airthings_wave_plus {
static const char *TAG = "airthings_wave_plus";
class AirthingsWavePlus : public PollingComponent, public BLEClientNode {
public:
AirthingsWavePlus();
void setup() override;
void dump_config() override;
void update() override;
void loop() override;
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; }
void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; }
void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; }
void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
protected:
bool is_valid_radon_value_(short radon);
bool is_valid_voc_value_(short voc);
bool is_valid_co2_value_(short co2);
void read_sensors_(uint8_t *value, uint16_t value_len);
void request_read_values_();
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *radon_sensor_{nullptr};
sensor::Sensor *radon_long_term_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
sensor::Sensor *co2_sensor_{nullptr};
sensor::Sensor *tvoc_sensor_{nullptr};
uint16_t handle;
espbt::ESPBTUUID service_uuid;
espbt::ESPBTUUID sensors_data_characteristic_uuid;
struct WavePlusReadings {
uint8_t version;
uint8_t humidity;
uint8_t ambientLight;
uint8_t unused01;
uint16_t radon;
uint16_t radon_lt;
uint16_t temperature;
uint16_t pressure;
uint16_t co2;
uint16_t voc;
};
};
} // namespace airthings_wave_plus
} // namespace esphome
#endif // ARDUINO_ARCH_ESP32

View File

@ -0,0 +1,116 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, ble_client
from esphome.const import (
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_PRESSURE,
STATE_CLASS_MEASUREMENT,
UNIT_PERCENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
ICON_RADIOACTIVE,
CONF_ID,
CONF_RADON,
CONF_RADON_LONG_TERM,
CONF_HUMIDITY,
CONF_TVOC,
CONF_CO2,
CONF_PRESSURE,
CONF_TEMPERATURE,
UNIT_BECQUEREL_PER_CUBIC_METER,
UNIT_PARTS_PER_MILLION,
UNIT_PARTS_PER_BILLION,
ICON_RADIATOR,
)
DEPENDENCIES = ["ble_client"]
airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus")
AirthingsWavePlus = airthings_wave_plus_ns.class_(
"AirthingsWavePlus", cg.PollingComponent, ble_client.BLEClientNode
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(AirthingsWavePlus),
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
accuracy_decimals=0,
),
cv.Optional(CONF_RADON): sensor.sensor_schema(
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
icon=ICON_RADIOACTIVE,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema(
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
icon=ICON_RADIOACTIVE,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=1,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("5mins"))
.extend(ble_client.BLE_CLIENT_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await ble_client.register_ble_node(var, config)
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
cg.add(var.set_humidity(sens))
if CONF_RADON in config:
sens = await sensor.new_sensor(config[CONF_RADON])
cg.add(var.set_radon(sens))
if CONF_RADON_LONG_TERM in config:
sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM])
cg.add(var.set_radon_long_term(sens))
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature(sens))
if CONF_PRESSURE in config:
sens = await sensor.new_sensor(config[CONF_PRESSURE])
cg.add(var.set_pressure(sens))
if CONF_CO2 in config:
sens = await sensor.new_sensor(config[CONF_CO2])
cg.add(var.set_co2(sens))
if CONF_TVOC in config:
sens = await sensor.new_sensor(config[CONF_TVOC])
cg.add(var.set_tvoc(sens))

View File

@ -1,3 +1,5 @@
import base64
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
@ -6,6 +8,7 @@ from esphome.const import (
CONF_DATA,
CONF_DATA_TEMPLATE,
CONF_ID,
CONF_KEY,
CONF_PASSWORD,
CONF_PORT,
CONF_REBOOT_TIMEOUT,
@ -19,7 +22,7 @@ from esphome.const import (
from esphome.core import coroutine_with_priority
DEPENDENCIES = ["network"]
AUTO_LOAD = ["async_tcp"]
AUTO_LOAD = ["socket"]
CODEOWNERS = ["@OttoWinter"]
api_ns = cg.esphome_ns.namespace("api")
@ -41,6 +44,22 @@ SERVICE_ARG_NATIVE_TYPES = {
"float[]": cg.std_vector.template(float),
"string[]": cg.std_vector.template(cg.std_string),
}
CONF_ENCRYPTION = "encryption"
def validate_encryption_key(value):
value = cv.string_strict(value)
try:
decoded = base64.b64decode(value, validate=True)
except ValueError as err:
raise cv.Invalid("Invalid key format, please check it's using base64") from err
if len(decoded) != 32:
raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
# Return original data for roundtrip conversion
return value
CONFIG_SCHEMA = cv.Schema(
{
@ -63,6 +82,11 @@ CONFIG_SCHEMA = cv.Schema(
),
}
),
cv.Optional(CONF_ENCRYPTION): cv.Schema(
{
cv.Required(CONF_KEY): validate_encryption_key,
}
),
}
).extend(cv.COMPONENT_SCHEMA)
@ -92,6 +116,15 @@ async def to_code(config):
cg.add(var.register_user_service(trigger))
await automation.build_automation(trigger, func_args, conf)
if CONF_ENCRYPTION in config:
conf = config[CONF_ENCRYPTION]
decoded = base64.b64decode(conf[CONF_KEY])
cg.add(var.set_noise_psk(list(decoded)))
cg.add_define("USE_API_NOISE")
cg.add_library("esphome/noise-c", "0.1.1")
else:
cg.add_define("USE_API_PLAINTEXT")
cg.add_define("USE_API")
cg.add_global(api_ns.using)

View File

@ -473,7 +473,8 @@ message ListEntitiesSensorResponse {
bool force_update = 8;
string device_class = 9;
SensorStateClass state_class = 10;
SensorLastResetType last_reset_type = 11;
// Last reset type removed in 2021.9.0
SensorLastResetType legacy_last_reset_type = 11;
bool disabled_by_default = 12;
}
message SensorStateResponse {

View File

@ -2,6 +2,7 @@
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#include "esphome/core/version.h"
#include <cerrno>
#ifdef USE_DEEP_SLEEP
#include "esphome/components/deep_sleep/deep_sleep_component.h"
@ -18,145 +19,146 @@ namespace api {
static const char *const TAG = "api.connection";
APIConnection::APIConnection(AsyncClient *client, APIServer *parent)
: client_(client), parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) {
this->client_->onError([](void *s, AsyncClient *c, int8_t error) { ((APIConnection *) s)->on_error_(error); }, this);
this->client_->onDisconnect([](void *s, AsyncClient *c) { ((APIConnection *) s)->on_disconnect_(); }, this);
this->client_->onTimeout([](void *s, AsyncClient *c, uint32_t time) { ((APIConnection *) s)->on_timeout_(time); },
this);
this->client_->onData([](void *s, AsyncClient *c, void *buf,
size_t len) { ((APIConnection *) s)->on_data_(reinterpret_cast<uint8_t *>(buf), len); },
this);
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
: parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) {
this->proto_write_buffer_.reserve(64);
this->send_buffer_.reserve(64);
this->recv_buffer_.reserve(32);
this->client_info_ = this->client_->remoteIP().toString().c_str();
#if defined(USE_API_PLAINTEXT)
helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
#elif defined(USE_API_NOISE)
helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())};
#else
#error "No frame helper defined"
#endif
}
void APIConnection::start() {
this->last_traffic_ = millis();
}
APIConnection::~APIConnection() { delete this->client_; }
void APIConnection::on_error_(int8_t error) { this->remove_ = true; }
void APIConnection::on_disconnect_() { this->remove_ = true; }
void APIConnection::on_timeout_(uint32_t time) { this->on_fatal_error(); }
void APIConnection::on_data_(uint8_t *buf, size_t len) {
if (len == 0 || buf == nullptr)
APIError err = helper_->init();
if (err != APIError::OK) {
on_fatal_error();
ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
return;
this->recv_buffer_.insert(this->recv_buffer_.end(), buf, buf + len);
}
void APIConnection::parse_recv_buffer_() {
if (this->recv_buffer_.empty() || this->remove_)
return;
while (!this->recv_buffer_.empty()) {
if (this->recv_buffer_[0] != 0x00) {
ESP_LOGW(TAG, "Invalid preamble from %s", this->client_info_.c_str());
this->on_fatal_error();
return;
}
uint32_t i = 1;
const uint32_t size = this->recv_buffer_.size();
uint32_t consumed;
auto msg_size_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed);
if (!msg_size_varint.has_value())
// not enough data there yet
return;
i += consumed;
uint32_t msg_size = msg_size_varint->as_uint32();
auto msg_type_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed);
if (!msg_type_varint.has_value())
// not enough data there yet
return;
i += consumed;
uint32_t msg_type = msg_type_varint->as_uint32();
if (size - i < msg_size)
// message body not fully received
return;
uint8_t *msg = &this->recv_buffer_[i];
this->read_message(msg_size, msg_type, msg);
if (this->remove_)
return;
// pop front
uint32_t total = i + msg_size;
this->recv_buffer_.erase(this->recv_buffer_.begin(), this->recv_buffer_.begin() + total);
this->last_traffic_ = millis();
}
}
void APIConnection::disconnect_client() {
this->client_->close();
this->remove_ = true;
client_info_ = helper_->getpeername();
helper_->set_log_info(client_info_);
}
void APIConnection::loop() {
if (this->remove_)
return;
if (this->next_close_) {
this->disconnect_client();
return;
}
if (!network_is_connected()) {
// when network is disconnected force disconnect immediately
// don't wait for timeout
this->on_fatal_error();
ESP_LOGW(TAG, "%s: Network unavailable, disconnecting", client_info_.c_str());
return;
}
if (this->client_->disconnected()) {
// failsafe for disconnect logic
this->on_disconnect_();
if (this->next_close_) {
// requested a disconnect
this->helper_->close();
this->remove_ = true;
return;
}
this->parse_recv_buffer_();
APIError err = helper_->loop();
if (err != APIError::OK) {
on_fatal_error();
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
return;
}
ReadPacketBuffer buffer;
err = helper_->read_packet(&buffer);
if (err == APIError::WOULD_BLOCK) {
// pass
} else if (err != APIError::OK) {
on_fatal_error();
if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) {
ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str());
} else {
ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
}
return;
} else {
this->last_traffic_ = millis();
// read a packet
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
if (this->remove_)
return;
}
this->list_entities_iterator_.advance();
this->initial_state_iterator_.advance();
const uint32_t keepalive = 60000;
const uint32_t now = millis();
if (this->sent_ping_) {
// Disconnect if not responded within 2.5*keepalive
if (millis() - this->last_traffic_ > (keepalive * 5) / 2) {
ESP_LOGW(TAG, "'%s' didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str());
this->disconnect_client();
if (now - this->last_traffic_ > (keepalive * 5) / 2) {
on_fatal_error();
ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str());
}
} else if (millis() - this->last_traffic_ > keepalive) {
} else if (now - this->last_traffic_ > keepalive) {
this->sent_ping_ = true;
this->send_ping_request(PingRequest());
}
#ifdef USE_ESP32_CAMERA
if (this->image_reader_.available()) {
uint32_t space = this->client_->space();
// reserve 15 bytes for metadata, and at least 64 bytes of data
if (space >= 15 + 64) {
uint32_t to_send = std::min(space - 15, this->image_reader_.available());
auto buffer = this->create_buffer();
// fixed32 key = 1;
buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash());
// bytes data = 2;
buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send);
// bool done = 3;
bool done = this->image_reader_.available() == to_send;
buffer.encode_bool(3, done);
bool success = this->send_buffer(buffer, 44);
if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) {
uint32_t to_send = std::min((size_t) 1024, this->image_reader_.available());
auto buffer = this->create_buffer();
// fixed32 key = 1;
buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash());
// bytes data = 2;
buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send);
// bool done = 3;
bool done = this->image_reader_.available() == to_send;
buffer.encode_bool(3, done);
bool success = this->send_buffer(buffer, 44);
if (success) {
this->image_reader_.consume_data(to_send);
}
if (success && done) {
this->image_reader_.return_image();
}
if (success) {
this->image_reader_.consume_data(to_send);
}
if (success && done) {
this->image_reader_.return_image();
}
}
#endif
if (state_subs_at_ != -1) {
const auto &subs = this->parent_->get_state_subs();
if (state_subs_at_ >= subs.size()) {
state_subs_at_ = -1;
} else {
auto &it = subs[state_subs_at_];
SubscribeHomeAssistantStateResponse resp;
resp.entity_id = it.entity_id;
resp.attribute = it.attribute.value();
if (this->send_subscribe_home_assistant_state_response(resp)) {
state_subs_at_++;
}
}
}
}
std::string get_default_unique_id(const std::string &component_type, Nameable *nameable) {
return App.get_name() + component_type + nameable->get_object_id();
}
DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) {
// remote initiated disconnect_client
// don't close yet, we still need to send the disconnect response
// close will happen on next loop
ESP_LOGD(TAG, "%s requested disconnected", client_info_.c_str());
this->next_close_ = true;
DisconnectResponse resp;
return resp;
}
void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
// pass
}
#ifdef USE_BINARY_SENSOR
bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state) {
if (!this->state_subscription_)
@ -241,6 +243,9 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) {
#endif
#ifdef USE_FAN
// Shut-up about usage of deprecated speed_level_to_enum/speed_enum_to_level functions for a bit.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
bool APIConnection::send_fan_state(fan::FanState *fan) {
if (!this->state_subscription_)
return false;
@ -295,6 +300,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
call.set_direction(static_cast<fan::FanDirection>(msg.direction));
call.perform();
}
#pragma GCC diagnostic pop
#endif
#ifdef USE_LIGHT
@ -417,7 +423,6 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) {
msg.force_update = sensor->get_force_update();
msg.device_class = sensor->get_device_class();
msg.state_class = static_cast<enums::SensorStateClass>(sensor->state_class);
msg.last_reset_type = static_cast<enums::SensorLastResetType>(sensor->last_reset_type);
msg.disabled_by_default = sensor->is_disabled_by_default();
return this->send_list_entities_sensor_response(msg);
@ -709,8 +714,8 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin
}
HelloResponse APIConnection::hello(const HelloRequest &msg) {
this->client_info_ = msg.client_info + " (" + this->client_->remoteIP().toString().c_str();
this->client_info_ += ")";
this->client_info_ = msg.client_info + " (" + this->helper_->getpeername() + ")";
this->helper_->set_log_info(client_info_);
ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str());
HelloResponse resp;
@ -727,7 +732,7 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
// bool invalid_password = 1;
resp.invalid_password = !correct;
if (correct) {
ESP_LOGD(TAG, "Client '%s' connected successfully!", this->client_info_.c_str());
ESP_LOGD(TAG, "%s: Connected successfully", this->client_info_.c_str());
this->connection_state_ = ConnectionState::AUTHENTICATED;
#ifdef USE_HOMEASSISTANT_TIME
@ -745,9 +750,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
resp.mac_address = get_mac_address_pretty();
resp.esphome_version = ESPHOME_VERSION;
resp.compilation_time = App.get_compilation_time();
#ifdef ARDUINO_BOARD
resp.model = ARDUINO_BOARD;
#endif
resp.model = ESPHOME_BOARD;
#ifdef USE_DEEP_SLEEP
resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
#endif
@ -775,57 +778,39 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
}
}
void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) {
for (auto &it : this->parent_->get_state_subs()) {
SubscribeHomeAssistantStateResponse resp;
resp.entity_id = it.entity_id;
resp.attribute = it.attribute.value();
if (!this->send_subscribe_home_assistant_state_response(resp)) {
this->on_fatal_error();
return;
}
}
state_subs_at_ = 0;
}
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) {
if (this->remove_)
return false;
if (!this->helper_->can_write_without_blocking())
return false;
std::vector<uint8_t> header;
header.push_back(0x00);
ProtoVarInt(buffer.get_buffer()->size()).encode(header);
ProtoVarInt(message_type).encode(header);
size_t needed_space = buffer.get_buffer()->size() + header.size();
if (needed_space > this->client_->space()) {
delay(0);
if (needed_space > this->client_->space()) {
// SubscribeLogsResponse
if (message_type != 29) {
ESP_LOGV(TAG, "Cannot send message because of TCP buffer space");
}
delay(0);
return false;
APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size());
if (err == APIError::WOULD_BLOCK)
return false;
if (err != APIError::OK) {
on_fatal_error();
if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) {
ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str());
} else {
ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
}
return false;
}
this->client_->add(reinterpret_cast<char *>(header.data()), header.size(),
ASYNC_WRITE_FLAG_COPY | ASYNC_WRITE_FLAG_MORE);
this->client_->add(reinterpret_cast<char *>(buffer.get_buffer()->data()), buffer.get_buffer()->size(),
ASYNC_WRITE_FLAG_COPY);
bool ret = this->client_->send();
return ret;
this->last_traffic_ = millis();
return true;
}
void APIConnection::on_unauthenticated_access() {
ESP_LOGD(TAG, "'%s' tried to access without authentication.", this->client_info_.c_str());
this->on_fatal_error();
ESP_LOGD(TAG, "%s: tried to access without authentication.", this->client_info_.c_str());
}
void APIConnection::on_no_setup_connection() {
ESP_LOGD(TAG, "'%s' tried to access without full connection.", this->client_info_.c_str());
this->on_fatal_error();
ESP_LOGD(TAG, "%s: tried to access without full connection.", this->client_info_.c_str());
}
void APIConnection::on_fatal_error() {
ESP_LOGV(TAG, "Error: Disconnecting %s", this->client_info_.c_str());
this->client_->close();
this->helper_->close();
this->remove_ = true;
}

View File

@ -5,16 +5,17 @@
#include "api_pb2.h"
#include "api_pb2_service.h"
#include "api_server.h"
#include "api_frame_helper.h"
namespace esphome {
namespace api {
class APIConnection : public APIServerConnection {
public:
APIConnection(AsyncClient *client, APIServer *parent);
virtual ~APIConnection();
APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
virtual ~APIConnection() = default;
void disconnect_client();
void start();
void loop();
bool send_list_info_done() {
@ -86,10 +87,7 @@ class APIConnection : public APIServerConnection {
}
#endif
void on_disconnect_response(const DisconnectResponse &value) override {
// we initiated disconnect_client
this->next_close_ = true;
}
void on_disconnect_response(const DisconnectResponse &value) override;
void on_ping_response(const PingResponse &value) override {
// we initiated ping
this->sent_ping_ = false;
@ -100,12 +98,7 @@ class APIConnection : public APIServerConnection {
#endif
HelloResponse hello(const HelloRequest &msg) override;
ConnectResponse connect(const ConnectRequest &msg) override;
DisconnectResponse disconnect(const DisconnectRequest &msg) override {
// remote initiated disconnect_client
this->next_close_ = true;
DisconnectResponse resp;
return resp;
}
DisconnectResponse disconnect(const DisconnectRequest &msg) override;
PingResponse ping(const PingRequest &msg) override { return {}; }
DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override;
void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
@ -135,19 +128,16 @@ class APIConnection : public APIServerConnection {
void on_unauthenticated_access() override;
void on_no_setup_connection() override;
ProtoWriteBuffer create_buffer() override {
this->send_buffer_.clear();
return {&this->send_buffer_};
// FIXME: ensure no recursive writes can happen
this->proto_write_buffer_.clear();
return {&this->proto_write_buffer_};
}
bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override;
protected:
friend APIServer;
void on_error_(int8_t error);
void on_disconnect_();
void on_timeout_(uint32_t time);
void on_data_(uint8_t *buf, size_t len);
void parse_recv_buffer_();
bool send_(const void *buf, size_t len, bool force);
enum class ConnectionState {
WAITING_FOR_HELLO,
@ -157,8 +147,10 @@ class APIConnection : public APIServerConnection {
bool remove_{false};
std::vector<uint8_t> send_buffer_;
std::vector<uint8_t> recv_buffer_;
// Buffer used to encode proto messages
// Re-use to prevent allocations
std::vector<uint8_t> proto_write_buffer_;
std::unique_ptr<APIFrameHelper> helper_;
std::string client_info_;
#ifdef USE_ESP32_CAMERA
@ -170,12 +162,11 @@ class APIConnection : public APIServerConnection {
uint32_t last_traffic_;
bool sent_ping_{false};
bool service_call_subscription_{false};
bool current_nodelay_{false};
bool next_close_{false};
AsyncClient *client_;
bool next_close_ = false;
APIServer *parent_;
InitialStateIterator initial_state_iterator_;
ListEntitiesIterator list_entities_iterator_;
int state_subs_at_ = -1;
};
} // namespace api

View File

@ -0,0 +1,961 @@
#include "api_frame_helper.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "proto.h"
namespace esphome {
namespace api {
static const char *const TAG = "api.socket";
/// Is the given return value (from read/write syscalls) a wouldblock error?
bool is_would_block(ssize_t ret) {
if (ret == -1) {
return errno == EWOULDBLOCK || errno == EAGAIN;
}
return ret == 0;
}
const char *api_error_to_str(APIError err) {
// not using switch to ensure compiler doesn't try to build a big table out of it
if (err == APIError::OK) {
return "OK";
} else if (err == APIError::WOULD_BLOCK) {
return "WOULD_BLOCK";
} else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) {
return "BAD_HANDSHAKE_PACKET_LEN";
} else if (err == APIError::BAD_INDICATOR) {
return "BAD_INDICATOR";
} else if (err == APIError::BAD_DATA_PACKET) {
return "BAD_DATA_PACKET";
} else if (err == APIError::TCP_NODELAY_FAILED) {
return "TCP_NODELAY_FAILED";
} else if (err == APIError::TCP_NONBLOCKING_FAILED) {
return "TCP_NONBLOCKING_FAILED";
} else if (err == APIError::CLOSE_FAILED) {
return "CLOSE_FAILED";
} else if (err == APIError::SHUTDOWN_FAILED) {
return "SHUTDOWN_FAILED";
} else if (err == APIError::BAD_STATE) {
return "BAD_STATE";
} else if (err == APIError::BAD_ARG) {
return "BAD_ARG";
} else if (err == APIError::SOCKET_READ_FAILED) {
return "SOCKET_READ_FAILED";
} else if (err == APIError::SOCKET_WRITE_FAILED) {
return "SOCKET_WRITE_FAILED";
} else if (err == APIError::HANDSHAKESTATE_READ_FAILED) {
return "HANDSHAKESTATE_READ_FAILED";
} else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) {
return "HANDSHAKESTATE_WRITE_FAILED";
} else if (err == APIError::HANDSHAKESTATE_BAD_STATE) {
return "HANDSHAKESTATE_BAD_STATE";
} else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) {
return "CIPHERSTATE_DECRYPT_FAILED";
} else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) {
return "CIPHERSTATE_ENCRYPT_FAILED";
} else if (err == APIError::OUT_OF_MEMORY) {
return "OUT_OF_MEMORY";
} else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) {
return "HANDSHAKESTATE_SETUP_FAILED";
} else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) {
return "HANDSHAKESTATE_SPLIT_FAILED";
} else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) {
return "BAD_HANDSHAKE_ERROR_BYTE";
}
return "UNKNOWN";
}
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__)
// uncomment to log raw packets
//#define HELPER_LOG_PACKETS
#ifdef USE_API_NOISE
static const char *const PROLOGUE_INIT = "NoiseAPIInit";
/// Convert a noise error code to a readable error
std::string noise_err_to_str(int err) {
if (err == NOISE_ERROR_NO_MEMORY)
return "NO_MEMORY";
if (err == NOISE_ERROR_UNKNOWN_ID)
return "UNKNOWN_ID";
if (err == NOISE_ERROR_UNKNOWN_NAME)
return "UNKNOWN_NAME";
if (err == NOISE_ERROR_MAC_FAILURE)
return "MAC_FAILURE";
if (err == NOISE_ERROR_NOT_APPLICABLE)
return "NOT_APPLICABLE";
if (err == NOISE_ERROR_SYSTEM)
return "SYSTEM";
if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
return "REMOTE_KEY_REQUIRED";
if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
return "LOCAL_KEY_REQUIRED";
if (err == NOISE_ERROR_PSK_REQUIRED)
return "PSK_REQUIRED";
if (err == NOISE_ERROR_INVALID_LENGTH)
return "INVALID_LENGTH";
if (err == NOISE_ERROR_INVALID_PARAM)
return "INVALID_PARAM";
if (err == NOISE_ERROR_INVALID_STATE)
return "INVALID_STATE";
if (err == NOISE_ERROR_INVALID_NONCE)
return "INVALID_NONCE";
if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
return "INVALID_PRIVATE_KEY";
if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
return "INVALID_PUBLIC_KEY";
if (err == NOISE_ERROR_INVALID_FORMAT)
return "INVALID_FORMAT";
if (err == NOISE_ERROR_INVALID_SIGNATURE)
return "INVALID_SIGNATURE";
return to_string(err);
}
/// Initialize the frame helper, returns OK if successful.
APIError APINoiseFrameHelper::init() {
if (state_ != State::INITIALIZE || socket_ == nullptr) {
HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE;
}
int err = socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nonblocking failed with errno %d", errno);
return APIError::TCP_NONBLOCKING_FAILED;
}
int enable = 1;
err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nodelay failed with errno %d", errno);
return APIError::TCP_NODELAY_FAILED;
}
// init prologue
prologue_.insert(prologue_.end(), PROLOGUE_INIT, PROLOGUE_INIT + strlen(PROLOGUE_INIT));
state_ = State::CLIENT_HELLO;
return APIError::OK;
}
/// Run through handshake messages (if in that phase)
APIError APINoiseFrameHelper::loop() {
APIError err = state_action_();
if (err == APIError::WOULD_BLOCK)
return APIError::OK;
if (err != APIError::OK)
return err;
if (!tx_buf_.empty()) {
err = try_send_tx_buf_();
if (err != APIError::OK) {
return err;
}
}
return APIError::OK;
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
*
* @param frame: The struct to hold the frame information in.
* msg_start: points to the start of the payload - this pointer is only valid until the next
* try_receive_raw_ call
*
* @return 0 if a full packet is in rx_buf_
* @return -1 if error, check errno.
*
* errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
* errno ENOMEM: Not enough memory for reading packet.
* errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
*/
APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
int err;
APIError aerr;
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
}
// read header
if (rx_header_buf_len_ < 3) {
// no header information yet
size_t to_read = 3 - rx_header_buf_len_;
ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
if (is_would_block(received)) {
return APIError::WOULD_BLOCK;
} else if (received == -1) {
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
}
rx_header_buf_len_ += received;
if (received != to_read) {
// not a full read
return APIError::WOULD_BLOCK;
}
// header reading done
}
// read body
uint8_t indicator = rx_header_buf_[0];
if (indicator != 0x01) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", indicator);
return APIError::BAD_INDICATOR;
}
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
if (state_ != State::DATA && msg_size > 128) {
// for handshake message only permit up to 128 bytes
state_ = State::FAILED;
HELPER_LOG("Bad packet len for handshake: %d", msg_size);
return APIError::BAD_HANDSHAKE_PACKET_LEN;
}
// reserve space for body
if (rx_buf_.size() != msg_size) {
rx_buf_.resize(msg_size);
}
if (rx_buf_len_ < msg_size) {
// more data to read
size_t to_read = msg_size - rx_buf_len_;
ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
if (is_would_block(received)) {
return APIError::WOULD_BLOCK;
} else if (received == -1) {
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
}
rx_buf_len_ += received;
if (received != to_read) {
// not all read
return APIError::WOULD_BLOCK;
}
}
// uncomment for even more debugging
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str());
#endif
frame->msg = std::move(rx_buf_);
// consume msg
rx_buf_ = {};
rx_buf_len_ = 0;
rx_header_buf_len_ = 0;
return APIError::OK;
}
/** To be called from read/write methods.
*
* This method runs through the internal handshake methods, if in that state.
*
* If the handshake is still active when this method returns and a read/write can't take place at
* the moment, returns WOULD_BLOCK.
* If an error occured, returns that error. Only returns OK if the transport is ready for data
* traffic.
*/
APIError APINoiseFrameHelper::state_action_() {
int err;
APIError aerr;
if (state_ == State::INITIALIZE) {
HELPER_LOG("Bad state for method: %d", (int) state_);
return APIError::BAD_STATE;
}
if (state_ == State::CLIENT_HELLO) {
// waiting for client hello
ParsedFrame frame;
aerr = try_read_frame_(&frame);
if (aerr == APIError::BAD_INDICATOR) {
send_explicit_handshake_reject_("Bad indicator byte");
return aerr;
}
if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
send_explicit_handshake_reject_("Bad handshake packet len");
return aerr;
}
if (aerr != APIError::OK)
return aerr;
// ignore contents, may be used in future for flags
prologue_.push_back((uint8_t)(frame.msg.size() >> 8));
prologue_.push_back((uint8_t) frame.msg.size());
prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end());
state_ = State::SERVER_HELLO;
}
if (state_ == State::SERVER_HELLO) {
// send server hello
uint8_t msg[1];
msg[0] = 0x01; // chosen proto
aerr = write_frame_(msg, 1);
if (aerr != APIError::OK)
return aerr;
// start handshake
aerr = init_handshake_();
if (aerr != APIError::OK)
return aerr;
state_ = State::HANDSHAKE;
}
if (state_ == State::HANDSHAKE) {
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE) {
// waiting for handshake msg
ParsedFrame frame;
aerr = try_read_frame_(&frame);
if (aerr == APIError::BAD_INDICATOR) {
send_explicit_handshake_reject_("Bad indicator byte");
return aerr;
}
if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
send_explicit_handshake_reject_("Bad handshake packet len");
return aerr;
}
if (aerr != APIError::OK)
return aerr;
if (frame.msg.empty()) {
send_explicit_handshake_reject_("Empty handshake message");
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} else if (frame.msg[0] != 0x00) {
HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]);
send_explicit_handshake_reject_("Bad handshake error byte");
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
}
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1);
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str());
if (err == NOISE_ERROR_MAC_FAILURE) {
send_explicit_handshake_reject_("Handshake MAC failure");
} else {
send_explicit_handshake_reject_("Handshake error");
}
return APIError::HANDSHAKESTATE_READ_FAILED;
}
aerr = check_handshake_finished_();
if (aerr != APIError::OK)
return aerr;
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
uint8_t buffer[65];
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_write_message failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_WRITE_FAILED;
}
buffer[0] = 0x00; // success
aerr = write_frame_(buffer, mbuf.size + 1);
if (aerr != APIError::OK)
return aerr;
aerr = check_handshake_finished_();
if (aerr != APIError::OK)
return aerr;
} else {
// bad state for action
state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
}
if (state_ == State::CLOSED || state_ == State::FAILED) {
return APIError::BAD_STATE;
}
return APIError::OK;
}
void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) {
std::vector<uint8_t> data;
data.resize(reason.length() + 1);
data[0] = 0x01; // failure
for (size_t i = 0; i < reason.length(); i++) {
data[i + 1] = (uint8_t) reason[i];
}
// temporarily remove failed state
auto orig_state = state_;
state_ = State::EXPLICIT_REJECT;
write_frame_(data.data(), data.size());
state_ = orig_state;
}
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
int err;
APIError aerr;
aerr = state_action_();
if (aerr != APIError::OK) {
return aerr;
}
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
ParsedFrame frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK)
return aerr;
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, frame.msg.data(), frame.msg.size(), frame.msg.size());
err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_cipherstate_decrypt failed: %s", noise_err_to_str(err).c_str());
return APIError::CIPHERSTATE_DECRYPT_FAILED;
}
size_t msg_size = mbuf.size;
uint8_t *msg_data = frame.msg.data();
if (msg_size < 4) {
state_ = State::FAILED;
HELPER_LOG("Bad data packet: size %d too short", msg_size);
return APIError::BAD_DATA_PACKET;
}
// uint16_t type;
// uint16_t data_len;
// uint8_t *data;
// uint8_t *padding; zero or more bytes to fill up the rest of the packet
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
if (data_len > msg_size - 4) {
state_ = State::FAILED;
HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
return APIError::BAD_DATA_PACKET;
}
buffer->container = std::move(frame.msg);
buffer->data_offset = 4;
buffer->data_len = data_len;
buffer->type = type;
return APIError::OK;
}
bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
int err;
APIError aerr;
aerr = state_action_();
if (aerr != APIError::OK) {
return aerr;
}
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
size_t padding = 0;
size_t msg_len = 4 + payload_len + padding;
size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_);
auto tmpbuf = std::unique_ptr<uint8_t[]>{new (std::nothrow) uint8_t[frame_len]};
if (tmpbuf == nullptr) {
HELPER_LOG("Could not allocate for writing packet");
return APIError::OUT_OF_MEMORY;
}
tmpbuf[0] = 0x01; // indicator
// tmpbuf[1], tmpbuf[2] to be set later
const uint8_t msg_offset = 3;
const uint8_t payload_offset = msg_offset + 4;
tmpbuf[msg_offset + 0] = (uint8_t)(type >> 8); // type
tmpbuf[msg_offset + 1] = (uint8_t) type;
tmpbuf[msg_offset + 2] = (uint8_t)(payload_len >> 8); // data_len
tmpbuf[msg_offset + 3] = (uint8_t) payload_len;
// copy data
std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]);
// fill padding with zeros
std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0);
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset);
err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_cipherstate_encrypt failed: %s", noise_err_to_str(err).c_str());
return APIError::CIPHERSTATE_ENCRYPT_FAILED;
}
size_t total_len = 3 + mbuf.size;
tmpbuf[1] = (uint8_t)(mbuf.size >> 8);
tmpbuf[2] = (uint8_t) mbuf.size;
// write raw to not have two packets sent if NAGLE disabled
aerr = write_raw_(&tmpbuf[0], total_len);
if (aerr != APIError::OK) {
return aerr;
}
return APIError::OK;
}
APIError APINoiseFrameHelper::try_send_tx_buf_() {
// try send from tx_buf
while (state_ != State::CLOSED && !tx_buf_.empty()) {
ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
if (sent == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN)
break;
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
} else if (sent == 0) {
break;
}
// TODO: inefficient if multiple packets in txbuf
// replace with deque of buffers
tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
}
return APIError::OK;
}
/** Write the data to the socket, or buffer it a write would block
*
* @param data The data to write
* @param len The length of data
*/
APIError APINoiseFrameHelper::write_raw_(const uint8_t *data, size_t len) {
if (len == 0)
return APIError::OK;
int err;
APIError aerr;
// uncomment for even more debugging
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str());
#endif
if (!tx_buf_.empty()) {
// try to empty tx_buf_ first
aerr = try_send_tx_buf_();
if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
return aerr;
}
if (!tx_buf_.empty()) {
// tx buf not empty, can't write now because then stream would be inconsistent
tx_buf_.insert(tx_buf_.end(), data, data + len);
return APIError::OK;
}
ssize_t sent = socket_->write(data, len);
if (is_would_block(sent)) {
// operation would block, add buffer to tx_buf
tx_buf_.insert(tx_buf_.end(), data, data + len);
return APIError::OK;
} else if (sent == -1) {
// an error occured
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
} else if (sent != len) {
// partially sent, add end to tx_buf
tx_buf_.insert(tx_buf_.end(), data + sent, data + len);
return APIError::OK;
}
// fully sent
return APIError::OK;
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
APIError aerr;
uint8_t header[3];
header[0] = 0x01; // indicator
header[1] = (uint8_t)(len >> 8);
header[2] = (uint8_t) len;
aerr = write_raw_(header, 3);
if (aerr != APIError::OK)
return aerr;
aerr = write_raw_(data, len);
return aerr;
}
/** Initiate the data structures for the handshake.
*
* @return 0 on success, -1 on error (check errno)
*/
APIError APINoiseFrameHelper::init_handshake_() {
int err;
memset(&nid_, 0, sizeof(nid_));
// const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
// err = noise_protocol_name_to_id(&nid_, proto, strlen(proto));
nid_.pattern_id = NOISE_PATTERN_NN;
nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY;
nid_.dh_id = NOISE_DH_CURVE25519;
nid_.prefix_id = NOISE_PREFIX_STANDARD;
nid_.hybrid_id = NOISE_DH_NONE;
nid_.hash_id = NOISE_HASH_SHA256;
nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0;
err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_new_by_id failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_SETUP_FAILED;
}
const auto &psk = ctx_->get_psk();
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_set_pre_shared_key failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_SETUP_FAILED;
}
err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_set_prologue failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_SETUP_FAILED;
}
// set_prologue copies it into handshakestate, so we can get rid of it now
prologue_ = {};
err = noise_handshakestate_start(handshake_);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_start failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_SETUP_FAILED;
}
return APIError::OK;
}
APIError APINoiseFrameHelper::check_handshake_finished_() {
assert(state_ == State::HANDSHAKE);
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
return APIError::OK;
if (action != NOISE_ACTION_SPLIT) {
state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_handshakestate_split failed: %s", noise_err_to_str(err).c_str());
return APIError::HANDSHAKESTATE_SPLIT_FAILED;
}
HELPER_LOG("Handshake complete!");
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
state_ = State::DATA;
return APIError::OK;
}
APINoiseFrameHelper::~APINoiseFrameHelper() {
if (handshake_ != nullptr) {
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
}
if (send_cipher_ != nullptr) {
noise_cipherstate_free(send_cipher_);
send_cipher_ = nullptr;
}
if (recv_cipher_ != nullptr) {
noise_cipherstate_free(recv_cipher_);
recv_cipher_ = nullptr;
}
}
APIError APINoiseFrameHelper::close() {
state_ = State::CLOSED;
int err = socket_->close();
if (err == -1)
return APIError::CLOSE_FAILED;
return APIError::OK;
}
APIError APINoiseFrameHelper::shutdown(int how) {
int err = socket_->shutdown(how);
if (err == -1)
return APIError::SHUTDOWN_FAILED;
if (how == SHUT_RDWR) {
state_ = State::CLOSED;
}
return APIError::OK;
}
extern "C" {
// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
void noise_rand_bytes(void *output, size_t len) { esphome::fill_random(reinterpret_cast<uint8_t *>(output), len); }
}
#endif // USE_API_NOISE
#ifdef USE_API_PLAINTEXT
/// Initialize the frame helper, returns OK if successful.
APIError APIPlaintextFrameHelper::init() {
if (state_ != State::INITIALIZE || socket_ == nullptr) {
HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE;
}
int err = socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nonblocking failed with errno %d", errno);
return APIError::TCP_NONBLOCKING_FAILED;
}
int enable = 1;
err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nodelay failed with errno %d", errno);
return APIError::TCP_NODELAY_FAILED;
}
state_ = State::DATA;
return APIError::OK;
}
/// Not used for plaintext
APIError APIPlaintextFrameHelper::loop() {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
// try send pending TX data
if (!tx_buf_.empty()) {
APIError err = try_send_tx_buf_();
if (err != APIError::OK) {
return err;
}
}
return APIError::OK;
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
*
* @param frame: The struct to hold the frame information in.
* msg: store the parsed frame in that struct
*
* @return See APIError
*
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
*/
APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
int err;
APIError aerr;
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
}
// read header
while (!rx_header_parsed_) {
uint8_t data;
ssize_t received = socket_->read(&data, 1);
if (is_would_block(received)) {
return APIError::WOULD_BLOCK;
} else if (received == -1) {
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
}
rx_header_buf_.push_back(data);
// try parse header
if (rx_header_buf_[0] != 0x00) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
return APIError::BAD_INDICATOR;
}
size_t i = 1;
uint32_t consumed = 0;
auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
if (!msg_size_varint.has_value()) {
// not enough data there yet
continue;
}
i += consumed;
rx_header_parsed_len_ = msg_size_varint->as_uint32();
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
if (!msg_type_varint.has_value()) {
// not enough data there yet
continue;
}
rx_header_parsed_type_ = msg_type_varint->as_uint32();
rx_header_parsed_ = true;
}
// header reading done
// reserve space for body
if (rx_buf_.size() != rx_header_parsed_len_) {
rx_buf_.resize(rx_header_parsed_len_);
}
if (rx_buf_len_ < rx_header_parsed_len_) {
// more data to read
size_t to_read = rx_header_parsed_len_ - rx_buf_len_;
ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
if (is_would_block(received)) {
return APIError::WOULD_BLOCK;
} else if (received == -1) {
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
}
rx_buf_len_ += received;
if (received != to_read) {
// not all read
return APIError::WOULD_BLOCK;
}
}
// uncomment for even more debugging
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str());
#endif
frame->msg = std::move(rx_buf_);
// consume msg
rx_buf_ = {};
rx_buf_len_ = 0;
rx_header_buf_.clear();
rx_header_parsed_ = false;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
int err;
APIError aerr;
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
ParsedFrame frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK)
return aerr;
buffer->container = std::move(frame.msg);
buffer->data_offset = 0;
buffer->data_len = rx_header_parsed_len_;
buffer->type = rx_header_parsed_type_;
return APIError::OK;
}
bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
int err;
APIError aerr;
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
std::vector<uint8_t> header;
header.push_back(0x00);
ProtoVarInt(payload_len).encode(header);
ProtoVarInt(type).encode(header);
aerr = write_raw_(&header[0], header.size());
if (aerr != APIError::OK) {
return aerr;
}
aerr = write_raw_(payload, payload_len);
if (aerr != APIError::OK) {
return aerr;
}
return APIError::OK;
}
APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
// try send from tx_buf
while (state_ != State::CLOSED && !tx_buf_.empty()) {
ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
if (is_would_block(sent)) {
break;
} else if (sent == -1) {
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
}
// TODO: inefficient if multiple packets in txbuf
// replace with deque of buffers
tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
}
return APIError::OK;
}
/** Write the data to the socket, or buffer it a write would block
*
* @param data The data to write
* @param len The length of data
*/
APIError APIPlaintextFrameHelper::write_raw_(const uint8_t *data, size_t len) {
if (len == 0)
return APIError::OK;
int err;
APIError aerr;
// uncomment for even more debugging
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str());
#endif
if (!tx_buf_.empty()) {
// try to empty tx_buf_ first
aerr = try_send_tx_buf_();
if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
return aerr;
}
if (!tx_buf_.empty()) {
// tx buf not empty, can't write now because then stream would be inconsistent
tx_buf_.insert(tx_buf_.end(), data, data + len);
return APIError::OK;
}
ssize_t sent = socket_->write(data, len);
if (is_would_block(sent)) {
// operation would block, add buffer to tx_buf
tx_buf_.insert(tx_buf_.end(), data, data + len);
return APIError::OK;
} else if (sent == -1) {
// an error occured
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
} else if (sent != len) {
// partially sent, add end to tx_buf
tx_buf_.insert(tx_buf_.end(), data + sent, data + len);
return APIError::OK;
}
// fully sent
return APIError::OK;
}
APIError APIPlaintextFrameHelper::close() {
state_ = State::CLOSED;
int err = socket_->close();
if (err == -1)
return APIError::CLOSE_FAILED;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::shutdown(int how) {
int err = socket_->shutdown(how);
if (err == -1)
return APIError::SHUTDOWN_FAILED;
if (how == SHUT_RDWR) {
state_ = State::CLOSED;
}
return APIError::OK;
}
#endif // USE_API_PLAINTEXT
} // namespace api
} // namespace esphome

View File

@ -0,0 +1,182 @@
#pragma once
#include <cstdint>
#include <vector>
#include <deque>
#include "esphome/core/defines.h"
#ifdef USE_API_NOISE
#include "noise/protocol.h"
#endif
#include "esphome/components/socket/socket.h"
#include "api_noise_context.h"
namespace esphome {
namespace api {
struct ReadPacketBuffer {
std::vector<uint8_t> container;
uint16_t type;
size_t data_offset;
size_t data_len;
};
struct PacketBuffer {
const std::vector<uint8_t> container;
uint16_t type;
uint8_t data_offset;
uint8_t data_len;
};
enum class APIError : int {
OK = 0,
WOULD_BLOCK = 1001,
BAD_HANDSHAKE_PACKET_LEN = 1002,
BAD_INDICATOR = 1003,
BAD_DATA_PACKET = 1004,
TCP_NODELAY_FAILED = 1005,
TCP_NONBLOCKING_FAILED = 1006,
CLOSE_FAILED = 1007,
SHUTDOWN_FAILED = 1008,
BAD_STATE = 1009,
BAD_ARG = 1010,
SOCKET_READ_FAILED = 1011,
SOCKET_WRITE_FAILED = 1012,
HANDSHAKESTATE_READ_FAILED = 1013,
HANDSHAKESTATE_WRITE_FAILED = 1014,
HANDSHAKESTATE_BAD_STATE = 1015,
CIPHERSTATE_DECRYPT_FAILED = 1016,
CIPHERSTATE_ENCRYPT_FAILED = 1017,
OUT_OF_MEMORY = 1018,
HANDSHAKESTATE_SETUP_FAILED = 1019,
HANDSHAKESTATE_SPLIT_FAILED = 1020,
BAD_HANDSHAKE_ERROR_BYTE = 1021,
};
const char *api_error_to_str(APIError err);
class APIFrameHelper {
public:
virtual APIError init() = 0;
virtual APIError loop() = 0;
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
virtual bool can_write_without_blocking() = 0;
virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0;
virtual std::string getpeername() = 0;
virtual APIError close() = 0;
virtual APIError shutdown(int how) = 0;
// Give this helper a name for logging
virtual void set_log_info(std::string info) = 0;
};
#ifdef USE_API_NOISE
class APINoiseFrameHelper : public APIFrameHelper {
public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
: socket_(std::move(socket)), ctx_(ctx) {}
~APINoiseFrameHelper();
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
bool can_write_without_blocking() override;
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
std::string getpeername() override { return socket_->getpeername(); }
APIError close() override;
APIError shutdown(int how) override;
// Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); }
protected:
struct ParsedFrame {
std::vector<uint8_t> msg;
};
APIError state_action_();
APIError try_read_frame_(ParsedFrame *frame);
APIError try_send_tx_buf_();
APIError write_frame_(const uint8_t *data, size_t len);
APIError write_raw_(const uint8_t *data, size_t len);
APIError init_handshake_();
APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const std::string &reason);
std::unique_ptr<socket::Socket> socket_;
std::string info_;
uint8_t rx_header_buf_[3];
size_t rx_header_buf_len_ = 0;
std::vector<uint8_t> rx_buf_;
size_t rx_buf_len_ = 0;
std::vector<uint8_t> tx_buf_;
std::vector<uint8_t> prologue_;
std::shared_ptr<APINoiseContext> ctx_;
NoiseHandshakeState *handshake_ = nullptr;
NoiseCipherState *send_cipher_ = nullptr;
NoiseCipherState *recv_cipher_ = nullptr;
NoiseProtocolId nid_;
enum class State {
INITIALIZE = 1,
CLIENT_HELLO = 2,
SERVER_HELLO = 3,
HANDSHAKE = 4,
DATA = 5,
CLOSED = 6,
FAILED = 7,
EXPLICIT_REJECT = 8,
} state_ = State::INITIALIZE;
};
#endif // USE_API_NOISE
#ifdef USE_API_PLAINTEXT
class APIPlaintextFrameHelper : public APIFrameHelper {
public:
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
~APIPlaintextFrameHelper() = default;
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
bool can_write_without_blocking() override;
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
std::string getpeername() override { return socket_->getpeername(); }
APIError close() override;
APIError shutdown(int how) override;
// Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); }
protected:
struct ParsedFrame {
std::vector<uint8_t> msg;
};
APIError try_read_frame_(ParsedFrame *frame);
APIError try_send_tx_buf_();
APIError write_raw_(const uint8_t *data, size_t len);
std::unique_ptr<socket::Socket> socket_;
std::string info_;
std::vector<uint8_t> rx_header_buf_;
bool rx_header_parsed_ = false;
uint32_t rx_header_parsed_type_ = 0;
uint32_t rx_header_parsed_len_ = 0;
std::vector<uint8_t> rx_buf_;
size_t rx_buf_len_ = 0;
std::vector<uint8_t> tx_buf_;
enum class State {
INITIALIZE = 1,
DATA = 2,
CLOSED = 3,
FAILED = 4,
} state_ = State::INITIALIZE;
};
#endif
} // namespace api
} // namespace esphome

View File

@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
#include <array>
#include "esphome/core/defines.h"
namespace esphome {
namespace api {
#ifdef USE_API_NOISE
using psk_t = std::array<uint8_t, 32>;
class APINoiseContext {
public:
void set_psk(psk_t psk) { psk_ = std::move(psk); }
const psk_t &get_psk() const { return psk_; }
protected:
psk_t psk_;
};
#endif // USE_API_NOISE
} // namespace api
} // namespace esphome

View File

@ -1817,7 +1817,7 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va
return true;
}
case 11: {
this->last_reset_type = value.as_enum<enums::SensorLastResetType>();
this->legacy_last_reset_type = value.as_enum<enums::SensorLastResetType>();
return true;
}
case 12: {
@ -1879,7 +1879,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(8, this->force_update);
buffer.encode_string(9, this->device_class);
buffer.encode_enum<enums::SensorStateClass>(10, this->state_class);
buffer.encode_enum<enums::SensorLastResetType>(11, this->last_reset_type);
buffer.encode_enum<enums::SensorLastResetType>(11, this->legacy_last_reset_type);
buffer.encode_bool(12, this->disabled_by_default);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
@ -1928,8 +1928,8 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const {
out.append(proto_enum_to_string<enums::SensorStateClass>(this->state_class));
out.append("\n");
out.append(" last_reset_type: ");
out.append(proto_enum_to_string<enums::SensorLastResetType>(this->last_reset_type));
out.append(" legacy_last_reset_type: ");
out.append(proto_enum_to_string<enums::SensorLastResetType>(this->legacy_last_reset_type));
out.append("\n");
out.append(" disabled_by_default: ");

View File

@ -510,7 +510,7 @@ class ListEntitiesSensorResponse : public ProtoMessage {
bool force_update{false};
std::string device_class{};
enums::SensorStateClass state_class{};
enums::SensorLastResetType last_reset_type{};
enums::SensorLastResetType legacy_last_reset_type{};
bool disabled_by_default{false};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP

View File

@ -1,10 +1,11 @@
#include "api_server.h"
#include "api_connection.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/core/util.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#include "esphome/core/version.h"
#include <cerrno>
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
@ -21,20 +22,45 @@ static const char *const TAG = "api";
void APIServer::setup() {
ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server...");
this->setup_controller();
this->server_ = AsyncServer(this->port_);
this->server_.setNoDelay(false);
this->server_.begin();
this->server_.onClient(
[](void *s, AsyncClient *client) {
if (client == nullptr)
return;
socket_ = socket::socket(AF_INET, SOCK_STREAM, 0);
if (socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket.");
this->mark_failed();
return;
}
int enable = 1;
int err = socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
// we can still continue
}
err = socket_->setblocking(false);
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
this->mark_failed();
return;
}
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = ESPHOME_INADDR_ANY;
server.sin_port = htons(this->port_);
err = socket_->bind((struct sockaddr *) &server, sizeof(server));
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
this->mark_failed();
return;
}
err = socket_->listen(4);
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
this->mark_failed();
return;
}
// can't print here because in lwIP thread
// ESP_LOGD(TAG, "New client connected from %s", client->remoteIP().toString().c_str());
auto *a_this = (APIServer *) s;
a_this->clients_.push_back(new APIConnection(client, a_this));
},
this);
#ifdef USE_LOGGER
if (logger::global_logger != nullptr) {
logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) {
@ -59,12 +85,26 @@ void APIServer::setup() {
#endif
}
void APIServer::loop() {
// Accept new clients
while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
auto sock = socket_->accept((struct sockaddr *) &source_addr, &addr_len);
if (!sock)
break;
ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str());
auto *conn = new APIConnection(std::move(sock), this);
clients_.push_back(conn);
conn->start();
}
// Partition clients into remove and active
auto new_end =
std::partition(this->clients_.begin(), this->clients_.end(), [](APIConnection *conn) { return !conn->remove_; });
// print disconnection messages
for (auto it = new_end; it != this->clients_.end(); ++it) {
ESP_LOGD(TAG, "Disconnecting %s", (*it)->client_info_.c_str());
ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str());
}
// only then delete the pointers, otherwise log routine
// would access freed memory

View File

@ -4,19 +4,14 @@
#include "esphome/core/controller.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include "esphome/components/socket/socket.h"
#include "api_pb2.h"
#include "api_pb2_service.h"
#include "util.h"
#include "list_entities.h"
#include "subscribe_state.h"
#include "user_services.h"
#ifdef ARDUINO_ARCH_ESP32
#include <AsyncTCP.h>
#endif
#ifdef ARDUINO_ARCH_ESP8266
#include <ESPAsyncTCP.h>
#endif
#include "api_noise_context.h"
namespace esphome {
namespace api {
@ -35,6 +30,12 @@ class APIServer : public Component, public Controller {
void set_port(uint16_t port);
void set_password(const std::string &password);
void set_reboot_timeout(uint32_t reboot_timeout);
#ifdef USE_API_NOISE
void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(std::move(psk)); }
std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
#endif // USE_API_NOISE
void handle_disconnect(APIConnection *conn);
#ifdef USE_BINARY_SENSOR
void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override;
@ -86,7 +87,7 @@ class APIServer : public Component, public Controller {
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
protected:
AsyncServer server_{0};
std::unique_ptr<socket::Socket> socket_ = nullptr;
uint16_t port_{6053};
uint32_t reboot_timeout_{300000};
uint32_t last_connected_{0};
@ -94,6 +95,10 @@ class APIServer : public Component, public Controller {
std::string password_;
std::vector<HomeAssistantStateSubscription> state_subs_;
std::vector<UserServiceDescriptor *> user_services_;
#ifdef USE_API_NOISE
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
#endif // USE_API_NOISE
};
extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@ -0,0 +1,73 @@
import asyncio
import logging
from datetime import datetime
from typing import Optional
from aioesphomeapi import APIClient, ReconnectLogic, APIConnectionError, LogLevel
import zeroconf
from esphome.const import CONF_KEY, CONF_PORT, CONF_PASSWORD, __version__
from esphome.util import safe_print
from . import CONF_ENCRYPTION
_LOGGER = logging.getLogger(__name__)
async def async_run_logs(config, address):
conf = config["api"]
port: int = int(conf[CONF_PORT])
password: str = conf[CONF_PASSWORD]
noise_psk: Optional[str] = None
if CONF_ENCRYPTION in conf:
noise_psk = conf[CONF_ENCRYPTION][CONF_KEY]
_LOGGER.info("Starting log output from %s using esphome API", address)
zc = zeroconf.Zeroconf()
cli = APIClient(
asyncio.get_event_loop(),
address,
port,
password,
client_info=f"ESPHome Logs {__version__}",
noise_psk=noise_psk,
)
first_connect = True
def on_log(msg):
time_ = datetime.now().time().strftime("[%H:%M:%S]")
text = msg.message.decode("utf8", "backslashreplace")
safe_print(time_ + text)
async def on_connect():
nonlocal first_connect
try:
await cli.subscribe_logs(
on_log,
log_level=LogLevel.LOG_LEVEL_VERY_VERBOSE,
dump_config=first_connect,
)
first_connect = False
except APIConnectionError:
cli.disconnect()
async def on_disconnect():
_LOGGER.warning("Disconnected from API")
zc = zeroconf.Zeroconf()
reconnect = ReconnectLogic(
client=cli,
on_connect=on_connect,
on_disconnect=on_disconnect,
zeroconf_instance=zc,
)
await reconnect.start()
try:
while True:
await asyncio.sleep(60)
except KeyboardInterrupt:
await reconnect.stop()
zc.close()
def run_logs(config, address):
asyncio.run(async_run_logs(config, address))

View File

@ -19,8 +19,8 @@ from esphome.const import (
DEVICE_CLASS_VOLTAGE,
ICON_LIGHTBULB,
ICON_CURRENT_AC,
LAST_RESET_TYPE_AUTO,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_HERTZ,
UNIT_VOLT,
UNIT_AMPERE,
@ -94,15 +94,13 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t,
cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t,

View File

@ -55,7 +55,10 @@ void BinaryFan::loop() {
ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable));
}
}
float BinaryFan::get_setup_priority() const { return setup_priority::DATA; }
// We need a higher priority than the FanState component to make sure that the traits are set
// when that component sets itself up.
float BinaryFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; }
} // namespace binary
} // namespace esphome

View File

@ -48,6 +48,7 @@ from esphome.const import (
DEVICE_CLASS_SAFETY,
DEVICE_CLASS_SMOKE,
DEVICE_CLASS_SOUND,
DEVICE_CLASS_UPDATE,
DEVICE_CLASS_VIBRATION,
DEVICE_CLASS_WINDOW,
)
@ -79,6 +80,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_SAFETY,
DEVICE_CLASS_SMOKE,
DEVICE_CLASS_SOUND,
DEVICE_CLASS_UPDATE,
DEVICE_CLASS_VIBRATION,
DEVICE_CLASS_WINDOW,
]

View File

@ -1,7 +1,14 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor, esp32_ble_tracker
from esphome.const import CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_ID
from esphome.const import (
CONF_MAC_ADDRESS,
CONF_SERVICE_UUID,
CONF_IBEACON_MAJOR,
CONF_IBEACON_MINOR,
CONF_IBEACON_UUID,
CONF_ID,
)
DEPENDENCIES = ["esp32_ble_tracker"]
@ -13,17 +20,30 @@ BLEPresenceDevice = ble_presence_ns.class_(
esp32_ble_tracker.ESPBTDeviceListener,
)
def _validate(config):
if CONF_IBEACON_MAJOR in config and CONF_IBEACON_UUID not in config:
raise cv.Invalid("iBeacon major identifier requires iBeacon UUID")
if CONF_IBEACON_MINOR in config and CONF_IBEACON_UUID not in config:
raise cv.Invalid("iBeacon minor identifier requires iBeacon UUID")
return config
CONFIG_SCHEMA = cv.All(
binary_sensor.BINARY_SENSOR_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(BLEPresenceDevice),
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t,
cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t,
cv.Optional(CONF_IBEACON_UUID): cv.uuid,
}
)
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID),
cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_IBEACON_UUID),
_validate,
)
@ -50,5 +70,15 @@ async def to_code(config):
)
)
elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format):
uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID])
uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID])
cg.add(var.set_service_uuid128(uuid128))
if CONF_IBEACON_UUID in config:
ibeacon_uuid = esp32_ble_tracker.as_hex_array(str(config[CONF_IBEACON_UUID]))
cg.add(var.set_ibeacon_uuid(ibeacon_uuid))
if CONF_IBEACON_MAJOR in config:
cg.add(var.set_ibeacon_major(config[CONF_IBEACON_MAJOR]))
if CONF_IBEACON_MINOR in config:
cg.add(var.set_ibeacon_minor(config[CONF_IBEACON_MINOR]))

View File

@ -14,41 +14,78 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
public Component {
public:
void set_address(uint64_t address) {
this->by_address_ = true;
this->match_by_ = MATCH_BY_MAC_ADDRESS;
this->address_ = address;
}
void set_service_uuid16(uint16_t uuid) {
this->by_address_ = false;
this->match_by_ = MATCH_BY_SERVICE_UUID;
this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid);
}
void set_service_uuid32(uint32_t uuid) {
this->by_address_ = false;
this->match_by_ = MATCH_BY_SERVICE_UUID;
this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint32(uuid);
}
void set_service_uuid128(uint8_t *uuid) {
this->by_address_ = false;
this->match_by_ = MATCH_BY_SERVICE_UUID;
this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid);
}
void set_ibeacon_uuid(uint8_t *uuid) {
this->match_by_ = MATCH_BY_IBEACON_UUID;
this->ibeacon_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid);
}
void set_ibeacon_major(uint16_t major) {
this->check_ibeacon_major_ = true;
this->ibeacon_major_ = major;
}
void set_ibeacon_minor(uint16_t minor) {
this->check_ibeacon_minor_ = true;
this->ibeacon_minor_ = minor;
}
void on_scan_end() override {
if (!this->found_)
this->publish_state(false);
this->found_ = false;
}
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override {
if (this->by_address_) {
if (device.address_uint64() == this->address_) {
this->publish_state(true);
this->found_ = true;
return true;
}
} else {
for (auto uuid : device.get_service_uuids()) {
if (this->uuid_ == uuid) {
this->publish_state(device.get_rssi());
switch (this->match_by_) {
case MATCH_BY_MAC_ADDRESS:
if (device.address_uint64() == this->address_) {
this->publish_state(true);
this->found_ = true;
return true;
}
}
break;
case MATCH_BY_SERVICE_UUID:
for (auto uuid : device.get_service_uuids()) {
if (this->uuid_ == uuid) {
this->publish_state(device.get_rssi());
this->found_ = true;
return true;
}
}
break;
case MATCH_BY_IBEACON_UUID:
if (!device.get_ibeacon().has_value()) {
return false;
}
auto ibeacon = device.get_ibeacon().value();
if (this->ibeacon_uuid_ != ibeacon.get_uuid()) {
return false;
}
if (this->check_ibeacon_major_ && this->ibeacon_major_ != ibeacon.get_major()) {
return false;
}
if (this->check_ibeacon_minor_ && this->ibeacon_minor_ != ibeacon.get_minor()) {
return false;
}
this->publish_state(device.get_rssi());
this->found_ = true;
return true;
}
return false;
}
@ -56,10 +93,20 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
enum MATCH_TYPE { MATCH_BY_MAC_ADDRESS, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID };
MATCH_TYPE match_by_;
bool found_{false};
bool by_address_{false};
uint64_t address_;
esp32_ble_tracker::ESPBTUUID uuid_;
esp32_ble_tracker::ESPBTUUID ibeacon_uuid_;
uint16_t ibeacon_major_;
bool check_ibeacon_major_;
uint16_t ibeacon_minor_;
bool check_ibeacon_minor_;
};
} // namespace ble_presence

View File

@ -60,5 +60,5 @@ async def to_code(config):
)
)
elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format):
uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID])
uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID])
cg.add(var.set_service_uuid128(uuid128))

View File

@ -10,7 +10,7 @@ static const char *const TAG = "bme680_bsec.sensor";
static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"};
BME680BSECComponent *BME680BSECComponent::instance;
BME680BSECComponent *BME680BSECComponent::instance; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void BME680BSECComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up BME680 via BSEC...");
@ -359,7 +359,7 @@ void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float va
sensor->publish_state(value);
}
void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value) {
void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) {
if (!sensor || (sensor->has_state() && sensor->state == value)) {
return;
}

View File

@ -70,7 +70,7 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
int64_t get_time_ns_();
void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false);
void publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value);
void publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value);
void load_state_();
void save_state_(uint8_t accuracy);

View File

@ -16,7 +16,7 @@ static const char *const TAG = "ccs811";
return; \
}
#define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICAITON_FAILED)
#define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICATION_FAILED)
void CCS811Component::setup() {
// page 9 programming guide - hwid is always 0x81
@ -38,12 +38,14 @@ void CCS811Component::setup() {
// set MEAS_MODE (page 5)
uint8_t meas_mode = 0;
uint32_t interval = this->get_update_interval();
if (interval <= 1000)
meas_mode = 1 << 4;
else if (interval <= 10000)
meas_mode = 2 << 4;
if (interval >= 60 * 1000)
meas_mode = 3 << 4; // sensor takes a reading every 60 seconds
else if (interval >= 10 * 1000)
meas_mode = 2 << 4; // sensor takes a reading every 10 seconds
else if (interval >= 1 * 1000)
meas_mode = 1 << 4; // sensor takes a reading every second
else
meas_mode = 3 << 4;
meas_mode = 4 << 4; // sensor takes a reading every 250ms
CHECKED_IO(this->write_byte(0x01, meas_mode))
@ -51,6 +53,36 @@ void CCS811Component::setup() {
// baseline available, write to sensor
this->write_bytes(0x11, decode_uint16(*this->baseline_));
}
auto hardware_version_data = this->read_bytes<1>(0x21);
auto bootloader_version_data = this->read_bytes<2>(0x23);
auto application_version_data = this->read_bytes<2>(0x24);
uint8_t hardware_version = 0;
uint16_t bootloader_version = 0;
uint16_t application_version = 0;
if (hardware_version_data.has_value()) {
hardware_version = (*hardware_version_data)[0];
}
if (bootloader_version_data.has_value()) {
bootloader_version = encode_uint16((*bootloader_version_data)[0], (*bootloader_version_data)[1]);
}
if (application_version_data.has_value()) {
application_version = encode_uint16((*application_version_data)[0], (*application_version_data)[1]);
}
ESP_LOGD(TAG, "hardware_version=0x%x bootloader_version=0x%x application_version=0x%x\n", hardware_version,
bootloader_version, application_version);
if (this->version_ != nullptr) {
char version[20]; // "15.15.15 (0xffff)" is 17 chars, plus NUL, plus wiggle room
sprintf(version, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15), (application_version >> 8 & 15),
(application_version >> 4 & 15), application_version);
ESP_LOGD(TAG, "publishing version state: %s", version);
this->version_->publish_state(version);
}
}
void CCS811Component::update() {
if (!this->status_has_data_())
@ -117,6 +149,7 @@ void CCS811Component::dump_config() {
LOG_UPDATE_INTERVAL(this)
LOG_SENSOR(" ", "CO2 Sensor", this->co2_)
LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_)
LOG_TEXT_SENSOR(" ", "Firmware Version Sensor", this->version_)
if (this->baseline_) {
ESP_LOGCONFIG(TAG, " Baseline: %04X", *this->baseline_);
} else {
@ -124,7 +157,7 @@ void CCS811Component::dump_config() {
}
if (this->is_failed()) {
switch (this->error_code_) {
case COMMUNICAITON_FAILED:
case COMMUNICATION_FAILED:
ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
break;
case INVALID_ID:

View File

@ -3,6 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
@ -12,6 +13,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice {
public:
void set_co2(sensor::Sensor *co2) { co2_ = co2; }
void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; }
void set_version(text_sensor::TextSensor *version) { version_ = version; }
void set_baseline(uint16_t baseline) { baseline_ = baseline; }
void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
@ -34,7 +36,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice {
enum ErrorCode {
UNKNOWN,
COMMUNICAITON_FAILED,
COMMUNICATION_FAILED,
INVALID_ID,
SENSOR_REPORTED_ERROR,
APP_INVALID,
@ -43,6 +45,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice {
sensor::Sensor *co2_{nullptr};
sensor::Sensor *tvoc_{nullptr};
text_sensor::TextSensor *version_{nullptr};
optional<uint16_t> baseline_{};
/// Input sensor for humidity reading.
sensor::Sensor *humidity_{nullptr};

View File

@ -1,9 +1,13 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.components import i2c, sensor, text_sensor
from esphome.const import (
CONF_ICON,
CONF_ID,
ICON_RADIATOR,
ICON_RESTART,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
STATE_CLASS_MEASUREMENT,
UNIT_PARTS_PER_MILLION,
UNIT_PARTS_PER_BILLION,
@ -12,9 +16,12 @@ from esphome.const import (
CONF_TEMPERATURE,
CONF_TVOC,
CONF_HUMIDITY,
CONF_VERSION,
ICON_MOLECULE_CO2,
)
AUTO_LOAD = ["text_sensor"]
CODEOWNERS = ["@habbie"]
DEPENDENCIES = ["i2c"]
ccs811_ns = cg.esphome_ns.namespace("ccs811")
@ -30,14 +37,22 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_VERSION): text_sensor.TEXT_SENSOR_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
cv.Optional(CONF_ICON, default=ICON_RESTART): cv.icon,
}
),
cv.Optional(CONF_BASELINE): cv.hex_uint16_t,
cv.Optional(CONF_TEMPERATURE): cv.use_id(sensor.Sensor),
cv.Optional(CONF_HUMIDITY): cv.use_id(sensor.Sensor),
@ -58,6 +73,11 @@ async def to_code(config):
sens = await sensor.new_sensor(config[CONF_TVOC])
cg.add(var.set_tvoc(sens))
if CONF_VERSION in config:
sens = cg.new_Pvariable(config[CONF_VERSION][CONF_ID])
await text_sensor.register_text_sensor(sens, config[CONF_VERSION])
cg.add(var.set_version(sens))
if CONF_BASELINE in config:
cg.add(var.set_baseline(config[CONF_BASELINE]))

View File

@ -63,6 +63,7 @@ validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True)
ClimatePreset = climate_ns.enum("ClimatePreset")
CLIMATE_PRESETS = {
"NONE": ClimatePreset.CLIMATE_PRESET_NONE,
"ECO": ClimatePreset.CLIMATE_PRESET_ECO,
"AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,

View File

@ -494,5 +494,74 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
climate->publish_state();
}
template<typename T1, typename T2> bool set_alternative(optional<T1> &dst, optional<T2> &alt, const T1 &src) {
bool is_changed = alt.has_value();
alt.reset();
if (is_changed || dst != src) {
dst = src;
is_changed = true;
}
return is_changed;
}
bool Climate::set_fan_mode_(ClimateFanMode mode) {
return set_alternative(this->fan_mode, this->custom_fan_mode, mode);
}
bool Climate::set_custom_fan_mode_(const std::string &mode) {
return set_alternative(this->custom_fan_mode, this->fan_mode, mode);
}
bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); }
bool Climate::set_custom_preset_(const std::string &preset) {
return set_alternative(this->custom_preset, this->preset, preset);
}
void Climate::dump_traits_(const char *tag) {
auto traits = this->get_traits();
ESP_LOGCONFIG(tag, "ClimateTraits:");
ESP_LOGCONFIG(tag, " [x] Visual settings:");
ESP_LOGCONFIG(tag, " - Min: %.1f", traits.get_visual_min_temperature());
ESP_LOGCONFIG(tag, " - Max: %.1f", traits.get_visual_max_temperature());
ESP_LOGCONFIG(tag, " - Step: %.1f", traits.get_visual_temperature_step());
if (traits.get_supports_current_temperature())
ESP_LOGCONFIG(tag, " [x] Supports current temperature");
if (traits.get_supports_two_point_target_temperature())
ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature");
if (traits.get_supports_action())
ESP_LOGCONFIG(tag, " [x] Supports action");
if (!traits.get_supported_modes().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported modes:");
for (ClimateMode m : traits.get_supported_modes())
ESP_LOGCONFIG(tag, " - %s", climate_mode_to_string(m));
}
if (!traits.get_supported_fan_modes().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported fan modes:");
for (ClimateFanMode m : traits.get_supported_fan_modes())
ESP_LOGCONFIG(tag, " - %s", climate_fan_mode_to_string(m));
}
if (!traits.get_supported_custom_fan_modes().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported custom fan modes:");
for (const std::string &s : traits.get_supported_custom_fan_modes())
ESP_LOGCONFIG(tag, " - %s", s.c_str());
}
if (!traits.get_supported_presets().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported presets:");
for (ClimatePreset p : traits.get_supported_presets())
ESP_LOGCONFIG(tag, " - %s", climate_preset_to_string(p));
}
if (!traits.get_supported_custom_presets().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported custom presets:");
for (const std::string &s : traits.get_supported_custom_presets())
ESP_LOGCONFIG(tag, " - %s", s.c_str());
}
if (!traits.get_supported_swing_modes().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported swing modes:");
for (ClimateSwingMode m : traits.get_supported_swing_modes())
ESP_LOGCONFIG(tag, " - %s", climate_swing_mode_to_string(m));
}
}
} // namespace climate
} // namespace esphome

View File

@ -245,6 +245,18 @@ class Climate : public Nameable {
protected:
friend ClimateCall;
/// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed.
bool set_fan_mode_(ClimateFanMode mode);
/// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed.
bool set_custom_fan_mode_(const std::string &mode);
/// Set preset. Reset custom preset. Return true if preset has been changed.
bool set_preset_(ClimatePreset preset);
/// Set custom preset. Reset primary preset. Return true if preset has been changed.
bool set_custom_preset_(const std::string &preset);
/** Get the default traits of this climate device.
*
* Traits are static data that encode the capabilities and static data for a climate device such as supported
@ -270,6 +282,7 @@ class Climate : public Nameable {
void save_state_();
uint32_t hash_base() override;
void dump_traits_(const char *tag);
CallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;

View File

@ -72,6 +72,7 @@ class ClimateTraits {
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { supported_fan_modes_ = std::move(modes); }
void add_supported_fan_mode(ClimateFanMode mode) { supported_fan_modes_.insert(mode); }
void add_supported_custom_fan_mode(const std::string &mode) { supported_custom_fan_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
@ -104,6 +105,7 @@ class ClimateTraits {
void set_supported_presets(std::set<ClimatePreset> presets) { supported_presets_ = std::move(presets); }
void add_supported_preset(ClimatePreset preset) { supported_presets_.insert(preset); }
void add_supported_custom_preset(const std::string &preset) { supported_custom_presets_.insert(preset); }
bool supports_preset(ClimatePreset preset) const { return supported_presets_.count(preset); }
bool get_supports_presets() const { return !supported_presets_.empty(); }
const std::set<climate::ClimatePreset> &get_supported_presets() const { return supported_presets_; }

View File

@ -125,16 +125,19 @@ class Cover : public Nameable {
*
* This is a legacy method and may be removed later, please use `.make_call()` instead.
*/
ESPDEPRECATED("open() is deprecated, use make_call().set_command_open() instead.", "2021.9")
void open();
/** Close the cover.
*
* This is a legacy method and may be removed later, please use `.make_call()` instead.
*/
ESPDEPRECATED("close() is deprecated, use make_call().set_command_close() instead.", "2021.9")
void close();
/** Stop the cover.
*
* This is a legacy method and may be removed later, please use `.make_call()` instead.
*/
ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop() instead.", "2021.9")
void stop();
void add_on_state_callback(std::function<void()> &&f);

View File

@ -19,7 +19,6 @@ from esphome.const import (
CONF_ICON,
CONF_ID,
CONF_INVERTED,
CONF_LAST_RESET_TYPE,
CONF_MAX_VALUE,
CONF_MIN_VALUE,
CONF_NAME,
@ -40,8 +39,8 @@ from esphome.const import (
ICON_BLUR,
ICON_EMPTY,
ICON_THERMOMETER,
LAST_RESET_TYPE_AUTO,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_CELSIUS,
UNIT_EMPTY,
UNIT_PERCENT,
@ -336,8 +335,7 @@ CONFIG_SCHEMA = cv.Schema(
CONF_UNIT_OF_MEASUREMENT: UNIT_WATT_HOURS,
CONF_ACCURACY_DECIMALS: 0,
CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
CONF_LAST_RESET_TYPE: LAST_RESET_TYPE_AUTO,
CONF_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING,
},
],
): [

View File

@ -11,8 +11,8 @@ class DemoSensor : public sensor::Sensor, public PollingComponent {
public:
void update() override {
float val = random_float();
bool is_auto = this->last_reset_type == sensor::LAST_RESET_TYPE_AUTO;
if (is_auto) {
bool increasing = this->state_class == sensor::STATE_CLASS_TOTAL_INCREASING;
if (increasing) {
float base = isnan(this->state) ? 0.0f : this->state;
this->publish_state(base + val * 10);
} else {

View File

@ -9,13 +9,17 @@ from esphome.const import (
DEVICE_CLASS_POWER,
DEVICE_CLASS_VOLTAGE,
ICON_EMPTY,
LAST_RESET_TYPE_NEVER,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_NONE,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_CUBIC_METER,
UNIT_EMPTY,
UNIT_KILOWATT,
UNIT_KILOWATT_HOURS,
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
UNIT_KILOVOLT_AMPS_REACTIVE,
UNIT_VOLT,
UNIT_WATT,
)
from . import Dsmr, CONF_DSMR_ID
@ -26,70 +30,80 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr),
cv.Optional("energy_delivered_lux"): sensor.sensor_schema(
"kWh",
UNIT_KILOWATT_HOURS,
ICON_EMPTY,
3,
DEVICE_CLASS_ENERGY,
STATE_CLASS_MEASUREMENT,
LAST_RESET_TYPE_NEVER,
STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_delivered_tariff1"): sensor.sensor_schema(
"kWh",
UNIT_KILOWATT_HOURS,
ICON_EMPTY,
3,
DEVICE_CLASS_ENERGY,
STATE_CLASS_MEASUREMENT,
LAST_RESET_TYPE_NEVER,
STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_delivered_tariff2"): sensor.sensor_schema(
"kWh",
UNIT_KILOWATT_HOURS,
ICON_EMPTY,
3,
DEVICE_CLASS_ENERGY,
STATE_CLASS_MEASUREMENT,
LAST_RESET_TYPE_NEVER,
STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_returned_lux"): sensor.sensor_schema(
"kWh",
UNIT_KILOWATT_HOURS,
ICON_EMPTY,
3,
DEVICE_CLASS_ENERGY,
STATE_CLASS_MEASUREMENT,
LAST_RESET_TYPE_NEVER,
STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_returned_tariff1"): sensor.sensor_schema(
"kWh",
UNIT_KILOWATT_HOURS,
ICON_EMPTY,
3,
DEVICE_CLASS_ENERGY,
STATE_CLASS_MEASUREMENT,
LAST_RESET_TYPE_NEVER,
STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_returned_tariff2"): sensor.sensor_schema(
"kWh",
UNIT_KILOWATT_HOURS,
ICON_EMPTY,
3,
DEVICE_CLASS_ENERGY,
STATE_CLASS_MEASUREMENT,
LAST_RESET_TYPE_NEVER,
STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("total_imported_energy"): sensor.sensor_schema(
"kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
ICON_EMPTY,
3,
DEVICE_CLASS_ENERGY,
STATE_CLASS_NONE,
),
cv.Optional("total_exported_energy"): sensor.sensor_schema(
"kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
ICON_EMPTY,
3,
DEVICE_CLASS_ENERGY,
STATE_CLASS_NONE,
),
cv.Optional("power_delivered"): sensor.sensor_schema(
UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
),
cv.Optional("power_returned"): sensor.sensor_schema(
UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
),
cv.Optional("reactive_power_delivered"): sensor.sensor_schema(
"kvar", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE
UNIT_KILOVOLT_AMPS_REACTIVE,
ICON_EMPTY,
3,
DEVICE_CLASS_POWER,
STATE_CLASS_MEASUREMENT,
),
cv.Optional("reactive_power_returned"): sensor.sensor_schema(
"kvar", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_MEASUREMENT
UNIT_KILOVOLT_AMPS_REACTIVE,
ICON_EMPTY,
3,
DEVICE_CLASS_POWER,
STATE_CLASS_MEASUREMENT,
),
cv.Optional("electricity_threshold"): sensor.sensor_schema(
UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
@ -107,13 +121,13 @@ CONFIG_SCHEMA = cv.Schema(
UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
),
cv.Optional("electricity_sags_l2"): sensor.sensor_schema(
UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
),
cv.Optional("electricity_sags_l3"): sensor.sensor_schema(
UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
),
cv.Optional("electricity_swells_l1"): sensor.sensor_schema(
UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
),
cv.Optional("electricity_swells_l2"): sensor.sensor_schema(
UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
@ -131,40 +145,64 @@ CONFIG_SCHEMA = cv.Schema(
UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT
),
cv.Optional("power_delivered_l1"): sensor.sensor_schema(
UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
),
cv.Optional("power_delivered_l2"): sensor.sensor_schema(
UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
),
cv.Optional("power_delivered_l3"): sensor.sensor_schema(
UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
),
cv.Optional("power_returned_l1"): sensor.sensor_schema(
UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
),
cv.Optional("power_returned_l2"): sensor.sensor_schema(
UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
),
cv.Optional("power_returned_l3"): sensor.sensor_schema(
UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
),
cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema(
UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
UNIT_KILOVOLT_AMPS_REACTIVE,
ICON_EMPTY,
3,
DEVICE_CLASS_POWER,
STATE_CLASS_MEASUREMENT,
),
cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema(
UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
UNIT_KILOVOLT_AMPS_REACTIVE,
ICON_EMPTY,
3,
DEVICE_CLASS_POWER,
STATE_CLASS_MEASUREMENT,
),
cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema(
UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
UNIT_KILOVOLT_AMPS_REACTIVE,
ICON_EMPTY,
3,
DEVICE_CLASS_POWER,
STATE_CLASS_MEASUREMENT,
),
cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema(
UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
UNIT_KILOVOLT_AMPS_REACTIVE,
ICON_EMPTY,
3,
DEVICE_CLASS_POWER,
STATE_CLASS_MEASUREMENT,
),
cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema(
UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
UNIT_KILOVOLT_AMPS_REACTIVE,
ICON_EMPTY,
3,
DEVICE_CLASS_POWER,
STATE_CLASS_MEASUREMENT,
),
cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema(
UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
UNIT_KILOVOLT_AMPS_REACTIVE,
ICON_EMPTY,
3,
DEVICE_CLASS_POWER,
STATE_CLASS_MEASUREMENT,
),
cv.Optional("voltage_l1"): sensor.sensor_schema(
UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE
@ -176,20 +214,18 @@ CONFIG_SCHEMA = cv.Schema(
UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE
),
cv.Optional("gas_delivered"): sensor.sensor_schema(
"",
UNIT_CUBIC_METER,
ICON_EMPTY,
3,
DEVICE_CLASS_GAS,
STATE_CLASS_MEASUREMENT,
LAST_RESET_TYPE_NEVER,
STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("gas_delivered_be"): sensor.sensor_schema(
"",
UNIT_CUBIC_METER,
ICON_EMPTY,
3,
DEVICE_CLASS_GAS,
STATE_CLASS_MEASUREMENT,
LAST_RESET_TYPE_NEVER,
STATE_CLASS_TOTAL_INCREASING,
),
}
).extend(cv.COMPONENT_SCHEMA)

View File

@ -84,6 +84,7 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
break;
}
it->schedule_show();
return true;
}

View File

@ -82,11 +82,9 @@ bool BLEServer::create_device_characteristics_() {
this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ);
model->set_value(this->model_.value());
} else {
#ifdef ARDUINO_BOARD
BLECharacteristic *model =
this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ);
model->set_value(ARDUINO_BOARD);
#endif
model->set_value(ESPHOME_BOARD);
}
BLECharacteristic *version =

View File

@ -108,6 +108,16 @@ def as_hex(value):
def as_hex_array(value):
value = value.replace("-", "")
cpp_array = [
f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)]
]
return cg.RawExpression(
"(uint8_t*)(const uint8_t[16]){{{}}}".format(",".join(cpp_array))
)
def as_reversed_hex_array(value):
value = value.replace("-", "")
cpp_array = [
f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)]
@ -193,7 +203,7 @@ async def to_code(config):
elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid32_format):
cg.add(trigger.set_service_uuid32(as_hex(conf[CONF_SERVICE_UUID])))
elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid128_format):
uuid128 = as_hex_array(conf[CONF_SERVICE_UUID])
uuid128 = as_reversed_hex_array(conf[CONF_SERVICE_UUID])
cg.add(trigger.set_service_uuid128(uuid128))
if CONF_MAC_ADDRESS in conf:
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
@ -205,7 +215,7 @@ async def to_code(config):
elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid32_format):
cg.add(trigger.set_manufacturer_uuid32(as_hex(conf[CONF_MANUFACTURER_ID])))
elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid128_format):
uuid128 = as_hex_array(conf[CONF_MANUFACTURER_ID])
uuid128 = as_reversed_hex_array(conf[CONF_MANUFACTURER_ID])
cg.add(trigger.set_manufacturer_uuid128(uuid128))
if CONF_MAC_ADDRESS in conf:
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))

View File

@ -434,6 +434,14 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e
}
for (auto &data : this->manufacturer_datas_) {
ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode(data.data).c_str());
if (this->get_ibeacon().has_value()) {
auto ibeacon = this->get_ibeacon().value();
ESP_LOGVV(TAG, " iBeacon data:");
ESP_LOGVV(TAG, " UUID: %s", ibeacon.get_uuid().to_string().c_str());
ESP_LOGVV(TAG, " Major: %u", ibeacon.get_major());
ESP_LOGVV(TAG, " Minor: %u", ibeacon.get_minor());
ESP_LOGVV(TAG, " TXPower: %d", ibeacon.get_signal_power());
}
}
for (auto &data : this->service_datas_) {
ESP_LOGVV(TAG, " Service data:");

View File

@ -1,9 +1,10 @@
#include "esp8266_pwm.h"
#include "esphome/core/macros.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef ARDUINO_ESP8266_RELEASE_2_3_0
#error ESP8266 PWM requires at least arduino_core_version 2.4.0
#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0)
#error ESP8266 PWM requires at least arduino_version 2.4.0
#endif
#include <core_esp8266_waveform.h>

View File

@ -1,13 +1,12 @@
import re
import logging
from pathlib import Path
import subprocess
import hashlib
import datetime
import esphome.config_validation as cv
from esphome.const import (
CONF_COMPONENTS,
CONF_REF,
CONF_REFRESH,
CONF_SOURCE,
CONF_URL,
CONF_TYPE,
@ -15,7 +14,7 @@ from esphome.const import (
CONF_PATH,
)
from esphome.core import CORE
from esphome import loader
from esphome import git, loader
_LOGGER = logging.getLogger(__name__)
@ -23,19 +22,11 @@ DOMAIN = CONF_EXTERNAL_COMPONENTS
TYPE_GIT = "git"
TYPE_LOCAL = "local"
CONF_REFRESH = "refresh"
CONF_REF = "ref"
def validate_git_ref(value):
if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None:
raise cv.Invalid("Not a valid git ref")
return value
GIT_SCHEMA = {
cv.Required(CONF_URL): cv.url,
cv.Optional(CONF_REF): validate_git_ref,
cv.Optional(CONF_REF): cv.git_ref,
}
LOCAL_SCHEMA = {
cv.Required(CONF_PATH): cv.directory,
@ -68,14 +59,6 @@ def validate_source_shorthand(value):
return SOURCE_SCHEMA(conf)
def validate_refresh(value: str):
if value.lower() == "always":
return validate_refresh("0s")
if value.lower() == "never":
return validate_refresh("1000y")
return cv.positive_time_period_seconds(value)
SOURCE_SCHEMA = cv.Any(
validate_source_shorthand,
cv.typed_schema(
@ -90,7 +73,7 @@ SOURCE_SCHEMA = cv.Any(
CONFIG_SCHEMA = cv.ensure_list(
{
cv.Required(CONF_SOURCE): SOURCE_SCHEMA,
cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, validate_refresh),
cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, cv.source_refresh),
cv.Optional(CONF_COMPONENTS, default="all"): cv.Any(
"all", cv.ensure_list(cv.string)
),
@ -102,65 +85,13 @@ async def to_code(config):
pass
def _compute_destination_path(key: str) -> Path:
base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN
h = hashlib.new("sha256")
h.update(key.encode())
return base_dir / h.hexdigest()[:8]
def _run_git_command(cmd, cwd=None):
try:
ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False)
except FileNotFoundError as err:
raise cv.Invalid(
"git is not installed but required for external_components.\n"
"Please see https://git-scm.com/book/en/v2/Getting-Started-Installing-Git for installing git"
) from err
if ret.returncode != 0 and ret.stderr:
err_str = ret.stderr.decode("utf-8")
lines = [x.strip() for x in err_str.splitlines()]
if lines[-1].startswith("fatal:"):
raise cv.Invalid(lines[-1][len("fatal: ") :])
raise cv.Invalid(err_str)
def _process_git_config(config: dict, refresh) -> str:
key = f"{config[CONF_URL]}@{config.get(CONF_REF)}"
repo_dir = _compute_destination_path(key)
if not repo_dir.is_dir():
_LOGGER.info("Cloning %s", key)
_LOGGER.debug("Location: %s", repo_dir)
cmd = ["git", "clone", "--depth=1"]
if CONF_REF in config:
cmd += ["--branch", config[CONF_REF]]
cmd += ["--", config[CONF_URL], str(repo_dir)]
_run_git_command(cmd)
else:
# Check refresh needed
file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD")
# On first clone, FETCH_HEAD does not exists
if not file_timestamp.exists():
file_timestamp = Path(repo_dir / ".git" / "HEAD")
age = datetime.datetime.now() - datetime.datetime.fromtimestamp(
file_timestamp.stat().st_mtime
)
if age.total_seconds() > refresh.total_seconds:
_LOGGER.info("Updating %s", key)
_LOGGER.debug("Location: %s", repo_dir)
# Stash local changes (if any)
_run_git_command(
["git", "stash", "push", "--include-untracked"], str(repo_dir)
)
# Fetch remote ref
cmd = ["git", "fetch", "--", "origin"]
if CONF_REF in config:
cmd.append(config[CONF_REF])
_run_git_command(cmd, str(repo_dir))
# Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch)
_run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir))
repo_dir = git.clone_or_update(
url=config[CONF_URL],
ref=config.get(CONF_REF),
refresh=refresh,
domain=DOMAIN,
)
if (repo_dir / "esphome" / "components").is_dir():
components_dir = repo_dir / "esphome" / "components"

View File

@ -15,9 +15,11 @@ from esphome.const import (
CONF_SPEED_COMMAND_TOPIC,
CONF_SPEED_STATE_TOPIC,
CONF_NAME,
CONF_ON_SPEED_SET,
CONF_ON_TURN_OFF,
CONF_ON_TURN_ON,
CONF_TRIGGER_ID,
CONF_DIRECTION,
)
from esphome.core import CORE, coroutine_with_priority
@ -27,6 +29,12 @@ fan_ns = cg.esphome_ns.namespace("fan")
FanState = fan_ns.class_("FanState", cg.Nameable, cg.Component)
MakeFan = cg.Application.struct("MakeFan")
FanDirection = fan_ns.enum("FanDirection")
FAN_DIRECTION_ENUM = {
"FORWARD": FanDirection.FAN_DIRECTION_FORWARD,
"REVERSE": FanDirection.FAN_DIRECTION_REVERSE,
}
# Actions
TurnOnAction = fan_ns.class_("TurnOnAction", automation.Action)
TurnOffAction = fan_ns.class_("TurnOffAction", automation.Action)
@ -34,6 +42,10 @@ ToggleAction = fan_ns.class_("ToggleAction", automation.Action)
FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template())
FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template())
FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template())
FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template())
FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template())
FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
{
@ -61,6 +73,11 @@ FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOffTrigger),
}
),
cv.Optional(CONF_ON_SPEED_SET): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger),
}
),
}
)
@ -100,6 +117,9 @@ async def setup_fan_core_(var, config):
for conf in config.get(CONF_ON_TURN_OFF, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_SPEED_SET, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
async def register_fan(var, config):
@ -143,6 +163,9 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args):
cv.Required(CONF_ID): cv.use_id(FanState),
cv.Optional(CONF_OSCILLATING): cv.templatable(cv.boolean),
cv.Optional(CONF_SPEED): cv.templatable(cv.int_range(1)),
cv.Optional(CONF_DIRECTION): cv.templatable(
cv.enum(FAN_DIRECTION_ENUM, upper=True)
),
}
),
)
@ -155,9 +178,35 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args):
if CONF_SPEED in config:
template_ = await cg.templatable(config[CONF_SPEED], args, int)
cg.add(var.set_speed(template_))
if CONF_DIRECTION in config:
template_ = await cg.templatable(config[CONF_DIRECTION], args, FanDirection)
cg.add(var.set_direction(template_))
return var
@automation.register_condition(
"fan.is_on",
FanIsOnCondition,
automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(FanState),
}
),
)
@automation.register_condition(
"fan.is_off",
FanIsOffCondition,
automation.maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(FanState),
}
),
)
async def fan_is_on_off_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren)
@coroutine_with_priority(100.0)
async def to_code(config):
cg.add_define("USE_FAN")

View File

@ -13,6 +13,7 @@ template<typename... Ts> class TurnOnAction : public Action<Ts...> {
TEMPLATABLE_VALUE(bool, oscillating)
TEMPLATABLE_VALUE(int, speed)
TEMPLATABLE_VALUE(FanDirection, direction)
void play(Ts... x) override {
auto call = this->state_->turn_on();
@ -22,6 +23,9 @@ template<typename... Ts> class TurnOnAction : public Action<Ts...> {
if (this->speed_.has_value()) {
call.set_speed(this->speed_.value(x...));
}
if (this->direction_.has_value()) {
call.set_direction(this->direction_.value(x...));
}
call.perform();
}
@ -46,6 +50,23 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> {
FanState *state_;
};
template<typename... Ts> class FanIsOnCondition : public Condition<Ts...> {
public:
explicit FanIsOnCondition(FanState *state) : state_(state) {}
bool check(Ts... x) override { return this->state_->state; }
protected:
FanState *state_;
};
template<typename... Ts> class FanIsOffCondition : public Condition<Ts...> {
public:
explicit FanIsOffCondition(FanState *state) : state_(state) {}
bool check(Ts... x) override { return !this->state_->state; }
protected:
FanState *state_;
};
class FanTurnOnTrigger : public Trigger<> {
public:
FanTurnOnTrigger(FanState *state) {
@ -82,5 +103,23 @@ class FanTurnOffTrigger : public Trigger<> {
bool last_on_;
};
class FanSpeedSetTrigger : public Trigger<> {
public:
FanSpeedSetTrigger(FanState *state) {
state->add_on_state_callback([this, state]() {
auto speed = state->speed;
auto should_trigger = speed != !this->last_speed_;
this->last_speed_ = speed;
if (should_trigger) {
this->trigger();
}
});
this->last_speed_ = state->speed;
}
protected:
int last_speed_;
};
} // namespace fan
} // namespace esphome

View File

@ -4,6 +4,9 @@
namespace esphome {
namespace fan {
// This whole file is deprecated, don't warn about usage of deprecated types in here.
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) {
const auto speed_ratio = static_cast<float>(speed_level) / (supported_speed_levels + 1);
const auto legacy_level = clamp<int>(static_cast<int>(ceilf(speed_ratio * 3)), 1, 3);

View File

@ -4,8 +4,16 @@
namespace esphome {
namespace fan {
// Shut-up about usage of deprecated FanSpeed for a bit.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
ESPDEPRECATED("FanSpeed and speed_level_to_enum() are deprecated.", "2021.9")
FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels);
ESPDEPRECATED("FanSpeed and speed_enum_to_level() are deprecated.", "2021.9")
int speed_enum_to_level(FanSpeed speed, int supported_speed_levels);
#pragma GCC diagnostic pop
} // namespace fan
} // namespace esphome

View File

@ -39,7 +39,7 @@ void FanState::setup() {
call.set_direction(recovered.direction);
call.perform();
}
float FanState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
float FanState::get_setup_priority() const { return setup_priority::DATA - 1.0f; }
uint32_t FanState::hash_base() { return 418001110UL; }
void FanStateCall::perform() const {
@ -67,6 +67,8 @@ void FanStateCall::perform() const {
this->state_->state_callback_.call();
}
// This whole method is deprecated, don't warn about usage of deprecated methods inside of it.
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
FanStateCall &FanStateCall::set_speed(const char *legacy_speed) {
const auto supported_speed_count = this->state_->get_traits().supported_speed_count();
if (strcasecmp(legacy_speed, "low") == 0) {

View File

@ -10,7 +10,7 @@ namespace esphome {
namespace fan {
/// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon
enum FanSpeed {
enum ESPDEPRECATED("FanSpeed is deprecated.", "2021.9") FanSpeed {
FAN_SPEED_LOW = 0, ///< The fan is running on low speed.
FAN_SPEED_MEDIUM = 1, ///< The fan is running on medium speed.
FAN_SPEED_HIGH = 2 ///< The fan is running on high/full speed.
@ -45,6 +45,7 @@ class FanStateCall {
this->speed_ = speed;
return *this;
}
ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9")
FanStateCall &set_speed(const char *legacy_speed);
FanStateCall &set_direction(FanDirection direction) {
this->direction_ = direction;

View File

@ -20,13 +20,12 @@ void FastLEDLightOutput::dump_config() {
ESP_LOGCONFIG(TAG, " Num LEDs: %u", this->num_leds_);
ESP_LOGCONFIG(TAG, " Max refresh rate: %u", *this->max_refresh_rate_);
}
void FastLEDLightOutput::loop() {
if (!this->should_show_())
return;
uint32_t now = micros();
void FastLEDLightOutput::write_state(light::LightState *state) {
// protect from refreshing too often
uint32_t now = micros();
if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) {
// try again next loop iteration, so that this change won't get lost
this->schedule_show();
return;
}
this->last_refresh_ = now;

View File

@ -213,7 +213,7 @@ class FastLEDLightOutput : public light::AddressableLight {
}
void setup() override;
void dump_config() override;
void loop() override;
void write_state(light::LightState *state) override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void clear_effect_data() override {

View File

@ -297,12 +297,6 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) {
}
}
// Validate footer
if (!data.expect_mark(FUJITSU_GENERAL_BIT_MARK)) {
ESP_LOGV(TAG, "Footer fail");
return false;
}
for (uint8_t byte = 0; byte < recv_message_length; ++byte) {
ESP_LOGVV(TAG, "%02X", recv_message[byte]);
}

View File

@ -13,9 +13,8 @@ from esphome.const import (
DEVICE_CLASS_POWER,
DEVICE_CLASS_VOLTAGE,
ICON_CURRENT_AC,
LAST_RESET_TYPE_AUTO,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_NONE,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_DEGREES,
UNIT_HERTZ,
@ -143,25 +142,23 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_TOTAL_GENERATION_TIME): sensor.sensor_schema(
unit_of_measurement=UNIT_HOURS,
accuracy_decimals=0,
state_class=STATE_CLASS_NONE,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_TODAY_GENERATION_TIME): sensor.sensor_schema(
unit_of_measurement=UNIT_MINUTE,
accuracy_decimals=0,
state_class=STATE_CLASS_NONE,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_INVERTER_MODULE_TEMP): sensor.sensor_schema(
unit_of_measurement=UNIT_DEGREES,

View File

@ -0,0 +1,3 @@
import esphome.codegen as cg
hbridge_ns = cg.esphome_ns.namespace("hbridge")

View File

@ -0,0 +1,70 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.automation import maybe_simple_id
from esphome.components import fan, output
from esphome.const import (
CONF_ID,
CONF_DECAY_MODE,
CONF_SPEED_COUNT,
CONF_PIN_A,
CONF_PIN_B,
CONF_ENABLE_PIN,
)
from .. import hbridge_ns
CODEOWNERS = ["@WeekendWarrior"]
HBridgeFan = hbridge_ns.class_("HBridgeFan", fan.FanState)
DecayMode = hbridge_ns.enum("DecayMode")
DECAY_MODE_OPTIONS = {
"SLOW": DecayMode.DECAY_MODE_SLOW,
"FAST": DecayMode.DECAY_MODE_FAST,
}
# Actions
BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action)
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan),
cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput),
cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput),
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
DECAY_MODE_OPTIONS, upper=True
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
}
).extend(cv.COMPONENT_SCHEMA)
@automation.register_action(
"fan.hbridge.brake",
BrakeAction,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(HBridgeFan)}),
)
async def fan_hbridge_brake_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
async def to_code(config):
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_SPEED_COUNT],
config[CONF_DECAY_MODE],
)
await fan.register_fan(var, config)
pin_a_ = await cg.get_variable(config[CONF_PIN_A])
cg.add(var.set_pin_a(pin_a_))
pin_b_ = await cg.get_variable(config[CONF_PIN_B])
cg.add(var.set_pin_b(pin_b_))
if CONF_ENABLE_PIN in config:
enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN])
cg.add(var.set_enable_pin(enable_pin))

View File

@ -0,0 +1,85 @@
#include "hbridge_fan.h"
#include "esphome/components/fan/fan_helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace hbridge {
static const char *const TAG = "fan.hbridge";
void HBridgeFan::set_hbridge_levels_(float a_level, float b_level) {
this->pin_a_->set_level(a_level);
this->pin_b_->set_level(b_level);
ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f", a_level, b_level);
}
// constant IN1/IN2, PWM on EN => power control, fast current decay
// constant IN1/EN, PWM on IN2 => power control, slow current decay
void HBridgeFan::set_hbridge_levels_(float a_level, float b_level, float enable) {
this->pin_a_->set_level(a_level);
this->pin_b_->set_level(b_level);
this->enable_->set_level(enable);
ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f, enable: %.2f", a_level, b_level, enable);
}
fan::FanStateCall HBridgeFan::brake() {
ESP_LOGD(TAG, "Braking");
(this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f) : this->set_hbridge_levels_(1.0f, 1.0f, 1.0f);
return this->make_call().set_state(false);
}
void HBridgeFan::dump_config() {
ESP_LOGCONFIG(TAG, "Fan '%s':", this->get_name().c_str());
if (this->get_traits().supports_oscillation()) {
ESP_LOGCONFIG(TAG, " Oscillation: YES");
}
if (this->get_traits().supports_direction()) {
ESP_LOGCONFIG(TAG, " Direction: YES");
}
if (this->decay_mode_ == DECAY_MODE_SLOW) {
ESP_LOGCONFIG(TAG, " Decay Mode: Slow");
} else {
ESP_LOGCONFIG(TAG, " Decay Mode: Fast");
}
}
void HBridgeFan::setup() {
auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_);
this->set_traits(traits);
this->add_on_state_callback([this]() { this->next_update_ = true; });
}
void HBridgeFan::loop() {
if (!this->next_update_) {
return;
}
this->next_update_ = false;
float speed = 0.0f;
if (this->state) {
speed = static_cast<float>(this->speed) / static_cast<float>(this->speed_count_);
}
if (speed == 0.0f) { // off means idle
(this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, speed)
: this->set_hbridge_levels_(speed, speed, speed);
return;
}
if (this->direction == fan::FAN_DIRECTION_FORWARD) {
if (this->decay_mode_ == DECAY_MODE_SLOW) {
(this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f - speed, 1.0f)
: this->set_hbridge_levels_(1.0f - speed, 1.0f, 1.0f);
} else { // DECAY_MODE_FAST
(this->enable_ == nullptr) ? this->set_hbridge_levels_(0.0f, speed)
: this->set_hbridge_levels_(0.0f, 1.0f, speed);
}
} else { // fan::FAN_DIRECTION_REVERSE
if (this->decay_mode_ == DECAY_MODE_SLOW) {
(this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f - speed)
: this->set_hbridge_levels_(1.0f, 1.0f - speed, 1.0f);
} else { // DECAY_MODE_FAST
(this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, 0.0f)
: this->set_hbridge_levels_(1.0f, 0.0f, speed);
}
}
}
} // namespace hbridge
} // namespace esphome

View File

@ -0,0 +1,58 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/components/output/binary_output.h"
#include "esphome/components/output/float_output.h"
#include "esphome/components/fan/fan_state.h"
namespace esphome {
namespace hbridge {
enum DecayMode {
DECAY_MODE_SLOW = 0,
DECAY_MODE_FAST = 1,
};
class HBridgeFan : public fan::FanState {
public:
HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; }
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
fan::FanStateCall brake();
int get_speed_count() { return this->speed_count_; }
// update Hbridge without a triggered FanState change, eg. for acceleration/deceleration ramping
void internal_update() { this->next_update_ = true; }
protected:
output::FloatOutput *pin_a_;
output::FloatOutput *pin_b_;
output::FloatOutput *enable_{nullptr};
output::BinaryOutput *oscillating_{nullptr};
bool next_update_{true};
int speed_count_{};
DecayMode decay_mode_{DECAY_MODE_SLOW};
void set_hbridge_levels_(float a_level, float b_level);
void set_hbridge_levels_(float a_level, float b_level, float enable);
};
template<typename... Ts> class BrakeAction : public Action<Ts...> {
public:
explicit BrakeAction(HBridgeFan *parent) : parent_(parent) {}
void play(Ts... x) override { this->parent_->brake(); }
HBridgeFan *parent_;
};
} // namespace hbridge
} // namespace esphome

View File

@ -2,8 +2,10 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import light, output
from esphome.const import CONF_OUTPUT_ID, CONF_PIN_A, CONF_PIN_B
from .. import hbridge_ns
CODEOWNERS = ["@DotNetDann"]
hbridge_ns = cg.esphome_ns.namespace("hbridge")
HBridgeLightOutput = hbridge_ns.class_(
"HBridgeLightOutput", cg.PollingComponent, light.LightOutput
)

View File

@ -18,8 +18,8 @@ from esphome.const import (
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_VOLTAGE,
LAST_RESET_TYPE_AUTO,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_VOLT,
UNIT_AMPERE,
UNIT_WATT,
@ -78,8 +78,7 @@ CONFIG_SCHEMA = cv.Schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance,
cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float,

View File

@ -6,6 +6,10 @@ from esphome.const import (
CONF_PM_2_5,
CONF_PM_10_0,
CONF_PM_1_0,
DEVICE_CLASS_AQI,
DEVICE_CLASS_PM1,
DEVICE_CLASS_PM10,
DEVICE_CLASS_PM25,
STATE_CLASS_MEASUREMENT,
UNIT_MICROGRAMS_PER_CUBIC_METER,
ICON_CHEMICAL_WEAPON,
@ -45,24 +49,28 @@ CONFIG_SCHEMA = cv.All(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM1,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM10,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AQI): sensor.sensor_schema(
unit_of_measurement=UNIT_INDEX,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_AQI,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{

View File

@ -42,7 +42,7 @@ CONFIG_SCHEMA = cv.All(
}
)
.extend(cv.polling_component_schema("1s"))
.extend(spi.spi_device_schema()),
.extend(spi.spi_device_schema(False)),
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
)

View File

@ -7,6 +7,7 @@ from esphome.const import (
CONF_DEFAULT_TRANSITION_LENGTH,
CONF_DISABLED_BY_DEFAULT,
CONF_EFFECTS,
CONF_FLASH_TRANSITION_LENGTH,
CONF_GAMMA_CORRECT,
CONF_ID,
CONF_INTERNAL,
@ -85,6 +86,9 @@ BRIGHTNESS_ONLY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend(
cv.Optional(
CONF_DEFAULT_TRANSITION_LENGTH, default="1s"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_FLASH_TRANSITION_LENGTH, default="0s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_EFFECTS): validate_effects(MONOCHROMATIC_EFFECTS),
}
)
@ -132,6 +136,10 @@ async def setup_light_core_(light_var, output_var, config):
config[CONF_DEFAULT_TRANSITION_LENGTH]
)
)
if CONF_FLASH_TRANSITION_LENGTH in config:
cg.add(
light_var.set_flash_transition_length(config[CONF_FLASH_TRANSITION_LENGTH])
)
if CONF_GAMMA_CORRECT in config:
cg.add(light_var.set_gamma_correct(config[CONF_GAMMA_CORRECT]))
effects = await cg.build_registry_list(

View File

@ -12,14 +12,13 @@ void AddressableLight::call_setup() {
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
this->set_interval(5000, [this]() {
const char *name = this->state_parent_ == nullptr ? "" : this->state_parent_->get_name().c_str();
ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s next_show=%s)", name, YESNO(this->effect_active_),
YESNO(this->next_show_));
ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s)", name, YESNO(this->effect_active_));
for (int i = 0; i < this->size(); i++) {
auto color = this->get(i);
ESP_LOGVV(TAG, " [%2d] Color: R=%3u G=%3u B=%3u W=%3u", i, color.get_red_raw(), color.get_green_raw(),
color.get_blue_raw(), color.get_white_raw());
}
ESP_LOGVV(TAG, "");
ESP_LOGVV(TAG, " ");
});
#endif
}
@ -36,7 +35,7 @@ Color esp_color_from_light_color_values(LightColorValues val) {
return Color(r, g, b, w);
}
void AddressableLight::write_state(LightState *state) {
void AddressableLight::update_state(LightState *state) {
auto val = state->current_values;
auto max_brightness = to_uint8_scale(val.get_brightness() * val.get_state());
this->correction_.set_local_brightness(max_brightness);

View File

@ -51,9 +51,9 @@ class AddressableLight : public LightOutput, public Component {
amnt = this->size();
this->range(amnt, this->size()) = this->range(0, -amnt);
}
// Indicates whether an effect that directly updates the output buffer is active to prevent overwriting
bool is_effect_active() const { return this->effect_active_; }
void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; }
void write_state(LightState *state) override;
std::unique_ptr<LightTransformer> create_default_transition() override;
void set_correction(float red, float green, float blue, float white = 1.0f) {
this->correction_.set_max_brightness(
@ -63,7 +63,8 @@ class AddressableLight : public LightOutput, public Component {
this->correction_.calculate_gamma_table(state->get_gamma_correct());
this->state_parent_ = state;
}
void schedule_show() { this->next_show_ = true; }
void update_state(LightState *state) override;
void schedule_show() { this->state_parent_->next_write_ = true; }
#ifdef USE_POWER_SUPPLY
void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); }
@ -74,9 +75,7 @@ class AddressableLight : public LightOutput, public Component {
protected:
friend class AddressableLightTransformer;
bool should_show_() const { return this->effect_active_ || this->next_show_; }
void mark_shown_() {
this->next_show_ = false;
#ifdef USE_POWER_SUPPLY
for (auto c : *this) {
if (c.get().is_on()) {
@ -90,7 +89,6 @@ class AddressableLight : public LightOutput, public Component {
virtual ESPColorView get_view_internal(int32_t index) const = 0;
bool effect_active_{false};
bool next_show_{true};
ESPColorCorrection correction_{};
#ifdef USE_POWER_SUPPLY
power_supply::PowerSupplyRequester power_;

View File

@ -63,6 +63,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect {
this->last_run_ = now;
this->f_(it, current_color, this->initial_run_);
this->initial_run_ = false;
it.schedule_show();
}
}
@ -87,6 +88,7 @@ class AddressableRainbowLightEffect : public AddressableLightEffect {
var = hsv;
hue += add;
}
it.schedule_show();
}
void set_speed(uint32_t speed) { this->speed_ = speed; }
void set_width(uint16_t width) { this->width_ = width; }
@ -134,6 +136,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
new_color.b = c.b;
}
}
it.schedule_show();
}
protected:
@ -151,25 +154,27 @@ class AddressableScanEffect : public AddressableLightEffect {
void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; }
void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; }
void apply(AddressableLight &it, const Color &current_color) override {
it.all() = Color::BLACK;
const uint32_t now = millis();
if (now - this->last_move_ < this->move_interval_)
return;
if (direction_) {
this->at_led_++;
if (this->at_led_ == it.size() - this->scan_width_)
this->direction_ = false;
} else {
this->at_led_--;
if (this->at_led_ == 0)
this->direction_ = true;
}
this->last_move_ = now;
it.all() = Color::BLACK;
for (auto i = 0; i < this->scan_width_; i++) {
it[this->at_led_ + i] = current_color;
}
const uint32_t now = millis();
if (now - this->last_move_ > this->move_interval_) {
if (direction_) {
this->at_led_++;
if (this->at_led_ == it.size() - this->scan_width_)
this->direction_ = false;
} else {
this->at_led_--;
if (this->at_led_ == 0)
this->direction_ = true;
}
this->last_move_ = now;
}
it.schedule_show();
}
protected:
@ -210,6 +215,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect {
continue;
addressable[pos].set_effect_data(1);
}
addressable.schedule_show();
}
void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; }
void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; }
@ -257,6 +263,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect {
const uint8_t color = random_uint32() & 0b111;
it[pos].set_effect_data(0b1000 | color);
}
it.schedule_show();
}
void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; }
void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; }
@ -301,6 +308,7 @@ class AddressableFireworksEffect : public AddressableLightEffect {
it[pos] = current_color;
}
}
it.schedule_show();
}
void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
void set_spark_probability(float spark_probability) { this->spark_probability_ = spark_probability; }
@ -335,6 +343,7 @@ class AddressableFlickerEffect : public AddressableLightEffect {
// slowly fade back to "real" value
var = (var.get() * inv_intensity) + (current_color * intensity);
}
it.schedule_show();
}
void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
void set_intensity(float intensity) { this->intensity_ = to_uint8_scale(intensity); }

View File

@ -156,7 +156,7 @@ class StrobeLightEffect : public LightEffect {
if (!color.is_on()) {
// Don't turn the light off, otherwise the light effect will be stopped
call.set_brightness_if_supported(0.0f);
call.set_brightness(0.0f);
call.set_state(true);
}
call.set_publish(false);
@ -196,7 +196,6 @@ class FlickerLightEffect : public LightEffect {
out.set_warm_white(remote.get_warm_white() * beta + current.get_warm_white() * alpha +
(random_cubic_float() * this->intensity_));
auto traits = this->state_->get_traits();
auto call = this->state_->make_call();
call.set_publish(false);
call.set_save(false);

View File

@ -52,7 +52,7 @@ enum class ColorMode : uint8_t {
/// Only on/off control.
ON_OFF = (uint8_t) ColorCapability::ON_OFF,
/// Dimmable light.
BRIGHTNESS = (uint8_t) ColorCapability::BRIGHTNESS,
BRIGHTNESS = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS),
/// White output only (use only if the light also has another color mode such as RGB).
WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::WHITE),
/// Controllable color temperature output.

View File

@ -8,26 +8,23 @@ namespace light {
static const char *const TAG = "light";
static const char *color_mode_to_human(ColorMode color_mode) {
switch (color_mode) {
case ColorMode::UNKNOWN:
return "Unknown";
case ColorMode::WHITE:
return "White";
case ColorMode::COLOR_TEMPERATURE:
return "Color temperature";
case ColorMode::COLD_WARM_WHITE:
return "Cold/warm white";
case ColorMode::RGB:
return "RGB";
case ColorMode::RGB_WHITE:
return "RGBW";
case ColorMode::RGB_COLD_WARM_WHITE:
return "RGB + cold/warm white";
case ColorMode::RGB_COLOR_TEMPERATURE:
return "RGB + color temperature";
default:
return "";
}
if (color_mode == ColorMode::UNKNOWN)
return "Unknown";
if (color_mode == ColorMode::WHITE)
return "White";
if (color_mode == ColorMode::COLOR_TEMPERATURE)
return "Color temperature";
if (color_mode == ColorMode::COLD_WARM_WHITE)
return "Cold/warm white";
if (color_mode == ColorMode::RGB)
return "RGB";
if (color_mode == ColorMode::RGB_WHITE)
return "RGBW";
if (color_mode == ColorMode::RGB_COLD_WARM_WHITE)
return "RGB + cold/warm white";
if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE)
return "RGB + color temperature";
return "";
}
void LightCall::perform() {

View File

@ -19,6 +19,13 @@ class LightOutput {
virtual void setup_state(LightState *state) {}
/// Called on every update of the current values of the associated LightState,
/// can optionally be used to do processing of this change.
virtual void update_state(LightState *state) {}
/// Called from loop() every time the light state has changed, and should
/// should write the new state to hardware. Every call to write_state() is
/// preceded by (at least) one call to update_state().
virtual void write_state(LightState *state) = 0;
};

View File

@ -114,9 +114,11 @@ void LightState::loop() {
// Apply transformer (if any)
if (this->transformer_ != nullptr) {
auto values = this->transformer_->apply();
this->next_write_ = values.has_value(); // don't write if transformer doesn't want us to
if (values.has_value())
if (values.has_value()) {
this->current_values = *values;
this->output_->update_state(this);
this->next_write_ = true;
}
if (this->transformer_->is_finished()) {
this->transformer_->stop();
@ -127,18 +129,15 @@ void LightState::loop() {
// Write state to the light
if (this->next_write_) {
this->output_->write_state(this);
this->next_write_ = false;
this->output_->write_state(this);
}
}
float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
uint32_t LightState::hash_base() { return 1114400283; }
void LightState::publish_state() {
this->remote_values_callback_.call();
this->next_write_ = true;
}
void LightState::publish_state() { this->remote_values_callback_.call(); }
LightOutput *LightState::get_output() const { return this->output_; }
std::string LightState::get_effect_name() {
@ -158,6 +157,11 @@ void LightState::add_new_target_state_reached_callback(std::function<void()> &&s
void LightState::set_default_transition_length(uint32_t default_transition_length) {
this->default_transition_length_ = default_transition_length;
}
uint32_t LightState::get_default_transition_length() const { return this->default_transition_length_; }
void LightState::set_flash_transition_length(uint32_t flash_transition_length) {
this->flash_transition_length_ = flash_transition_length;
}
uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; }
void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; }
void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
bool LightState::supports_effects() { return !this->effects_.empty(); }
@ -235,7 +239,7 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length) {
// If starting a flash if one is already happening, set end values to end values of current flash
// Hacky but works
if (this->transformer_ != nullptr)
end_colors = this->transformer_->get_target_values();
end_colors = this->transformer_->get_start_values();
this->transformer_ = make_unique<LightFlashTransformer>(*this);
this->transformer_->setup(end_colors, target, length);
@ -248,6 +252,7 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot
if (set_remote_values) {
this->remote_values = target;
}
this->output_->update_state(this);
this->next_write_ = true;
}

View File

@ -99,6 +99,11 @@ class LightState : public Nameable, public Component {
/// Set the default transition length, i.e. the transition length when no transition is provided.
void set_default_transition_length(uint32_t default_transition_length);
uint32_t get_default_transition_length() const;
/// Set the flash transition length
void set_flash_transition_length(uint32_t flash_transition_length);
uint32_t get_flash_transition_length() const;
/// Set the gamma correction factor
void set_gamma_correct(float gamma_correct);
@ -188,6 +193,8 @@ class LightState : public Nameable, public Component {
/// Default transition length for all transitions in ms.
uint32_t default_transition_length_{};
/// Transition length to use for flash transitions.
uint32_t flash_transition_length_{};
/// Gamma correction factor for the light.
float gamma_correct_{};
/// Restore mode of the light.

View File

@ -58,7 +58,43 @@ class LightFlashTransformer : public LightTransformer {
public:
LightFlashTransformer(LightState &state) : state_(state) {}
optional<LightColorValues> apply() override { return this->get_target_values(); }
void start() override {
this->transition_length_ = this->state_.get_flash_transition_length();
if (this->transition_length_ * 2 > this->length_)
this->transition_length_ = this->length_ / 2;
// do not create transition if length is 0
if (this->transition_length_ == 0)
return;
// first transition to original target
this->transformer_ = this->state_.get_output()->create_default_transition();
this->transformer_->setup(this->state_.current_values, this->target_values_, this->transition_length_);
}
optional<LightColorValues> apply() override {
// transition transformer does not handle 0 length as progress returns nan
if (this->transition_length_ == 0)
return this->target_values_;
if (this->transformer_ != nullptr) {
if (!this->transformer_->is_finished()) {
return this->transformer_->apply();
} else {
this->transformer_->stop();
this->transformer_ = nullptr;
}
}
if (millis() > this->start_time_ + this->length_ - this->transition_length_) {
// second transition back to start value
this->transformer_ = this->state_.get_output()->create_default_transition();
this->transformer_->setup(this->state_.current_values, this->get_start_values(), this->transition_length_);
}
// once transition is complete, don't change states until next transition
return optional<LightColorValues>();
}
// Restore the original values after the flash.
void stop() override {
@ -69,6 +105,8 @@ class LightFlashTransformer : public LightTransformer {
protected:
LightState &state_;
uint32_t transition_length_;
std::unique_ptr<LightTransformer> transformer_{nullptr};
};
} // namespace light

View File

@ -43,21 +43,24 @@ void Logger::write_header_(int level, const char *tag, int line) {
}
void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT
if (level > this->level_for(tag))
if (level > this->level_for(tag) || recursion_guard_)
return;
recursion_guard_ = true;
this->reset_buffer_();
this->write_header_(level, tag, line);
this->vprintf_to_buffer_(format, args);
this->write_footer_();
this->log_message_(level, tag);
recursion_guard_ = false;
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format,
va_list args) { // NOLINT
if (level > this->level_for(tag))
if (level > this->level_for(tag) || recursion_guard_)
return;
recursion_guard_ = true;
this->reset_buffer_();
// copy format string
const char *format_pgm_p = (PGM_P) format;
@ -78,6 +81,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr
this->vprintf_to_buffer_(this->tx_buffer_, args);
this->write_footer_();
this->log_message_(level, tag, offset);
recursion_guard_ = false;
}
#endif

View File

@ -113,6 +113,8 @@ class Logger : public Component {
};
std::vector<LogLevelOverride> log_levels_;
CallbackManager<void(int, const char *, const char *)> log_callback_{};
/// Prevents recursive log calls, if true a log message is already being processed.
bool recursion_guard_ = false;
};
extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@ -91,7 +91,7 @@ async def mcp23xxx_pin_to_code(config):
# BEGIN Removed pin schemas below to show error in configuration
# TODO remove in 1.19.0
# TODO remove in 2022.5.0
for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]:
PIN_SCHEMA = cv.Schema(
@ -110,6 +110,7 @@ for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]:
}
)
# pylint: disable=cell-var-from-loop
@pins.PIN_SCHEMA_REGISTRY.register(id, (PIN_SCHEMA, PIN_SCHEMA))
def pin_to_code(config):
pass

View File

@ -8,6 +8,7 @@ from esphome.const import (
CONF_ID,
CONF_TEMPERATURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_CARBON_DIOXIDE,
ICON_MOLECULE_CO2,
STATE_CLASS_MEASUREMENT,
UNIT_PARTS_PER_MILLION,
@ -34,6 +35,7 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(

View File

@ -0,0 +1,173 @@
#include "esphome/core/log.h"
#include "adapter.h"
namespace esphome {
namespace midea {
const char *const Constants::TAG = "midea";
const std::string Constants::FREEZE_PROTECTION = "freeze protection";
const std::string Constants::SILENT = "silent";
const std::string Constants::TURBO = "turbo";
ClimateMode Converters::to_climate_mode(MideaMode mode) {
switch (mode) {
case MideaMode::MODE_AUTO:
return ClimateMode::CLIMATE_MODE_HEAT_COOL;
case MideaMode::MODE_COOL:
return ClimateMode::CLIMATE_MODE_COOL;
case MideaMode::MODE_DRY:
return ClimateMode::CLIMATE_MODE_DRY;
case MideaMode::MODE_FAN_ONLY:
return ClimateMode::CLIMATE_MODE_FAN_ONLY;
case MideaMode::MODE_HEAT:
return ClimateMode::CLIMATE_MODE_HEAT;
default:
return ClimateMode::CLIMATE_MODE_OFF;
}
}
MideaMode Converters::to_midea_mode(ClimateMode mode) {
switch (mode) {
case ClimateMode::CLIMATE_MODE_HEAT_COOL:
return MideaMode::MODE_AUTO;
case ClimateMode::CLIMATE_MODE_COOL:
return MideaMode::MODE_COOL;
case ClimateMode::CLIMATE_MODE_DRY:
return MideaMode::MODE_DRY;
case ClimateMode::CLIMATE_MODE_FAN_ONLY:
return MideaMode::MODE_FAN_ONLY;
case ClimateMode::CLIMATE_MODE_HEAT:
return MideaMode::MODE_HEAT;
default:
return MideaMode::MODE_OFF;
}
}
ClimateSwingMode Converters::to_climate_swing_mode(MideaSwingMode mode) {
switch (mode) {
case MideaSwingMode::SWING_VERTICAL:
return ClimateSwingMode::CLIMATE_SWING_VERTICAL;
case MideaSwingMode::SWING_HORIZONTAL:
return ClimateSwingMode::CLIMATE_SWING_HORIZONTAL;
case MideaSwingMode::SWING_BOTH:
return ClimateSwingMode::CLIMATE_SWING_BOTH;
default:
return ClimateSwingMode::CLIMATE_SWING_OFF;
}
}
MideaSwingMode Converters::to_midea_swing_mode(ClimateSwingMode mode) {
switch (mode) {
case ClimateSwingMode::CLIMATE_SWING_VERTICAL:
return MideaSwingMode::SWING_VERTICAL;
case ClimateSwingMode::CLIMATE_SWING_HORIZONTAL:
return MideaSwingMode::SWING_HORIZONTAL;
case ClimateSwingMode::CLIMATE_SWING_BOTH:
return MideaSwingMode::SWING_BOTH;
default:
return MideaSwingMode::SWING_OFF;
}
}
MideaFanMode Converters::to_midea_fan_mode(ClimateFanMode mode) {
switch (mode) {
case ClimateFanMode::CLIMATE_FAN_LOW:
return MideaFanMode::FAN_LOW;
case ClimateFanMode::CLIMATE_FAN_MEDIUM:
return MideaFanMode::FAN_MEDIUM;
case ClimateFanMode::CLIMATE_FAN_HIGH:
return MideaFanMode::FAN_HIGH;
default:
return MideaFanMode::FAN_AUTO;
}
}
ClimateFanMode Converters::to_climate_fan_mode(MideaFanMode mode) {
switch (mode) {
case MideaFanMode::FAN_LOW:
return ClimateFanMode::CLIMATE_FAN_LOW;
case MideaFanMode::FAN_MEDIUM:
return ClimateFanMode::CLIMATE_FAN_MEDIUM;
case MideaFanMode::FAN_HIGH:
return ClimateFanMode::CLIMATE_FAN_HIGH;
default:
return ClimateFanMode::CLIMATE_FAN_AUTO;
}
}
bool Converters::is_custom_midea_fan_mode(MideaFanMode mode) {
switch (mode) {
case MideaFanMode::FAN_SILENT:
case MideaFanMode::FAN_TURBO:
return true;
default:
return false;
}
}
const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) {
switch (mode) {
case MideaFanMode::FAN_SILENT:
return Constants::SILENT;
default:
return Constants::TURBO;
}
}
MideaFanMode Converters::to_midea_fan_mode(const std::string &mode) {
if (mode == Constants::SILENT)
return MideaFanMode::FAN_SILENT;
return MideaFanMode::FAN_TURBO;
}
MideaPreset Converters::to_midea_preset(ClimatePreset preset) {
switch (preset) {
case ClimatePreset::CLIMATE_PRESET_SLEEP:
return MideaPreset::PRESET_SLEEP;
case ClimatePreset::CLIMATE_PRESET_ECO:
return MideaPreset::PRESET_ECO;
case ClimatePreset::CLIMATE_PRESET_BOOST:
return MideaPreset::PRESET_TURBO;
default:
return MideaPreset::PRESET_NONE;
}
}
ClimatePreset Converters::to_climate_preset(MideaPreset preset) {
switch (preset) {
case MideaPreset::PRESET_SLEEP:
return ClimatePreset::CLIMATE_PRESET_SLEEP;
case MideaPreset::PRESET_ECO:
return ClimatePreset::CLIMATE_PRESET_ECO;
case MideaPreset::PRESET_TURBO:
return ClimatePreset::CLIMATE_PRESET_BOOST;
default:
return ClimatePreset::CLIMATE_PRESET_NONE;
}
}
bool Converters::is_custom_midea_preset(MideaPreset preset) { return preset == MideaPreset::PRESET_FREEZE_PROTECTION; }
const std::string &Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; }
MideaPreset Converters::to_midea_preset(const std::string &preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; }
void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities) {
if (capabilities.supportAutoMode())
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT_COOL);
if (capabilities.supportCoolMode())
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_COOL);
if (capabilities.supportHeatMode())
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT);
if (capabilities.supportDryMode())
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_DRY);
if (capabilities.supportTurboPreset())
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_BOOST);
if (capabilities.supportEcoPreset())
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO);
if (capabilities.supportFrostProtectionPreset())
traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION);
}
} // namespace midea
} // namespace esphome

View File

@ -0,0 +1,42 @@
#pragma once
#include <Appliance/AirConditioner/AirConditioner.h>
#include "esphome/components/climate/climate_traits.h"
#include "appliance_base.h"
namespace esphome {
namespace midea {
using MideaMode = dudanov::midea::ac::Mode;
using MideaSwingMode = dudanov::midea::ac::SwingMode;
using MideaFanMode = dudanov::midea::ac::FanMode;
using MideaPreset = dudanov::midea::ac::Preset;
class Constants {
public:
static const char *const TAG;
static const std::string FREEZE_PROTECTION;
static const std::string SILENT;
static const std::string TURBO;
};
class Converters {
public:
static MideaMode to_midea_mode(ClimateMode mode);
static ClimateMode to_climate_mode(MideaMode mode);
static MideaSwingMode to_midea_swing_mode(ClimateSwingMode mode);
static ClimateSwingMode to_climate_swing_mode(MideaSwingMode mode);
static MideaPreset to_midea_preset(ClimatePreset preset);
static MideaPreset to_midea_preset(const std::string &preset);
static bool is_custom_midea_preset(MideaPreset preset);
static ClimatePreset to_climate_preset(MideaPreset preset);
static const std::string &to_custom_climate_preset(MideaPreset preset);
static MideaFanMode to_midea_fan_mode(ClimateFanMode fan_mode);
static MideaFanMode to_midea_fan_mode(const std::string &fan_mode);
static bool is_custom_midea_fan_mode(MideaFanMode fan_mode);
static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode);
static const std::string &to_custom_climate_fan_mode(MideaFanMode fan_mode);
static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities);
};
} // namespace midea
} // namespace esphome

View File

@ -0,0 +1,152 @@
#include "esphome/core/log.h"
#include "air_conditioner.h"
#include "adapter.h"
#ifdef USE_REMOTE_TRANSMITTER
#include "midea_ir.h"
#endif
namespace esphome {
namespace midea {
static void set_sensor(Sensor *sensor, float value) {
if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value))
sensor->publish_state(value);
}
template<typename T> void update_property(T &property, const T &value, bool &flag) {
if (property != value) {
property = value;
flag = true;
}
}
void AirConditioner::on_status_change() {
bool need_publish = false;
update_property(this->target_temperature, this->base_.getTargetTemp(), need_publish);
update_property(this->current_temperature, this->base_.getIndoorTemp(), need_publish);
auto mode = Converters::to_climate_mode(this->base_.getMode());
update_property(this->mode, mode, need_publish);
auto swing_mode = Converters::to_climate_swing_mode(this->base_.getSwingMode());
update_property(this->swing_mode, swing_mode, need_publish);
// Preset
auto preset = this->base_.getPreset();
if (Converters::is_custom_midea_preset(preset)) {
if (this->set_custom_preset_(Converters::to_custom_climate_preset(preset)))
need_publish = true;
} else if (this->set_preset_(Converters::to_climate_preset(preset))) {
need_publish = true;
}
// Fan mode
auto fan_mode = this->base_.getFanMode();
if (Converters::is_custom_midea_fan_mode(fan_mode)) {
if (this->set_custom_fan_mode_(Converters::to_custom_climate_fan_mode(fan_mode)))
need_publish = true;
} else if (this->set_fan_mode_(Converters::to_climate_fan_mode(fan_mode))) {
need_publish = true;
}
if (need_publish)
this->publish_state();
set_sensor(this->outdoor_sensor_, this->base_.getOutdoorTemp());
set_sensor(this->power_sensor_, this->base_.getPowerUsage());
set_sensor(this->humidity_sensor_, this->base_.getIndoorHum());
}
void AirConditioner::control(const ClimateCall &call) {
dudanov::midea::ac::Control ctrl{};
if (call.get_target_temperature().has_value())
ctrl.targetTemp = call.get_target_temperature().value();
if (call.get_swing_mode().has_value())
ctrl.swingMode = Converters::to_midea_swing_mode(call.get_swing_mode().value());
if (call.get_mode().has_value())
ctrl.mode = Converters::to_midea_mode(call.get_mode().value());
if (call.get_preset().has_value())
ctrl.preset = Converters::to_midea_preset(call.get_preset().value());
else if (call.get_custom_preset().has_value())
ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().value());
if (call.get_fan_mode().has_value())
ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value());
else if (call.get_custom_fan_mode().has_value())
ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().value());
this->base_.control(ctrl);
}
ClimateTraits AirConditioner::traits() {
auto traits = ClimateTraits();
traits.set_supports_current_temperature(true);
traits.set_visual_min_temperature(17);
traits.set_visual_max_temperature(30);
traits.set_visual_temperature_step(0.5);
traits.set_supported_modes(this->supported_modes_);
traits.set_supported_swing_modes(this->supported_swing_modes_);
traits.set_supported_presets(this->supported_presets_);
traits.set_supported_custom_presets(this->supported_custom_presets_);
traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_);
/* + MINIMAL SET OF CAPABILITIES */
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF);
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_FAN_ONLY);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH);
traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF);
traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_VERTICAL);
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE);
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_SLEEP);
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK)
Converters::to_climate_traits(traits, this->base_.getCapabilities());
return traits;
}
void AirConditioner::dump_config() {
ESP_LOGCONFIG(Constants::TAG, "MideaDongle:");
ESP_LOGCONFIG(Constants::TAG, " [x] Period: %dms", this->base_.getPeriod());
ESP_LOGCONFIG(Constants::TAG, " [x] Response timeout: %dms", this->base_.getTimeout());
ESP_LOGCONFIG(Constants::TAG, " [x] Request attempts: %d", this->base_.getNumAttempts());
#ifdef USE_REMOTE_TRANSMITTER
ESP_LOGCONFIG(Constants::TAG, " [x] Using RemoteTransmitter");
#endif
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) {
this->base_.getCapabilities().dump();
} else if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_ERROR) {
ESP_LOGW(Constants::TAG,
"Failed to get 0xB5 capabilities report. Suggest to disable it in config and manually set your "
"appliance options.");
}
this->dump_traits_(Constants::TAG);
}
/* ACTIONS */
void AirConditioner::do_follow_me(float temperature, bool beeper) {
#ifdef USE_REMOTE_TRANSMITTER
IrFollowMeData data(static_cast<uint8_t>(lroundf(temperature)), beeper);
this->transmit_ir(data);
#else
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
#endif
}
void AirConditioner::do_swing_step() {
#ifdef USE_REMOTE_TRANSMITTER
IrSpecialData data(0x01);
this->transmit_ir(data);
#else
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
#endif
}
void AirConditioner::do_display_toggle() {
if (this->base_.getCapabilities().supportLightControl()) {
this->base_.displayToggle();
} else {
#ifdef USE_REMOTE_TRANSMITTER
IrSpecialData data(0x08);
this->transmit_ir(data);
#else
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
#endif
}
}
} // namespace midea
} // namespace esphome

View File

@ -0,0 +1,41 @@
#pragma once
#include <Appliance/AirConditioner/AirConditioner.h>
#include "appliance_base.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace midea {
using sensor::Sensor;
using climate::ClimateCall;
class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner> {
public:
void dump_config() override;
void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; }
void set_humidity_setpoint_sensor(Sensor *sensor) { this->humidity_sensor_ = sensor; }
void set_power_sensor(Sensor *sensor) { this->power_sensor_ = sensor; }
void on_status_change() override;
/* ############### */
/* ### ACTIONS ### */
/* ############### */
void do_follow_me(float temperature, bool beeper = false);
void do_display_toggle();
void do_swing_step();
void do_beeper_on() { this->set_beeper_feedback(true); }
void do_beeper_off() { this->set_beeper_feedback(false); }
void do_power_on() { this->base_.setPowerState(true); }
void do_power_off() { this->base_.setPowerState(false); }
protected:
void control(const ClimateCall &call) override;
ClimateTraits traits() override;
Sensor *outdoor_sensor_{nullptr};
Sensor *humidity_sensor_{nullptr};
Sensor *power_sensor_{nullptr};
};
} // namespace midea
} // namespace esphome

View File

@ -0,0 +1,76 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/climate/climate.h"
#ifdef USE_REMOTE_TRANSMITTER
#include "esphome/components/remote_base/midea_protocol.h"
#include "esphome/components/remote_transmitter/remote_transmitter.h"
#endif
#include <Appliance/ApplianceBase.h>
#include <Helpers/Logger.h>
namespace esphome {
namespace midea {
using climate::ClimatePreset;
using climate::ClimateTraits;
using climate::ClimateMode;
using climate::ClimateSwingMode;
using climate::ClimateFanMode;
template<typename T> class ApplianceBase : public Component, public uart::UARTDevice, public climate::Climate {
static_assert(std::is_base_of<dudanov::midea::ApplianceBase, T>::value,
"T must derive from dudanov::midea::ApplianceBase class");
public:
ApplianceBase() {
this->base_.setStream(this);
this->base_.addOnStateCallback(std::bind(&ApplianceBase::on_status_change, this));
dudanov::midea::ApplianceBase::setLogger([](int level, const char *tag, int line, String format, va_list args) {
esp_log_vprintf_(level, tag, line, format.c_str(), args);
});
}
bool can_proceed() override {
return this->base_.getAutoconfStatus() != dudanov::midea::AutoconfStatus::AUTOCONF_PROGRESS;
}
float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; }
void setup() override { this->base_.setup(); }
void loop() override { this->base_.loop(); }
void set_period(uint32_t ms) { this->base_.setPeriod(ms); }
void set_response_timeout(uint32_t ms) { this->base_.setTimeout(ms); }
void set_request_attempts(uint32_t attempts) { this->base_.setNumAttempts(attempts); }
void set_beeper_feedback(bool state) { this->base_.setBeeper(state); }
void set_autoconf(bool value) { this->base_.setAutoconf(value); }
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); }
void set_custom_presets(std::set<std::string> presets) { this->supported_custom_presets_ = std::move(presets); }
void set_custom_fan_modes(std::set<std::string> modes) { this->supported_custom_fan_modes_ = std::move(modes); }
virtual void on_status_change() = 0;
#ifdef USE_REMOTE_TRANSMITTER
void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
this->transmitter_ = transmitter;
}
void transmit_ir(remote_base::MideaData &data) {
data.finalize();
auto transmit = this->transmitter_->transmit();
remote_base::MideaProtocol().encode(transmit.get_data(), data);
transmit.perform();
}
#endif
protected:
T base_;
std::set<ClimateMode> supported_modes_{};
std::set<ClimateSwingMode> supported_swing_modes_{};
std::set<ClimatePreset> supported_presets_{};
std::set<std::string> supported_custom_presets_{};
std::set<std::string> supported_custom_fan_modes_{};
#ifdef USE_REMOTE_TRANSMITTER
remote_transmitter::RemoteTransmitterComponent *transmitter_{nullptr};
#endif
};
} // namespace midea
} // namespace esphome

View File

@ -0,0 +1,56 @@
#pragma once
#include "esphome/core/automation.h"
#include "air_conditioner.h"
namespace esphome {
namespace midea {
template<typename... Ts> class MideaActionBase : public Action<Ts...> {
public:
void set_parent(AirConditioner *parent) { this->parent_ = parent; }
protected:
AirConditioner *parent_;
};
template<typename... Ts> class FollowMeAction : public MideaActionBase<Ts...> {
TEMPLATABLE_VALUE(float, temperature)
TEMPLATABLE_VALUE(bool, beeper)
void play(Ts... x) override {
this->parent_->do_follow_me(this->temperature_.value(x...), this->beeper_.value(x...));
}
};
template<typename... Ts> class SwingStepAction : public MideaActionBase<Ts...> {
public:
void play(Ts... x) override { this->parent_->do_swing_step(); }
};
template<typename... Ts> class DisplayToggleAction : public MideaActionBase<Ts...> {
public:
void play(Ts... x) override { this->parent_->do_display_toggle(); }
};
template<typename... Ts> class BeeperOnAction : public MideaActionBase<Ts...> {
public:
void play(Ts... x) override { this->parent_->do_beeper_on(); }
};
template<typename... Ts> class BeeperOffAction : public MideaActionBase<Ts...> {
public:
void play(Ts... x) override { this->parent_->do_beeper_off(); }
};
template<typename... Ts> class PowerOnAction : public MideaActionBase<Ts...> {
public:
void play(Ts... x) override { this->parent_->do_power_on(); }
};
template<typename... Ts> class PowerOffAction : public MideaActionBase<Ts...> {
public:
void play(Ts... x) override { this->parent_->do_power_off(); }
};
} // namespace midea
} // namespace esphome

View File

@ -0,0 +1,284 @@
from esphome.core import coroutine
from esphome import automation
from esphome.components import climate, sensor, uart, remote_transmitter
from esphome.components.remote_base import CONF_TRANSMITTER_ID
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_AUTOCONF,
CONF_BEEPER,
CONF_CUSTOM_FAN_MODES,
CONF_CUSTOM_PRESETS,
CONF_ID,
CONF_NUM_ATTEMPTS,
CONF_PERIOD,
CONF_SUPPORTED_MODES,
CONF_SUPPORTED_PRESETS,
CONF_SUPPORTED_SWING_MODES,
CONF_TIMEOUT,
CONF_TEMPERATURE,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
ICON_POWER,
ICON_THERMOMETER,
ICON_WATER_PERCENT,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PERCENT,
UNIT_WATT,
)
from esphome.components.climate import (
ClimateMode,
ClimatePreset,
ClimateSwingMode,
)
CODEOWNERS = ["@dudanov"]
DEPENDENCIES = ["climate", "uart", "wifi"]
AUTO_LOAD = ["sensor"]
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
CONF_POWER_USAGE = "power_usage"
CONF_HUMIDITY_SETPOINT = "humidity_setpoint"
midea_ns = cg.esphome_ns.namespace("midea")
AirConditioner = midea_ns.class_("AirConditioner", climate.Climate, cg.Component)
Capabilities = midea_ns.namespace("Constants")
def templatize(value):
if isinstance(value, cv.Schema):
value = value.schema
ret = {}
for key, val in value.items():
ret[key] = cv.templatable(val)
return cv.Schema(ret)
def register_action(name, type_, schema):
validator = templatize(schema).extend(MIDEA_ACTION_BASE_SCHEMA)
registerer = automation.register_action(f"midea_ac.{name}", type_, validator)
def decorator(func):
async def new_func(config, action_id, template_arg, args):
ac_ = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg)
cg.add(var.set_parent(ac_))
await coroutine(func)(var, config, args)
return var
return registerer(new_func)
return decorator
ALLOWED_CLIMATE_MODES = {
"HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL,
"COOL": ClimateMode.CLIMATE_MODE_COOL,
"HEAT": ClimateMode.CLIMATE_MODE_HEAT,
"DRY": ClimateMode.CLIMATE_MODE_DRY,
"FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY,
}
ALLOWED_CLIMATE_PRESETS = {
"ECO": ClimatePreset.CLIMATE_PRESET_ECO,
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
"SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
}
ALLOWED_CLIMATE_SWING_MODES = {
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
}
CUSTOM_FAN_MODES = {
"SILENT": Capabilities.SILENT,
"TURBO": Capabilities.TURBO,
}
CUSTOM_PRESETS = {
"FREEZE_PROTECTION": Capabilities.FREEZE_PROTECTION,
}
validate_modes = cv.enum(ALLOWED_CLIMATE_MODES, upper=True)
validate_presets = cv.enum(ALLOWED_CLIMATE_PRESETS, upper=True)
validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True)
validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True)
validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True)
CONFIG_SCHEMA = cv.All(
climate.CLIMATE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(AirConditioner),
cv.Optional(CONF_PERIOD, default="1s"): cv.time_period,
cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period,
cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5),
cv.Optional(CONF_TRANSMITTER_ID): cv.use_id(
remote_transmitter.RemoteTransmitterComponent
),
cv.Optional(CONF_BEEPER, default=False): cv.boolean,
cv.Optional(CONF_AUTOCONF, default=True): cv.boolean,
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(validate_modes),
cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list(
validate_swing_modes
),
cv.Optional(CONF_SUPPORTED_PRESETS): cv.ensure_list(validate_presets),
cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(validate_custom_presets),
cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list(
validate_custom_fan_modes
),
cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
icon=ICON_POWER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
icon=ICON_WATER_PERCENT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
# Actions
FollowMeAction = midea_ns.class_("FollowMeAction", automation.Action)
DisplayToggleAction = midea_ns.class_("DisplayToggleAction", automation.Action)
SwingStepAction = midea_ns.class_("SwingStepAction", automation.Action)
BeeperOnAction = midea_ns.class_("BeeperOnAction", automation.Action)
BeeperOffAction = midea_ns.class_("BeeperOffAction", automation.Action)
PowerOnAction = midea_ns.class_("PowerOnAction", automation.Action)
PowerOffAction = midea_ns.class_("PowerOffAction", automation.Action)
MIDEA_ACTION_BASE_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.use_id(AirConditioner),
}
)
# FollowMe action
MIDEA_FOLLOW_ME_MIN = 0
MIDEA_FOLLOW_ME_MAX = 37
MIDEA_FOLLOW_ME_SCHEMA = cv.Schema(
{
cv.Required(CONF_TEMPERATURE): cv.templatable(cv.temperature),
cv.Optional(CONF_BEEPER, default=False): cv.templatable(cv.boolean),
}
)
@register_action("follow_me", FollowMeAction, MIDEA_FOLLOW_ME_SCHEMA)
async def follow_me_to_code(var, config, args):
template_ = await cg.templatable(config[CONF_BEEPER], args, cg.bool_)
cg.add(var.set_beeper(template_))
template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.float_)
cg.add(var.set_temperature(template_))
# Toggle Display action
@register_action(
"display_toggle",
DisplayToggleAction,
cv.Schema({}),
)
async def display_toggle_to_code(var, config, args):
pass
# Swing Step action
@register_action(
"swing_step",
SwingStepAction,
cv.Schema({}),
)
async def swing_step_to_code(var, config, args):
pass
# Beeper On action
@register_action(
"beeper_on",
BeeperOnAction,
cv.Schema({}),
)
async def beeper_on_to_code(var, config, args):
pass
# Beeper Off action
@register_action(
"beeper_off",
BeeperOffAction,
cv.Schema({}),
)
async def beeper_off_to_code(var, config, args):
pass
# Power On action
@register_action(
"power_on",
PowerOnAction,
cv.Schema({}),
)
async def power_on_to_code(var, config, args):
pass
# Power Off action
@register_action(
"power_off",
PowerOffAction,
cv.Schema({}),
)
async def power_off_to_code(var, config, args):
pass
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
await climate.register_climate(var, config)
cg.add(var.set_period(config[CONF_PERIOD].total_milliseconds))
cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds))
cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS]))
if CONF_TRANSMITTER_ID in config:
cg.add_define("USE_REMOTE_TRANSMITTER")
transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID])
cg.add(var.set_transmitter(transmitter_))
cg.add(var.set_beeper_feedback(config[CONF_BEEPER]))
cg.add(var.set_autoconf(config[CONF_AUTOCONF]))
if CONF_SUPPORTED_MODES in config:
cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
if CONF_SUPPORTED_SWING_MODES in config:
cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES]))
if CONF_SUPPORTED_PRESETS in config:
cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS]))
if CONF_CUSTOM_PRESETS in config:
cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS]))
if CONF_CUSTOM_FAN_MODES in config:
cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES]))
if CONF_OUTDOOR_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
cg.add(var.set_outdoor_temperature_sensor(sens))
if CONF_POWER_USAGE in config:
sens = await sensor.new_sensor(config[CONF_POWER_USAGE])
cg.add(var.set_power_sensor(sens))
if CONF_HUMIDITY_SETPOINT in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
cg.add(var.set_humidity_setpoint_sensor(sens))
cg.add_library("dudanov/MideaUART", "1.1.5")

View File

@ -0,0 +1,42 @@
#pragma once
#ifdef USE_REMOTE_TRANSMITTER
#include "esphome/components/remote_base/midea_protocol.h"
namespace esphome {
namespace midea {
using IrData = remote_base::MideaData;
class IrFollowMeData : public IrData {
public:
// Default constructor (temp: 30C, beeper: off)
IrFollowMeData() : IrData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {}
// Copy from Base
IrFollowMeData(const IrData &data) : IrData(data) {}
// Direct from temperature and beeper values
IrFollowMeData(uint8_t temp, bool beeper = false) : IrFollowMeData() {
this->set_temp(temp);
this->set_beeper(beeper);
}
/* TEMPERATURE */
uint8_t temp() const { return this->data_[4] - 1; }
void set_temp(uint8_t val) { this->data_[4] = std::min(MAX_TEMP, val) + 1; }
/* BEEPER */
bool beeper() const { return this->data_[3] & 128; }
void set_beeper(bool val) { this->set_value_(3, 1, 7, val); }
protected:
static const uint8_t MAX_TEMP = 37;
};
class IrSpecialData : public IrData {
public:
IrSpecialData(uint8_t code) : IrData({MIDEA_TYPE_SPECIAL, code, 0xFF, 0xFF, 0xFF}) {}
};
} // namespace midea
} // namespace esphome
#endif

View File

@ -1,115 +1,3 @@
from esphome.components import climate, sensor
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_CUSTOM_FAN_MODES,
CONF_CUSTOM_PRESETS,
CONF_ID,
CONF_PRESET_BOOST,
CONF_PRESET_ECO,
CONF_PRESET_SLEEP,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PERCENT,
UNIT_WATT,
ICON_THERMOMETER,
ICON_POWER,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
ICON_WATER_PERCENT,
DEVICE_CLASS_HUMIDITY,
)
from esphome.components.midea_dongle import CONF_MIDEA_DONGLE_ID, MideaDongle
AUTO_LOAD = ["climate", "sensor", "midea_dongle"]
CODEOWNERS = ["@dudanov"]
CONF_BEEPER = "beeper"
CONF_SWING_HORIZONTAL = "swing_horizontal"
CONF_SWING_BOTH = "swing_both"
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
CONF_POWER_USAGE = "power_usage"
CONF_HUMIDITY_SETPOINT = "humidity_setpoint"
midea_ac_ns = cg.esphome_ns.namespace("midea_ac")
MideaAC = midea_ac_ns.class_("MideaAC", climate.Climate, cg.Component)
CLIMATE_CUSTOM_FAN_MODES = {
"SILENT": "silent",
"TURBO": "turbo",
}
validate_climate_custom_fan_mode = cv.enum(CLIMATE_CUSTOM_FAN_MODES, upper=True)
CLIMATE_CUSTOM_PRESETS = {
"FREEZE_PROTECTION": "freeze protection",
}
validate_climate_custom_preset = cv.enum(CLIMATE_CUSTOM_PRESETS, upper=True)
CONFIG_SCHEMA = cv.All(
climate.CLIMATE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(MideaAC),
cv.GenerateID(CONF_MIDEA_DONGLE_ID): cv.use_id(MideaDongle),
cv.Optional(CONF_BEEPER, default=False): cv.boolean,
cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list(
validate_climate_custom_fan_mode
),
cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(
validate_climate_custom_preset
),
cv.Optional(CONF_SWING_HORIZONTAL, default=False): cv.boolean,
cv.Optional(CONF_SWING_BOTH, default=False): cv.boolean,
cv.Optional(CONF_PRESET_ECO, default=False): cv.boolean,
cv.Optional(CONF_PRESET_SLEEP, default=False): cv.boolean,
cv.Optional(CONF_PRESET_BOOST, default=False): cv.boolean,
cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
icon=ICON_POWER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
icon=ICON_WATER_PERCENT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
}
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await climate.register_climate(var, config)
paren = await cg.get_variable(config[CONF_MIDEA_DONGLE_ID])
cg.add(var.set_midea_dongle_parent(paren))
cg.add(var.set_beeper_feedback(config[CONF_BEEPER]))
if CONF_CUSTOM_FAN_MODES in config:
cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES]))
if CONF_CUSTOM_PRESETS in config:
cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS]))
cg.add(var.set_swing_horizontal(config[CONF_SWING_HORIZONTAL]))
cg.add(var.set_swing_both(config[CONF_SWING_BOTH]))
cg.add(var.set_preset_eco(config[CONF_PRESET_ECO]))
cg.add(var.set_preset_sleep(config[CONF_PRESET_SLEEP]))
cg.add(var.set_preset_boost(config[CONF_PRESET_BOOST]))
if CONF_OUTDOOR_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
cg.add(var.set_outdoor_temperature_sensor(sens))
if CONF_POWER_USAGE in config:
sens = await sensor.new_sensor(config[CONF_POWER_USAGE])
cg.add(var.set_power_sensor(sens))
if CONF_HUMIDITY_SETPOINT in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
cg.add(var.set_humidity_setpoint_sensor(sens))
CONFIG_SCHEMA = cv.invalid("This platform has been renamed to midea in 2021.9")

View File

@ -1,208 +0,0 @@
#include "esphome/core/log.h"
#include "midea_climate.h"
namespace esphome {
namespace midea_ac {
static const char *const TAG = "midea_ac";
static void set_sensor(sensor::Sensor *sensor, float value) {
if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value))
sensor->publish_state(value);
}
template<typename T> void set_property(T &property, T value, bool &flag) {
if (property != value) {
property = value;
flag = true;
}
}
void MideaAC::on_frame(const midea_dongle::Frame &frame) {
const auto p = frame.as<PropertiesFrame>();
if (p.has_power_info()) {
set_sensor(this->power_sensor_, p.get_power_usage());
return;
} else if (!p.has_properties()) {
ESP_LOGW(TAG, "RX: frame has unknown type");
return;
}
if (p.get_type() == midea_dongle::MideaMessageType::DEVICE_CONTROL) {
ESP_LOGD(TAG, "RX: control frame");
this->ctrl_request_ = false;
} else {
ESP_LOGD(TAG, "RX: query frame");
}
if (this->ctrl_request_)
return;
this->cmd_frame_.set_properties(p); // copy properties from response
bool need_publish = false;
set_property(this->mode, p.get_mode(), need_publish);
set_property(this->target_temperature, p.get_target_temp(), need_publish);
set_property(this->current_temperature, p.get_indoor_temp(), need_publish);
if (p.is_custom_fan_mode()) {
this->fan_mode.reset();
optional<std::string> mode = p.get_custom_fan_mode();
set_property(this->custom_fan_mode, mode, need_publish);
} else {
this->custom_fan_mode.reset();
optional<climate::ClimateFanMode> mode = p.get_fan_mode();
set_property(this->fan_mode, mode, need_publish);
}
set_property(this->swing_mode, p.get_swing_mode(), need_publish);
if (p.is_custom_preset()) {
this->preset.reset();
optional<std::string> preset = p.get_custom_preset();
set_property(this->custom_preset, preset, need_publish);
} else {
this->custom_preset.reset();
set_property(this->preset, p.get_preset(), need_publish);
}
if (need_publish)
this->publish_state();
set_sensor(this->outdoor_sensor_, p.get_outdoor_temp());
set_sensor(this->humidity_sensor_, p.get_humidity_setpoint());
}
void MideaAC::on_update() {
if (this->ctrl_request_) {
ESP_LOGD(TAG, "TX: control");
this->parent_->write_frame(this->cmd_frame_);
} else {
ESP_LOGD(TAG, "TX: query");
if (this->power_sensor_ == nullptr || this->request_num_++ % 32)
this->parent_->write_frame(this->query_frame_);
else
this->parent_->write_frame(this->power_frame_);
}
}
bool MideaAC::allow_preset(climate::ClimatePreset preset) const {
switch (preset) {
case climate::CLIMATE_PRESET_ECO:
if (this->mode == climate::CLIMATE_MODE_COOL) {
return true;
} else {
ESP_LOGD(TAG, "ECO preset is only available in COOL mode");
}
break;
case climate::CLIMATE_PRESET_SLEEP:
if (this->mode == climate::CLIMATE_MODE_FAN_ONLY || this->mode == climate::CLIMATE_MODE_DRY) {
ESP_LOGD(TAG, "SLEEP preset is not available in FAN_ONLY or DRY mode");
} else {
return true;
}
break;
case climate::CLIMATE_PRESET_BOOST:
if (this->mode == climate::CLIMATE_MODE_HEAT || this->mode == climate::CLIMATE_MODE_COOL) {
return true;
} else {
ESP_LOGD(TAG, "BOOST preset is only available in HEAT or COOL mode");
}
break;
case climate::CLIMATE_PRESET_NONE:
return true;
default:
break;
}
return false;
}
bool MideaAC::allow_custom_preset(const std::string &custom_preset) const {
if (custom_preset == MIDEA_FREEZE_PROTECTION_PRESET) {
if (this->mode == climate::CLIMATE_MODE_HEAT) {
return true;
} else {
ESP_LOGD(TAG, "%s is only available in HEAT mode", MIDEA_FREEZE_PROTECTION_PRESET.c_str());
}
}
return false;
}
void MideaAC::control(const climate::ClimateCall &call) {
if (call.get_mode().has_value() && call.get_mode().value() != this->mode) {
this->cmd_frame_.set_mode(call.get_mode().value());
this->ctrl_request_ = true;
}
if (call.get_target_temperature().has_value() && call.get_target_temperature().value() != this->target_temperature) {
this->cmd_frame_.set_target_temp(call.get_target_temperature().value());
this->ctrl_request_ = true;
}
if (call.get_fan_mode().has_value() &&
(!this->fan_mode.has_value() || this->fan_mode.value() != call.get_fan_mode().value())) {
this->custom_fan_mode.reset();
this->cmd_frame_.set_fan_mode(call.get_fan_mode().value());
this->ctrl_request_ = true;
}
if (call.get_custom_fan_mode().has_value() &&
(!this->custom_fan_mode.has_value() || this->custom_fan_mode.value() != call.get_custom_fan_mode().value())) {
this->fan_mode.reset();
this->cmd_frame_.set_custom_fan_mode(call.get_custom_fan_mode().value());
this->ctrl_request_ = true;
}
if (call.get_swing_mode().has_value() && call.get_swing_mode().value() != this->swing_mode) {
this->cmd_frame_.set_swing_mode(call.get_swing_mode().value());
this->ctrl_request_ = true;
}
if (call.get_preset().has_value() && this->allow_preset(call.get_preset().value()) &&
(!this->preset.has_value() || this->preset.value() != call.get_preset().value())) {
this->custom_preset.reset();
this->cmd_frame_.set_preset(call.get_preset().value());
this->ctrl_request_ = true;
}
if (call.get_custom_preset().has_value() && this->allow_custom_preset(call.get_custom_preset().value()) &&
(!this->custom_preset.has_value() || this->custom_preset.value() != call.get_custom_preset().value())) {
this->preset.reset();
this->cmd_frame_.set_custom_preset(call.get_custom_preset().value());
this->ctrl_request_ = true;
}
if (this->ctrl_request_) {
this->cmd_frame_.set_beeper_feedback(this->beeper_feedback_);
this->cmd_frame_.finalize();
}
}
climate::ClimateTraits MideaAC::traits() {
auto traits = climate::ClimateTraits();
traits.set_visual_min_temperature(17);
traits.set_visual_max_temperature(30);
traits.set_visual_temperature_step(0.5);
traits.set_supported_modes({
climate::CLIMATE_MODE_OFF,
climate::CLIMATE_MODE_HEAT_COOL,
climate::CLIMATE_MODE_COOL,
climate::CLIMATE_MODE_DRY,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_MODE_FAN_ONLY,
});
traits.set_supported_fan_modes({
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH,
});
traits.set_supported_custom_fan_modes(this->traits_custom_fan_modes_);
traits.set_supported_swing_modes({
climate::CLIMATE_SWING_OFF,
climate::CLIMATE_SWING_VERTICAL,
});
if (traits_swing_horizontal_)
traits.add_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL);
if (traits_swing_both_)
traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH);
traits.set_supported_presets({
climate::CLIMATE_PRESET_NONE,
});
if (traits_preset_eco_)
traits.add_supported_preset(climate::CLIMATE_PRESET_ECO);
if (traits_preset_sleep_)
traits.add_supported_preset(climate::CLIMATE_PRESET_SLEEP);
if (traits_preset_boost_)
traits.add_supported_preset(climate::CLIMATE_PRESET_BOOST);
traits.set_supported_custom_presets(this->traits_custom_presets_);
traits.set_supports_current_temperature(true);
return traits;
}
} // namespace midea_ac
} // namespace esphome

View File

@ -1,68 +0,0 @@
#pragma once
#include <utility>
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/midea_dongle/midea_dongle.h"
#include "esphome/components/climate/climate.h"
#include "esphome/components/midea_dongle/midea_dongle.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "midea_frame.h"
namespace esphome {
namespace midea_ac {
class MideaAC : public midea_dongle::MideaAppliance, public climate::Climate, public Component {
public:
float get_setup_priority() const override { return setup_priority::LATE; }
void on_frame(const midea_dongle::Frame &frame) override;
void on_update() override;
void setup() override { this->parent_->set_appliance(this); }
void set_midea_dongle_parent(midea_dongle::MideaDongle *parent) { this->parent_ = parent; }
void set_outdoor_temperature_sensor(sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; }
void set_humidity_setpoint_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; }
void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; }
void set_beeper_feedback(bool state) { this->beeper_feedback_ = state; }
void set_swing_horizontal(bool state) { this->traits_swing_horizontal_ = state; }
void set_swing_both(bool state) { this->traits_swing_both_ = state; }
void set_preset_eco(bool state) { this->traits_preset_eco_ = state; }
void set_preset_sleep(bool state) { this->traits_preset_sleep_ = state; }
void set_preset_boost(bool state) { this->traits_preset_boost_ = state; }
bool allow_preset(climate::ClimatePreset preset) const;
void set_custom_fan_modes(std::set<std::string> custom_fan_modes) {
this->traits_custom_fan_modes_ = std::move(custom_fan_modes);
}
void set_custom_presets(std::set<std::string> custom_presets) {
this->traits_custom_presets_ = std::move(custom_presets);
}
bool allow_custom_preset(const std::string &custom_preset) const;
protected:
/// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override;
/// Return the traits of this controller.
climate::ClimateTraits traits() override;
const QueryFrame query_frame_;
const PowerQueryFrame power_frame_;
CommandFrame cmd_frame_;
midea_dongle::MideaDongle *parent_{nullptr};
sensor::Sensor *outdoor_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr};
uint8_t request_num_{0};
bool ctrl_request_{false};
bool beeper_feedback_{false};
bool traits_swing_horizontal_{false};
bool traits_swing_both_{false};
bool traits_preset_eco_{false};
bool traits_preset_sleep_{false};
bool traits_preset_boost_{false};
std::set<std::string> traits_custom_fan_modes_{{}};
std::set<std::string> traits_custom_presets_{{}};
};
} // namespace midea_ac
} // namespace esphome

View File

@ -1,238 +0,0 @@
#include "midea_frame.h"
namespace esphome {
namespace midea_ac {
static const char *const TAG = "midea_ac";
const std::string MIDEA_SILENT_FAN_MODE = "silent";
const std::string MIDEA_TURBO_FAN_MODE = "turbo";
const std::string MIDEA_FREEZE_PROTECTION_PRESET = "freeze protection";
const uint8_t QueryFrame::INIT[] = {0xAA, 0x21, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x81,
0x00, 0xFF, 0x03, 0xFF, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x37, 0x31};
const uint8_t PowerQueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x21,
0x01, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x17, 0x6A};
const uint8_t CommandFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x40, 0x00,
0x00, 0x00, 0x7F, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
float PropertiesFrame::get_target_temp() const {
float temp = static_cast<float>((this->pbuf_[12] & 0x0F) + 16);
if (this->pbuf_[12] & 0x10)
temp += 0.5;
return temp;
}
void PropertiesFrame::set_target_temp(float temp) {
uint8_t tmp = static_cast<uint8_t>(temp * 16.0) + 4;
tmp = ((tmp & 8) << 1) | (tmp >> 4);
this->pbuf_[12] &= ~0x1F;
this->pbuf_[12] |= tmp;
}
static float i16tof(int16_t in) { return static_cast<float>(in - 50) / 2.0; }
float PropertiesFrame::get_indoor_temp() const { return i16tof(this->pbuf_[21]); }
float PropertiesFrame::get_outdoor_temp() const { return i16tof(this->pbuf_[22]); }
float PropertiesFrame::get_humidity_setpoint() const { return static_cast<float>(this->pbuf_[29] & 0x7F); }
climate::ClimateMode PropertiesFrame::get_mode() const {
if (!this->get_power_())
return climate::CLIMATE_MODE_OFF;
switch (this->pbuf_[12] >> 5) {
case MIDEA_MODE_AUTO:
return climate::CLIMATE_MODE_HEAT_COOL;
case MIDEA_MODE_COOL:
return climate::CLIMATE_MODE_COOL;
case MIDEA_MODE_DRY:
return climate::CLIMATE_MODE_DRY;
case MIDEA_MODE_HEAT:
return climate::CLIMATE_MODE_HEAT;
case MIDEA_MODE_FAN_ONLY:
return climate::CLIMATE_MODE_FAN_ONLY;
default:
return climate::CLIMATE_MODE_OFF;
}
}
void PropertiesFrame::set_mode(climate::ClimateMode mode) {
uint8_t m;
switch (mode) {
case climate::CLIMATE_MODE_HEAT_COOL:
m = MIDEA_MODE_AUTO;
break;
case climate::CLIMATE_MODE_COOL:
m = MIDEA_MODE_COOL;
break;
case climate::CLIMATE_MODE_DRY:
m = MIDEA_MODE_DRY;
break;
case climate::CLIMATE_MODE_HEAT:
m = MIDEA_MODE_HEAT;
break;
case climate::CLIMATE_MODE_FAN_ONLY:
m = MIDEA_MODE_FAN_ONLY;
break;
default:
this->set_power_(false);
return;
}
this->set_power_(true);
this->pbuf_[12] &= ~0xE0;
this->pbuf_[12] |= m << 5;
}
optional<climate::ClimatePreset> PropertiesFrame::get_preset() const {
if (this->get_eco_mode())
return climate::CLIMATE_PRESET_ECO;
if (this->get_sleep_mode())
return climate::CLIMATE_PRESET_SLEEP;
if (this->get_turbo_mode())
return climate::CLIMATE_PRESET_BOOST;
return climate::CLIMATE_PRESET_NONE;
}
void PropertiesFrame::set_preset(climate::ClimatePreset preset) {
this->clear_presets();
switch (preset) {
case climate::CLIMATE_PRESET_ECO:
this->set_eco_mode(true);
break;
case climate::CLIMATE_PRESET_SLEEP:
this->set_sleep_mode(true);
break;
case climate::CLIMATE_PRESET_BOOST:
this->set_turbo_mode(true);
break;
default:
break;
}
}
void PropertiesFrame::clear_presets() {
this->set_eco_mode(false);
this->set_sleep_mode(false);
this->set_turbo_mode(false);
this->set_freeze_protection_mode(false);
}
bool PropertiesFrame::is_custom_preset() const { return this->get_freeze_protection_mode(); }
const std::string &PropertiesFrame::get_custom_preset() const { return midea_ac::MIDEA_FREEZE_PROTECTION_PRESET; };
void PropertiesFrame::set_custom_preset(const std::string &preset) {
this->clear_presets();
if (preset == MIDEA_FREEZE_PROTECTION_PRESET)
this->set_freeze_protection_mode(true);
}
bool PropertiesFrame::is_custom_fan_mode() const {
switch (this->pbuf_[13]) {
case MIDEA_FAN_SILENT:
case MIDEA_FAN_TURBO:
return true;
default:
return false;
}
}
climate::ClimateFanMode PropertiesFrame::get_fan_mode() const {
switch (this->pbuf_[13]) {
case MIDEA_FAN_LOW:
return climate::CLIMATE_FAN_LOW;
case MIDEA_FAN_MEDIUM:
return climate::CLIMATE_FAN_MEDIUM;
case MIDEA_FAN_HIGH:
return climate::CLIMATE_FAN_HIGH;
default:
return climate::CLIMATE_FAN_AUTO;
}
}
void PropertiesFrame::set_fan_mode(climate::ClimateFanMode mode) {
uint8_t m;
switch (mode) {
case climate::CLIMATE_FAN_LOW:
m = MIDEA_FAN_LOW;
break;
case climate::CLIMATE_FAN_MEDIUM:
m = MIDEA_FAN_MEDIUM;
break;
case climate::CLIMATE_FAN_HIGH:
m = MIDEA_FAN_HIGH;
break;
default:
m = MIDEA_FAN_AUTO;
break;
}
this->pbuf_[13] = m;
}
const std::string &PropertiesFrame::get_custom_fan_mode() const {
switch (this->pbuf_[13]) {
case MIDEA_FAN_SILENT:
return MIDEA_SILENT_FAN_MODE;
default:
return MIDEA_TURBO_FAN_MODE;
}
}
void PropertiesFrame::set_custom_fan_mode(const std::string &mode) {
uint8_t m;
if (mode == MIDEA_SILENT_FAN_MODE) {
m = MIDEA_FAN_SILENT;
} else {
m = MIDEA_FAN_TURBO;
}
this->pbuf_[13] = m;
}
climate::ClimateSwingMode PropertiesFrame::get_swing_mode() const {
switch (this->pbuf_[17] & 0x0F) {
case MIDEA_SWING_VERTICAL:
return climate::CLIMATE_SWING_VERTICAL;
case MIDEA_SWING_HORIZONTAL:
return climate::CLIMATE_SWING_HORIZONTAL;
case MIDEA_SWING_BOTH:
return climate::CLIMATE_SWING_BOTH;
default:
return climate::CLIMATE_SWING_OFF;
}
}
void PropertiesFrame::set_swing_mode(climate::ClimateSwingMode mode) {
uint8_t m;
switch (mode) {
case climate::CLIMATE_SWING_VERTICAL:
m = MIDEA_SWING_VERTICAL;
break;
case climate::CLIMATE_SWING_HORIZONTAL:
m = MIDEA_SWING_HORIZONTAL;
break;
case climate::CLIMATE_SWING_BOTH:
m = MIDEA_SWING_BOTH;
break;
default:
m = MIDEA_SWING_OFF;
break;
}
this->pbuf_[17] = 0x30 | m;
}
float PropertiesFrame::get_power_usage() const {
uint32_t power = 0;
const uint8_t *ptr = this->pbuf_ + 28;
for (uint32_t weight = 1;; weight *= 10, ptr--) {
power += (*ptr % 16) * weight;
weight *= 10;
power += (*ptr / 16) * weight;
if (weight == 100000)
return static_cast<float>(power) * 0.1;
}
}
} // namespace midea_ac
} // namespace esphome

View File

@ -1,165 +0,0 @@
#pragma once
#include "esphome/components/climate/climate.h"
#include "esphome/components/midea_dongle/midea_frame.h"
namespace esphome {
namespace midea_ac {
extern const std::string MIDEA_SILENT_FAN_MODE;
extern const std::string MIDEA_TURBO_FAN_MODE;
extern const std::string MIDEA_FREEZE_PROTECTION_PRESET;
/// Enum for all modes a Midea device can be in.
enum MideaMode : uint8_t {
/// The Midea device is set to automatically change the heating/cooling cycle
MIDEA_MODE_AUTO = 1,
/// The Midea device is manually set to cool mode (not in auto mode!)
MIDEA_MODE_COOL = 2,
/// The Midea device is manually set to dry mode
MIDEA_MODE_DRY = 3,
/// The Midea device is manually set to heat mode (not in auto mode!)
MIDEA_MODE_HEAT = 4,
/// The Midea device is manually set to fan only mode
MIDEA_MODE_FAN_ONLY = 5,
};
/// Enum for all modes a Midea fan can be in
enum MideaFanMode : uint8_t {
/// The fan mode is set to Auto
MIDEA_FAN_AUTO = 102,
/// The fan mode is set to Silent
MIDEA_FAN_SILENT = 20,
/// The fan mode is set to Low
MIDEA_FAN_LOW = 40,
/// The fan mode is set to Medium
MIDEA_FAN_MEDIUM = 60,
/// The fan mode is set to High
MIDEA_FAN_HIGH = 80,
/// The fan mode is set to Turbo
MIDEA_FAN_TURBO = 100,
};
/// Enum for all modes a Midea swing can be in
enum MideaSwingMode : uint8_t {
/// The sing mode is set to Off
MIDEA_SWING_OFF = 0b0000,
/// The fan mode is set to Both
MIDEA_SWING_BOTH = 0b1111,
/// The fan mode is set to Vertical
MIDEA_SWING_VERTICAL = 0b1100,
/// The fan mode is set to Horizontal
MIDEA_SWING_HORIZONTAL = 0b0011,
};
class PropertiesFrame : public midea_dongle::BaseFrame {
public:
PropertiesFrame() = delete;
PropertiesFrame(uint8_t *data) : BaseFrame(data) {}
PropertiesFrame(const Frame &frame) : BaseFrame(frame) {}
bool has_properties() const {
return this->has_response_type(0xC0) && (this->has_type(0x03) || this->has_type(0x02));
}
bool has_power_info() const { return this->has_response_type(0xC1); }
/* TARGET TEMPERATURE */
float get_target_temp() const;
void set_target_temp(float temp);
/* MODE */
climate::ClimateMode get_mode() const;
void set_mode(climate::ClimateMode mode);
/* FAN SPEED */
bool is_custom_fan_mode() const;
climate::ClimateFanMode get_fan_mode() const;
void set_fan_mode(climate::ClimateFanMode mode);
const std::string &get_custom_fan_mode() const;
void set_custom_fan_mode(const std::string &mode);
/* SWING MODE */
climate::ClimateSwingMode get_swing_mode() const;
void set_swing_mode(climate::ClimateSwingMode mode);
/* INDOOR TEMPERATURE */
float get_indoor_temp() const;
/* OUTDOOR TEMPERATURE */
float get_outdoor_temp() const;
/* HUMIDITY SETPOINT */
float get_humidity_setpoint() const;
/* ECO MODE */
bool get_eco_mode() const { return this->pbuf_[19] & 0x10; }
void set_eco_mode(bool state) { this->set_bytemask_(19, 0x80, state); }
/* SLEEP MODE */
bool get_sleep_mode() const { return this->pbuf_[20] & 0x01; }
void set_sleep_mode(bool state) { this->set_bytemask_(20, 0x01, state); }
/* TURBO MODE */
bool get_turbo_mode() const { return this->pbuf_[18] & 0x20 || this->pbuf_[20] & 0x02; }
void set_turbo_mode(bool state) {
this->set_bytemask_(18, 0x20, state);
this->set_bytemask_(20, 0x02, state);
}
/* FREEZE PROTECTION */
bool get_freeze_protection_mode() const { return this->pbuf_[31] & 0x80; }
void set_freeze_protection_mode(bool state) { this->set_bytemask_(31, 0x80, state); }
/* PRESET */
optional<climate::ClimatePreset> get_preset() const;
void set_preset(climate::ClimatePreset preset);
void clear_presets();
bool is_custom_preset() const;
const std::string &get_custom_preset() const;
void set_custom_preset(const std::string &preset);
/* POWER USAGE */
float get_power_usage() const;
/// Set properties from another frame
void set_properties(const PropertiesFrame &p) { memcpy(this->pbuf_ + 11, p.data() + 11, 10); }
protected:
/* POWER */
bool get_power_() const { return this->pbuf_[11] & 0x01; }
void set_power_(bool state) { this->set_bytemask_(11, 0x01, state); }
};
// Query state frame (read-only)
class QueryFrame : public midea_dongle::StaticFrame<midea_dongle::Frame> {
public:
QueryFrame() : StaticFrame(FPSTR(this->INIT)) {}
private:
static const uint8_t PROGMEM INIT[];
};
// Power query state frame (read-only)
class PowerQueryFrame : public midea_dongle::StaticFrame<midea_dongle::Frame> {
public:
PowerQueryFrame() : StaticFrame(FPSTR(this->INIT)) {}
private:
static const uint8_t PROGMEM INIT[];
};
// Command frame
class CommandFrame : public midea_dongle::StaticFrame<PropertiesFrame> {
public:
CommandFrame() : StaticFrame(FPSTR(this->INIT)) {}
void set_beeper_feedback(bool state) { this->set_bytemask_(11, 0x40, state); }
private:
static const uint8_t PROGMEM INIT[];
};
} // namespace midea_ac
} // namespace esphome

View File

@ -1,30 +0,0 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart
from esphome.const import CONF_ID
DEPENDENCIES = ["wifi", "uart"]
CODEOWNERS = ["@dudanov"]
midea_dongle_ns = cg.esphome_ns.namespace("midea_dongle")
MideaDongle = midea_dongle_ns.class_("MideaDongle", cg.Component, uart.UARTDevice)
CONF_MIDEA_DONGLE_ID = "midea_dongle_id"
CONF_STRENGTH_ICON = "strength_icon"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(MideaDongle),
cv.Optional(CONF_STRENGTH_ICON, default=False): cv.boolean,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
cg.add(var.use_strength_icon(config[CONF_STRENGTH_ICON]))

View File

@ -1,98 +0,0 @@
#include "midea_dongle.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace midea_dongle {
static const char *const TAG = "midea_dongle";
void MideaDongle::loop() {
while (this->available()) {
const uint8_t rx = this->read();
if (this->idx_ <= OFFSET_LENGTH) {
if (this->idx_ == OFFSET_LENGTH) {
if (rx <= OFFSET_BODY || rx >= sizeof(this->buf_)) {
this->reset_();
continue;
}
this->cnt_ = rx;
} else if (rx != SYNC_BYTE) {
continue;
}
}
this->buf_[this->idx_++] = rx;
if (--this->cnt_)
continue;
this->reset_();
const BaseFrame frame(this->buf_);
ESP_LOGD(TAG, "RX: %s", frame.to_string().c_str());
if (!frame.is_valid()) {
ESP_LOGW(TAG, "RX: frame check failed!");
continue;
}
if (frame.get_type() == QUERY_NETWORK) {
this->notify_.set_type(QUERY_NETWORK);
this->need_notify_ = true;
continue;
}
if (this->appliance_ != nullptr)
this->appliance_->on_frame(frame);
}
}
void MideaDongle::update() {
const bool is_conn = WiFi.isConnected();
uint8_t wifi_strength = 0;
if (!this->rssi_timer_) {
if (is_conn)
wifi_strength = 4;
} else if (is_conn) {
if (--this->rssi_timer_) {
wifi_strength = this->notify_.get_signal_strength();
} else {
this->rssi_timer_ = 60;
const int32_t dbm = WiFi.RSSI();
if (dbm > -63)
wifi_strength = 4;
else if (dbm > -75)
wifi_strength = 3;
else if (dbm > -88)
wifi_strength = 2;
else if (dbm > -100)
wifi_strength = 1;
}
} else {
this->rssi_timer_ = 1;
}
if (this->notify_.is_connected() != is_conn) {
this->notify_.set_connected(is_conn);
this->need_notify_ = true;
}
if (this->notify_.get_signal_strength() != wifi_strength) {
this->notify_.set_signal_strength(wifi_strength);
this->need_notify_ = true;
}
if (!--this->notify_timer_) {
this->notify_.set_type(NETWORK_NOTIFY);
this->need_notify_ = true;
}
if (this->need_notify_) {
ESP_LOGD(TAG, "TX: notify WiFi STA %s, signal strength %d", is_conn ? "connected" : "not connected", wifi_strength);
this->need_notify_ = false;
this->notify_timer_ = 600;
this->notify_.finalize();
this->write_frame(this->notify_);
return;
}
if (this->appliance_ != nullptr)
this->appliance_->on_update();
}
void MideaDongle::write_frame(const Frame &frame) {
this->write_array(frame.data(), frame.size());
ESP_LOGD(TAG, "TX: %s", frame.to_string().c_str());
}
} // namespace midea_dongle
} // namespace esphome

View File

@ -1,56 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/wifi/wifi_component.h"
#include "esphome/components/uart/uart.h"
#include "midea_frame.h"
namespace esphome {
namespace midea_dongle {
enum MideaApplianceType : uint8_t { DEHUMIDIFIER = 0xA1, AIR_CONDITIONER = 0xAC, BROADCAST = 0xFF };
enum MideaMessageType : uint8_t {
DEVICE_CONTROL = 0x02,
DEVICE_QUERY = 0x03,
NETWORK_NOTIFY = 0x0D,
QUERY_NETWORK = 0x63,
};
struct MideaAppliance {
/// Calling on update event
virtual void on_update() = 0;
/// Calling on frame receive event
virtual void on_frame(const Frame &frame) = 0;
};
class MideaDongle : public PollingComponent, public uart::UARTDevice {
public:
MideaDongle() : PollingComponent(1000) {}
float get_setup_priority() const override { return setup_priority::LATE; }
void update() override;
void loop() override;
void set_appliance(MideaAppliance *app) { this->appliance_ = app; }
void use_strength_icon(bool state) { this->rssi_timer_ = state; }
void write_frame(const Frame &frame);
protected:
MideaAppliance *appliance_{nullptr};
NotifyFrame notify_;
unsigned notify_timer_{1};
// Buffer
uint8_t buf_[36];
// Index
uint8_t idx_{0};
// Reverse receive counter
uint8_t cnt_{2};
uint8_t rssi_timer_{0};
bool need_notify_{false};
// Reset receiver state
void reset_() {
this->idx_ = 0;
this->cnt_ = 2;
}
};
} // namespace midea_dongle
} // namespace esphome

View File

@ -1,95 +0,0 @@
#include "midea_frame.h"
namespace esphome {
namespace midea_dongle {
const uint8_t BaseFrame::CRC_TABLE[] = {
0x00, 0x5E, 0xBC, 0xE2, 0x61, 0x3F, 0xDD, 0x83, 0xC2, 0x9C, 0x7E, 0x20, 0xA3, 0xFD, 0x1F, 0x41, 0x9D, 0xC3, 0x21,
0x7F, 0xFC, 0xA2, 0x40, 0x1E, 0x5F, 0x01, 0xE3, 0xBD, 0x3E, 0x60, 0x82, 0xDC, 0x23, 0x7D, 0x9F, 0xC1, 0x42, 0x1C,
0xFE, 0xA0, 0xE1, 0xBF, 0x5D, 0x03, 0x80, 0xDE, 0x3C, 0x62, 0xBE, 0xE0, 0x02, 0x5C, 0xDF, 0x81, 0x63, 0x3D, 0x7C,
0x22, 0xC0, 0x9E, 0x1D, 0x43, 0xA1, 0xFF, 0x46, 0x18, 0xFA, 0xA4, 0x27, 0x79, 0x9B, 0xC5, 0x84, 0xDA, 0x38, 0x66,
0xE5, 0xBB, 0x59, 0x07, 0xDB, 0x85, 0x67, 0x39, 0xBA, 0xE4, 0x06, 0x58, 0x19, 0x47, 0xA5, 0xFB, 0x78, 0x26, 0xC4,
0x9A, 0x65, 0x3B, 0xD9, 0x87, 0x04, 0x5A, 0xB8, 0xE6, 0xA7, 0xF9, 0x1B, 0x45, 0xC6, 0x98, 0x7A, 0x24, 0xF8, 0xA6,
0x44, 0x1A, 0x99, 0xC7, 0x25, 0x7B, 0x3A, 0x64, 0x86, 0xD8, 0x5B, 0x05, 0xE7, 0xB9, 0x8C, 0xD2, 0x30, 0x6E, 0xED,
0xB3, 0x51, 0x0F, 0x4E, 0x10, 0xF2, 0xAC, 0x2F, 0x71, 0x93, 0xCD, 0x11, 0x4F, 0xAD, 0xF3, 0x70, 0x2E, 0xCC, 0x92,
0xD3, 0x8D, 0x6F, 0x31, 0xB2, 0xEC, 0x0E, 0x50, 0xAF, 0xF1, 0x13, 0x4D, 0xCE, 0x90, 0x72, 0x2C, 0x6D, 0x33, 0xD1,
0x8F, 0x0C, 0x52, 0xB0, 0xEE, 0x32, 0x6C, 0x8E, 0xD0, 0x53, 0x0D, 0xEF, 0xB1, 0xF0, 0xAE, 0x4C, 0x12, 0x91, 0xCF,
0x2D, 0x73, 0xCA, 0x94, 0x76, 0x28, 0xAB, 0xF5, 0x17, 0x49, 0x08, 0x56, 0xB4, 0xEA, 0x69, 0x37, 0xD5, 0x8B, 0x57,
0x09, 0xEB, 0xB5, 0x36, 0x68, 0x8A, 0xD4, 0x95, 0xCB, 0x29, 0x77, 0xF4, 0xAA, 0x48, 0x16, 0xE9, 0xB7, 0x55, 0x0B,
0x88, 0xD6, 0x34, 0x6A, 0x2B, 0x75, 0x97, 0xC9, 0x4A, 0x14, 0xF6, 0xA8, 0x74, 0x2A, 0xC8, 0x96, 0x15, 0x4B, 0xA9,
0xF7, 0xB6, 0xE8, 0x0A, 0x54, 0xD7, 0x89, 0x6B, 0x35};
const uint8_t NotifyFrame::INIT[] = {0xAA, 0x1F, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x0D, 0x01,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
bool BaseFrame::is_valid() const { return /*this->has_valid_crc_() &&*/ this->has_valid_cs_(); }
void BaseFrame::finalize() {
this->update_crc_();
this->update_cs_();
}
void BaseFrame::update_crc_() {
uint8_t crc = 0;
uint8_t *ptr = this->pbuf_ + OFFSET_BODY;
uint8_t len = this->length_() - OFFSET_BODY;
while (--len)
crc = pgm_read_byte(BaseFrame::CRC_TABLE + (crc ^ *ptr++));
*ptr = crc;
}
void BaseFrame::update_cs_() {
uint8_t cs = 0;
uint8_t *ptr = this->pbuf_ + OFFSET_LENGTH;
uint8_t len = this->length_();
while (--len)
cs -= *ptr++;
*ptr = cs;
}
bool BaseFrame::has_valid_crc_() const {
uint8_t crc = 0;
uint8_t len = this->length_() - OFFSET_BODY;
const uint8_t *ptr = this->pbuf_ + OFFSET_BODY;
for (; len; ptr++, len--)
crc = pgm_read_byte(BaseFrame::CRC_TABLE + (crc ^ *ptr));
return !crc;
}
bool BaseFrame::has_valid_cs_() const {
uint8_t cs = 0;
uint8_t len = this->length_();
const uint8_t *ptr = this->pbuf_ + OFFSET_LENGTH;
for (; len; ptr++, len--)
cs -= *ptr;
return !cs;
}
void BaseFrame::set_bytemask_(uint8_t idx, uint8_t mask, bool state) {
uint8_t *dst = this->pbuf_ + idx;
if (state)
*dst |= mask;
else
*dst &= ~mask;
}
static char u4hex(uint8_t num) { return num + ((num < 10) ? '0' : ('A' - 10)); }
String Frame::to_string() const {
String ret;
char buf[4];
buf[2] = ' ';
buf[3] = '\0';
ret.reserve(3 * 36);
const uint8_t *it = this->data();
for (size_t i = 0; i < this->size(); i++, it++) {
buf[0] = u4hex(*it >> 4);
buf[1] = u4hex(*it & 15);
ret.concat(buf);
}
return ret;
}
} // namespace midea_dongle
} // namespace esphome

View File

@ -1,104 +0,0 @@
#pragma once
#include "esphome/core/component.h"
namespace esphome {
namespace midea_dongle {
static const uint8_t OFFSET_START = 0;
static const uint8_t OFFSET_LENGTH = 1;
static const uint8_t OFFSET_APPTYPE = 2;
static const uint8_t OFFSET_BODY = 10;
static const uint8_t SYNC_BYTE = 0xAA;
class Frame {
public:
Frame() = delete;
Frame(uint8_t *data) : pbuf_(data) {}
Frame(const Frame &frame) : pbuf_(frame.data()) {}
// Frame buffer
uint8_t *data() const { return this->pbuf_; }
// Frame size
uint8_t size() const { return this->length_() + OFFSET_LENGTH; }
uint8_t app_type() const { return this->pbuf_[OFFSET_APPTYPE]; }
template<typename T> typename std::enable_if<std::is_base_of<Frame, T>::value, T>::type as() const {
return T(*this);
}
String to_string() const;
protected:
uint8_t *pbuf_;
uint8_t length_() const { return this->pbuf_[OFFSET_LENGTH]; }
};
class BaseFrame : public Frame {
public:
BaseFrame() = delete;
BaseFrame(uint8_t *data) : Frame(data) {}
BaseFrame(const Frame &frame) : Frame(frame) {}
// Check for valid
bool is_valid() const;
// Prepare for sending to device
void finalize();
uint8_t get_type() const { return this->pbuf_[9]; }
void set_type(uint8_t value) { this->pbuf_[9] = value; }
bool has_response_type(uint8_t type) const { return this->resp_type_() == type; }
bool has_type(uint8_t type) const { return this->get_type() == type; }
protected:
static const uint8_t PROGMEM CRC_TABLE[256];
void set_bytemask_(uint8_t idx, uint8_t mask, bool state);
uint8_t resp_type_() const { return this->pbuf_[OFFSET_BODY]; }
bool has_valid_crc_() const;
bool has_valid_cs_() const;
void update_crc_();
void update_cs_();
};
template<typename T = Frame, size_t buf_size = 36> class StaticFrame : public T {
public:
// Default constructor
StaticFrame() : T(this->buf_) {}
// Copy constructor
StaticFrame(const Frame &src) : T(this->buf_) {
if (src.length_() < sizeof(this->buf_)) {
memcpy(this->buf_, src.data(), src.length_() + OFFSET_LENGTH);
}
}
// Constructor for RAM data
StaticFrame(const uint8_t *src) : T(this->buf_) {
const uint8_t len = src[OFFSET_LENGTH];
if (len < sizeof(this->buf_)) {
memcpy(this->buf_, src, len + OFFSET_LENGTH);
}
}
// Constructor for PROGMEM data
StaticFrame(const __FlashStringHelper *pgm) : T(this->buf_) {
const uint8_t *src = reinterpret_cast<decltype(src)>(pgm);
const uint8_t len = pgm_read_byte(src + OFFSET_LENGTH);
if (len < sizeof(this->buf_)) {
memcpy_P(this->buf_, src, len + OFFSET_LENGTH);
}
}
protected:
uint8_t buf_[buf_size];
};
// Device network notification frame
class NotifyFrame : public midea_dongle::StaticFrame<BaseFrame, 32> {
public:
NotifyFrame() : StaticFrame(FPSTR(NotifyFrame::INIT)) {}
void set_signal_strength(uint8_t value) { this->pbuf_[12] = value; }
uint8_t get_signal_strength() const { return this->pbuf_[12]; }
void set_connected(bool state) { this->pbuf_[18] = state ? 0 : 1; }
bool is_connected() const { return !this->pbuf_[18]; }
private:
static const uint8_t PROGMEM INIT[];
};
} // namespace midea_dongle
} // namespace esphome

View File

@ -102,9 +102,7 @@ bool MQTTComponent::send_discovery_() {
device_info["identifiers"] = get_mac_address();
device_info["name"] = node_name;
device_info["sw_version"] = "esphome v" ESPHOME_VERSION " " + App.get_compilation_time();
#ifdef ARDUINO_BOARD
device_info["model"] = ARDUINO_BOARD;
#endif
device_info["model"] = ESPHOME_BOARD;
device_info["manufacturer"] = "espressif";
},
0, discovery_info.retain);

View File

@ -65,7 +65,9 @@ void MQTTFanComponent::setup() {
if (this->state_->get_traits().supports_speed()) {
this->subscribe(this->get_speed_command_topic(), [this](const std::string &topic, const std::string &payload) {
this->state_->make_call().set_speed(payload.c_str()).perform();
this->state_->make_call()
.set_speed(payload.c_str()) // NOLINT(clang-diagnostic-deprecated-declarations)
.perform();
});
}
@ -98,17 +100,18 @@ bool MQTTFanComponent::publish_state() {
auto traits = this->state_->get_traits();
if (traits.supports_speed()) {
const char *payload;
// NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations)
switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) {
case FAN_SPEED_LOW: {
case FAN_SPEED_LOW: { // NOLINT(clang-diagnostic-deprecated-declarations)
payload = "low";
break;
}
case FAN_SPEED_MEDIUM: {
case FAN_SPEED_MEDIUM: { // NOLINT(clang-diagnostic-deprecated-declarations)
payload = "medium";
break;
}
default:
case FAN_SPEED_HIGH: {
case FAN_SPEED_HIGH: { // NOLINT(clang-diagnostic-deprecated-declarations)
payload = "high";
break;
}

View File

@ -38,18 +38,23 @@ void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscover
root["color_mode"] = true;
JsonArray &color_modes = root.createNestedArray("supported_color_modes");
if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE))
if (traits.supports_color_mode(ColorMode::ON_OFF))
color_modes.add("onoff");
if (traits.supports_color_mode(ColorMode::BRIGHTNESS))
color_modes.add("brightness");
if (traits.supports_color_mode(ColorMode::WHITE))
color_modes.add("white");
if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) ||
traits.supports_color_mode(ColorMode::COLD_WARM_WHITE))
color_modes.add("color_temp");
if (traits.supports_color_mode(ColorMode::RGB))
color_modes.add("rgb");
if (traits.supports_color_mode(ColorMode::RGB_WHITE))
if (traits.supports_color_mode(ColorMode::RGB_WHITE) ||
// HA doesn't support RGBCT, and there's no CWWW->CT emulation in ESPHome yet, so ignore CT control for now
traits.supports_color_mode(ColorMode::RGB_COLOR_TEMPERATURE))
color_modes.add("rgbw");
if (traits.supports_color_mode(ColorMode::RGB_COLD_WARM_WHITE))
color_modes.add("rgbww");
if (traits.supports_color_mode(ColorMode::BRIGHTNESS))
color_modes.add("brightness");
if (traits.supports_color_mode(ColorMode::ON_OFF))
color_modes.add("onoff");
// legacy API
if (traits.supports_color_capability(ColorCapability::BRIGHTNESS))

View File

@ -61,8 +61,8 @@ void MQTTSensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryCo
if (this->sensor_->get_force_update())
root["force_update"] = true;
if (this->sensor_->state_class == sensor::STATE_CLASS_MEASUREMENT)
root["state_class"] = "measurement";
if (this->sensor_->state_class != STATE_CLASS_NONE)
root["state_class"] = state_class_to_string(this->sensor_->state_class);
config.command_topic = false;
}

View File

@ -1,13 +1,14 @@
#pragma once
#include "esphome/core/macros.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/color.h"
#include "esphome/components/light/light_output.h"
#include "esphome/components/light/addressable_light.h"
#ifdef ARDUINO_ESP8266_RELEASE_2_3_0
#error The NeoPixelBus library requires at least arduino_core_version 2.4.x
#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0)
#error The NeoPixelBus library requires at least arduino_version 2.4.x
#endif
#include "NeoPixelBus.h"
@ -82,10 +83,7 @@ class NeoPixelBusLightOutputBase : public light::AddressableLight {
this->controller_->Begin();
}
void loop() override {
if (!this->should_show_())
return;
void write_state(light::LightState *state) override {
this->mark_shown_();
this->controller_->Dirty();

View File

@ -178,28 +178,29 @@ void OTAComponent::handle_() {
#endif
if (!Update.begin(ota_size, U_FLASH)) {
uint8_t error = Update.getError();
StreamString ss;
Update.printError(ss);
#ifdef ARDUINO_ARCH_ESP8266
if (ss.indexOf("Invalid bootstrapping") != -1) {
if (error == UPDATE_ERROR_BOOTSTRAP) {
error_code = OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING;
goto error;
}
if (ss.indexOf("new Flash config wrong") != -1 || ss.indexOf("new Flash config wsong") != -1) {
if (error == UPDATE_ERROR_NEW_FLASH_CONFIG) {
error_code = OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG;
goto error;
}
if (ss.indexOf("Flash config wrong real") != -1 || ss.indexOf("Flash config wsong real") != -1) {
if (error == UPDATE_ERROR_FLASH_CONFIG) {
error_code = OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG;
goto error;
}
if (ss.indexOf("Not Enough Space") != -1) {
if (error == UPDATE_ERROR_SPACE) {
error_code = OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE;
goto error;
}
#endif
#ifdef ARDUINO_ARCH_ESP32
if (ss.indexOf("Bad Size Given") != -1) {
if (error == UPDATE_ERROR_SIZE) {
error_code = OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE;
goto error;
}

View File

@ -1,6 +1,19 @@
import re
from pathlib import Path
from esphome.core import EsphomeError
from esphome import git, yaml_util
from esphome.const import (
CONF_FILE,
CONF_FILES,
CONF_PACKAGES,
CONF_REF,
CONF_REFRESH,
CONF_URL,
)
import esphome.config_validation as cv
from esphome.const import CONF_PACKAGES
DOMAIN = CONF_PACKAGES
def _merge_package(full_old, full_new):
@ -23,11 +36,119 @@ def _merge_package(full_old, full_new):
return merge(full_old, full_new)
def validate_git_package(config: dict):
new_config = config
for key, conf in config.items():
if CONF_URL in conf:
try:
conf = BASE_SCHEMA(conf)
if CONF_FILE in conf:
new_config[key][CONF_FILES] = [conf[CONF_FILE]]
del new_config[key][CONF_FILE]
except cv.MultipleInvalid as e:
with cv.prepend_path([key]):
raise e
except cv.Invalid as e:
raise cv.Invalid(
"Extra keys not allowed in git based package",
path=[key] + e.path,
) from e
return new_config
def validate_yaml_filename(value):
value = cv.string(value)
if not (value.endswith(".yaml") or value.endswith(".yml")):
raise cv.Invalid("Only YAML (.yaml / .yml) files are supported.")
return value
def validate_source_shorthand(value):
if not isinstance(value, str):
raise cv.Invalid("Shorthand only for strings")
m = re.match(
r"github://([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)/([a-zA-Z0-9\-_.\./]+)(?:@([a-zA-Z0-9\-_.\./]+))?",
value,
)
if m is None:
raise cv.Invalid(
"Source is not a file system path or in expected github://username/name/[sub-folder/]file-path.yml[@branch-or-tag] format!"
)
conf = {
CONF_URL: f"https://github.com/{m.group(1)}/{m.group(2)}.git",
CONF_FILE: m.group(3),
}
if m.group(4):
conf[CONF_REF] = m.group(4)
# print(conf)
return BASE_SCHEMA(conf)
BASE_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_URL): cv.url,
cv.Exclusive(CONF_FILE, "files"): validate_yaml_filename,
cv.Exclusive(CONF_FILES, "files"): cv.All(
cv.ensure_list(validate_yaml_filename),
cv.Length(min=1),
),
cv.Optional(CONF_REF): cv.git_ref,
cv.Optional(CONF_REFRESH, default="1d"): cv.All(
cv.string, cv.source_refresh
),
}
),
cv.has_at_least_one_key(CONF_FILE, CONF_FILES),
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
str: cv.Any(validate_source_shorthand, BASE_SCHEMA, dict),
}
),
validate_git_package,
)
def _process_base_package(config: dict) -> dict:
repo_dir = git.clone_or_update(
url=config[CONF_URL],
ref=config.get(CONF_REF),
refresh=config[CONF_REFRESH],
domain=DOMAIN,
)
files: str = config[CONF_FILES]
packages = {}
for file in files:
yaml_file: Path = repo_dir / file
if not yaml_file.is_file():
raise cv.Invalid(f"{file} does not exist in repository", path=[CONF_FILES])
try:
packages[file] = yaml_util.load_yaml(yaml_file)
except EsphomeError as e:
raise cv.Invalid(
f"{file} is not a valid YAML file. Please check the file contents."
) from e
return {"packages": packages}
def do_packages_pass(config: dict):
if CONF_PACKAGES not in config:
return config
packages = config[CONF_PACKAGES]
with cv.prepend_path(CONF_PACKAGES):
packages = CONFIG_SCHEMA(packages)
if not isinstance(packages, dict):
raise cv.Invalid(
"Packages must be a key to value mapping, got {} instead"
@ -37,6 +158,8 @@ def do_packages_pass(config: dict):
for package_name, package_config in packages.items():
with cv.prepend_path(package_name):
recursive_package = package_config
if CONF_URL in package_config:
package_config = _process_base_package(package_config)
if isinstance(package_config, dict):
recursive_package = do_packages_pass(package_config)
config = _merge_package(recursive_package, config)

View File

@ -50,13 +50,11 @@ class PartitionLightOutput : public light::AddressableLight {
}
}
light::LightTraits get_traits() override { return this->segments_[0].get_src()->get_traits(); }
void loop() override {
if (this->should_show_()) {
for (auto seg : this->segments_) {
seg.get_src()->schedule_show();
}
this->mark_shown_();
void write_state(light::LightState *state) override {
for (auto seg : this->segments_) {
seg.get_src()->schedule_show();
}
this->mark_shown_();
}
protected:

View File

@ -7,6 +7,7 @@ namespace pm1006 {
static const char *const TAG = "pm1006";
static const uint8_t PM1006_RESPONSE_HEADER[] = {0x16, 0x11, 0x0B};
static const uint8_t PM1006_REQUEST[] = {0x11, 0x02, 0x0B, 0x01, 0xE1};
void PM1006Component::setup() {
// because this implementation is currently rx-only, there is nothing to setup
@ -15,9 +16,15 @@ void PM1006Component::setup() {
void PM1006Component::dump_config() {
ESP_LOGCONFIG(TAG, "PM1006:");
LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_);
LOG_UPDATE_INTERVAL(this);
this->check_uart_settings(9600);
}
void PM1006Component::update() {
ESP_LOGV(TAG, "sending measurement request");
this->write_array(PM1006_REQUEST, sizeof(PM1006_REQUEST));
}
void PM1006Component::loop() {
while (this->available() != 0) {
this->read_byte(&this->data_[this->data_index_]);

View File

@ -7,7 +7,7 @@
namespace esphome {
namespace pm1006 {
class PM1006Component : public Component, public uart::UARTDevice {
class PM1006Component : public PollingComponent, public uart::UARTDevice {
public:
PM1006Component() = default;
@ -15,6 +15,7 @@ class PM1006Component : public Component, public uart::UARTDevice {
void setup() override;
void dump_config() override;
void loop() override;
void update() override;
float get_setup_priority() const override;

View File

@ -4,11 +4,15 @@ from esphome.components import sensor, uart
from esphome.const import (
CONF_ID,
CONF_PM_2_5,
CONF_UPDATE_INTERVAL,
DEVICE_CLASS_PM25,
STATE_CLASS_MEASUREMENT,
UNIT_MICROGRAMS_PER_CUBIC_METER,
ICON_BLUR,
)
from esphome.core import TimePeriodMilliseconds
CODEOWNERS = ["@habbie"]
DEPENDENCIES = ["uart"]
pm1006_ns = cg.esphome_ns.namespace("pm1006")
@ -23,15 +27,34 @@ CONFIG_SCHEMA = cv.All(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_BLUR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA),
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.polling_component_schema("never")),
)
def validate_interval_uart(config):
require_tx = False
interval = config.get(CONF_UPDATE_INTERVAL)
if isinstance(interval, TimePeriodMilliseconds):
# 'never' is encoded as a very large int, not as a TimePeriodMilliseconds objects
require_tx = True
uart.final_validate_device_schema(
"pm1006", baud_rate=9600, require_rx=True, require_tx=require_tx
)(config)
FINAL_VALIDATE_SCHEMA = validate_interval_uart
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@ -13,7 +13,10 @@ from esphome.const import (
UNIT_MICROGRAMS_PER_CUBIC_METER,
ICON_CHEMICAL_WEAPON,
ICON_COUNTER,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_PM1,
DEVICE_CLASS_PM10,
DEVICE_CLASS_PM25,
STATE_CLASS_MEASUREMENT,
)
CODEOWNERS = ["@sjtrny"]
@ -36,40 +39,61 @@ CONFIG_SCHEMA = (
cv.GenerateID(): cv.declare_id(PMSA003IComponent),
cv.Optional(CONF_STANDARD_UNITS, default=True): cv.boolean,
cv.Optional(CONF_PM_1_0): sensor.sensor_schema(
UNIT_MICROGRAMS_PER_CUBIC_METER,
ICON_CHEMICAL_WEAPON,
2,
DEVICE_CLASS_EMPTY,
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=2,
device_class=DEVICE_CLASS_PM1,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
UNIT_MICROGRAMS_PER_CUBIC_METER,
ICON_CHEMICAL_WEAPON,
2,
DEVICE_CLASS_EMPTY,
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=2,
device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
UNIT_MICROGRAMS_PER_CUBIC_METER,
ICON_CHEMICAL_WEAPON,
2,
DEVICE_CLASS_EMPTY,
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=2,
device_class=DEVICE_CLASS_PM10,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PMC_0_3): sensor.sensor_schema(
UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY
unit_of_measurement=UNIT_COUNTS_PER_100ML,
icon=ICON_COUNTER,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PMC_0_5): sensor.sensor_schema(
UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY
unit_of_measurement=UNIT_COUNTS_PER_100ML,
icon=ICON_COUNTER,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PMC_1_0): sensor.sensor_schema(
UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY
unit_of_measurement=UNIT_COUNTS_PER_100ML,
icon=ICON_COUNTER,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PMC_2_5): sensor.sensor_schema(
UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY
unit_of_measurement=UNIT_COUNTS_PER_100ML,
icon=ICON_COUNTER,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PMC_5_0): sensor.sensor_schema(
UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY
unit_of_measurement=UNIT_COUNTS_PER_100ML,
icon=ICON_COUNTER,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PMC_10_0): sensor.sensor_schema(
UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY
unit_of_measurement=UNIT_COUNTS_PER_100ML,
icon=ICON_COUNTER,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
@ -90,15 +114,15 @@ TYPES = {
}
def to_code(config):
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
yield i2c.register_i2c_device(var, config)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_standard_units(config[CONF_STANDARD_UNITS]))
for key, funcName in TYPES.items():
if key in config:
sens = yield sensor.new_sensor(config[key])
sens = await sensor.new_sensor(config[key])
cg.add(getattr(var, funcName)(sens))

View File

@ -19,6 +19,9 @@ from esphome.const import (
CONF_PM_10_0UM,
CONF_TEMPERATURE,
CONF_TYPE,
DEVICE_CLASS_PM1,
DEVICE_CLASS_PM10,
DEVICE_CLASS_PM25,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
@ -75,19 +78,19 @@ CONFIG_SCHEMA = (
UNIT_MICROGRAMS_PER_CUBIC_METER,
ICON_CHEMICAL_WEAPON,
0,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_PM1,
),
cv.Optional(CONF_PM_2_5_STD): sensor.sensor_schema(
UNIT_MICROGRAMS_PER_CUBIC_METER,
ICON_CHEMICAL_WEAPON,
0,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_PM25,
),
cv.Optional(CONF_PM_10_0_STD): sensor.sensor_schema(
UNIT_MICROGRAMS_PER_CUBIC_METER,
ICON_CHEMICAL_WEAPON,
0,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_PM10,
),
cv.Optional(CONF_PM_1_0): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,

View File

@ -49,7 +49,7 @@ void PN532::setup() {
}
// Set up SAM (secure access module)
uint8_t sam_timeout = std::min(255u, this->update_interval_ / 50);
uint8_t sam_timeout = std::min<uint8_t>(255u, this->update_interval_ / 50);
if (!this->write_command_({
PN532_COMMAND_SAMCONFIGURATION,
0x01, // normal mode

View File

@ -42,7 +42,7 @@ void PowerSupply::request_high_power() {
void PowerSupply::unrequest_high_power() {
this->active_requests_--;
if (this->active_requests_ < 0) {
// we're just going to use 0 as our now counter.
// we're just going to use 0 as our new counter.
this->active_requests_ = 0;
}

View File

@ -13,7 +13,7 @@ from esphome.const import (
CONF_TOTAL,
ICON_PULSE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_NONE,
STATE_CLASS_TOTAL_INCREASING,
UNIT_PULSES_PER_MINUTE,
UNIT_PULSES,
)
@ -95,7 +95,7 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_PULSES,
icon=ICON_PULSE,
accuracy_decimals=0,
state_class=STATE_CLASS_NONE,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
}
)

View File

@ -11,8 +11,8 @@ from esphome.const import (
CONF_TOTAL,
CONF_VALUE,
ICON_PULSE,
LAST_RESET_TYPE_AUTO,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_PULSES,
UNIT_PULSES_PER_MINUTE,
)
@ -64,8 +64,7 @@ CONFIG_SCHEMA = sensor.sensor_schema(
unit_of_measurement=UNIT_PULSES,
icon=ICON_PULSE,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
}
)

View File

@ -11,8 +11,8 @@ from esphome.const import (
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_VOLTAGE,
LAST_RESET_TYPE_AUTO,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_VOLT,
UNIT_AMPERE,
UNIT_WATT,
@ -50,8 +50,7 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
}
)

View File

@ -15,8 +15,8 @@ from esphome.const import (
DEVICE_CLASS_POWER,
DEVICE_CLASS_ENERGY,
ICON_CURRENT_AC,
LAST_RESET_TYPE_AUTO,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_HERTZ,
UNIT_VOLT,
UNIT_AMPERE,
@ -55,8 +55,7 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(
unit_of_measurement=UNIT_HERTZ,

View File

@ -1085,3 +1085,45 @@ async def panasonic_action(var, config, args):
cg.add(var.set_address(template_))
template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint32)
cg.add(var.set_command(template_))
# Midea
MideaData, MideaBinarySensor, MideaTrigger, MideaAction, MideaDumper = declare_protocol(
"Midea"
)
MideaAction = ns.class_("MideaAction", RemoteTransmitterActionBase)
MIDEA_SCHEMA = cv.Schema(
{
cv.Required(CONF_CODE): cv.All(
[cv.Any(cv.hex_uint8_t, cv.uint8_t)],
cv.Length(min=5, max=5),
),
cv.GenerateID(CONF_CODE_STORAGE_ID): cv.declare_id(cg.uint8),
}
)
@register_binary_sensor("midea", MideaBinarySensor, MIDEA_SCHEMA)
def midea_binary_sensor(var, config):
arr_ = cg.progmem_array(config[CONF_CODE_STORAGE_ID], config[CONF_CODE])
cg.add(var.set_code(arr_))
@register_trigger("midea", MideaTrigger, MideaData)
def midea_trigger(var, config):
pass
@register_dumper("midea", MideaDumper)
def midea_dumper(var, config):
pass
@register_action(
"midea",
MideaAction,
MIDEA_SCHEMA,
)
async def midea_action(var, config, args):
arr_ = cg.progmem_array(config[CONF_CODE_STORAGE_ID], config[CONF_CODE])
cg.add(var.set_code(arr_))

View File

@ -0,0 +1,99 @@
#include "midea_protocol.h"
#include "esphome/core/log.h"
namespace esphome {
namespace remote_base {
static const char *const TAG = "remote.midea";
uint8_t MideaData::calc_cs_() const {
uint8_t cs = 0;
for (const uint8_t *it = this->data(); it != this->data() + OFFSET_CS; ++it)
cs -= reverse_bits_8(*it);
return reverse_bits_8(cs);
}
bool MideaData::check_compliment(const MideaData &rhs) const {
const uint8_t *it0 = rhs.data();
for (const uint8_t *it1 = this->data(); it1 != this->data() + this->size(); ++it0, ++it1) {
if (*it0 != ~(*it1))
return false;
}
return true;
}
void MideaProtocol::data(RemoteTransmitData *dst, const MideaData &src, bool compliment) {
for (const uint8_t *it = src.data(); it != src.data() + src.size(); ++it) {
const uint8_t data = compliment ? ~(*it) : *it;
for (uint8_t mask = 128; mask; mask >>= 1) {
if (data & mask)
one(dst);
else
zero(dst);
}
}
}
void MideaProtocol::encode(RemoteTransmitData *dst, const MideaData &data) {
dst->set_carrier_frequency(38000);
dst->reserve(2 + 48 * 2 + 2 + 2 + 48 * 2 + 2);
MideaProtocol::header(dst);
MideaProtocol::data(dst, data);
MideaProtocol::footer(dst);
MideaProtocol::header(dst);
MideaProtocol::data(dst, data, true);
MideaProtocol::footer(dst);
}
bool MideaProtocol::expect_one(RemoteReceiveData &src) {
if (!src.peek_item(BIT_HIGH_US, BIT_ONE_LOW_US))
return false;
src.advance(2);
return true;
}
bool MideaProtocol::expect_zero(RemoteReceiveData &src) {
if (!src.peek_item(BIT_HIGH_US, BIT_ZERO_LOW_US))
return false;
src.advance(2);
return true;
}
bool MideaProtocol::expect_header(RemoteReceiveData &src) {
if (!src.peek_item(HEADER_HIGH_US, HEADER_LOW_US))
return false;
src.advance(2);
return true;
}
bool MideaProtocol::expect_footer(RemoteReceiveData &src) {
if (!src.peek_item(BIT_HIGH_US, MIN_GAP_US))
return false;
src.advance(2);
return true;
}
bool MideaProtocol::expect_data(RemoteReceiveData &src, MideaData &out) {
for (uint8_t *dst = out.data(); dst != out.data() + out.size(); ++dst) {
for (uint8_t mask = 128; mask; mask >>= 1) {
if (MideaProtocol::expect_one(src))
*dst |= mask;
else if (!MideaProtocol::expect_zero(src))
return false;
}
}
return true;
}
optional<MideaData> MideaProtocol::decode(RemoteReceiveData src) {
MideaData out, inv;
if (MideaProtocol::expect_header(src) && MideaProtocol::expect_data(src, out) && MideaProtocol::expect_footer(src) &&
out.is_valid() && MideaProtocol::expect_data(src, inv) && out.check_compliment(inv))
return out;
return {};
}
void MideaProtocol::dump(const MideaData &data) { ESP_LOGD(TAG, "Received Midea: %s", data.to_string().c_str()); }
} // namespace remote_base
} // namespace esphome

View File

@ -0,0 +1,105 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "remote_base.h"
namespace esphome {
namespace remote_base {
class MideaData {
public:
// Make zero-filled
MideaData() { memset(this->data_, 0, sizeof(this->data_)); }
// Make from initializer_list
MideaData(std::initializer_list<uint8_t> data) { std::copy(data.begin(), data.end(), this->data()); }
// Make from vector
MideaData(const std::vector<uint8_t> &data) {
memcpy(this->data_, data.data(), std::min<size_t>(data.size(), sizeof(this->data_)));
}
// Make 40-bit copy from PROGMEM array
MideaData(const uint8_t *data) { memcpy_P(this->data_, data, OFFSET_CS); }
// Default copy constructor
MideaData(const MideaData &) = default;
uint8_t *data() { return this->data_; }
const uint8_t *data() const { return this->data_; }
uint8_t size() const { return sizeof(this->data_); }
bool is_valid() const { return this->data_[OFFSET_CS] == this->calc_cs_(); }
void finalize() { this->data_[OFFSET_CS] = this->calc_cs_(); }
bool check_compliment(const MideaData &rhs) const;
std::string to_string() const { return hexencode(*this); }
// compare only 40-bits
bool operator==(const MideaData &rhs) const { return !memcmp(this->data_, rhs.data_, OFFSET_CS); }
enum MideaDataType : uint8_t {
MIDEA_TYPE_COMMAND = 0xA1,
MIDEA_TYPE_SPECIAL = 0xA2,
MIDEA_TYPE_FOLLOW_ME = 0xA4,
};
MideaDataType type() const { return static_cast<MideaDataType>(this->data_[0]); }
template<typename T> T to() const { return T(*this); }
protected:
void set_value_(uint8_t offset, uint8_t val_mask, uint8_t shift, uint8_t val) {
data_[offset] &= ~(val_mask << shift);
data_[offset] |= (val << shift);
}
static const uint8_t OFFSET_CS = 5;
// 48-bits data
uint8_t data_[6];
// Calculate checksum
uint8_t calc_cs_() const;
};
class MideaProtocol : public RemoteProtocol<MideaData> {
public:
void encode(RemoteTransmitData *dst, const MideaData &data) override;
optional<MideaData> decode(RemoteReceiveData src) override;
void dump(const MideaData &data) override;
protected:
static const int32_t TICK_US = 560;
static const int32_t HEADER_HIGH_US = 8 * TICK_US;
static const int32_t HEADER_LOW_US = 8 * TICK_US;
static const int32_t BIT_HIGH_US = 1 * TICK_US;
static const int32_t BIT_ONE_LOW_US = 3 * TICK_US;
static const int32_t BIT_ZERO_LOW_US = 1 * TICK_US;
static const int32_t MIN_GAP_US = 10 * TICK_US;
static void one(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); }
static void zero(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); }
static void header(RemoteTransmitData *dst) { dst->item(HEADER_HIGH_US, HEADER_LOW_US); }
static void footer(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, MIN_GAP_US); }
static void data(RemoteTransmitData *dst, const MideaData &src, bool compliment = false);
static bool expect_one(RemoteReceiveData &src);
static bool expect_zero(RemoteReceiveData &src);
static bool expect_header(RemoteReceiveData &src);
static bool expect_footer(RemoteReceiveData &src);
static bool expect_data(RemoteReceiveData &src, MideaData &out);
};
class MideaBinarySensor : public RemoteReceiverBinarySensorBase {
public:
bool matches(RemoteReceiveData src) override {
auto data = MideaProtocol().decode(src);
return data.has_value() && data.value() == this->data_;
}
void set_code(const uint8_t *code) { this->data_ = code; }
protected:
MideaData data_;
};
using MideaTrigger = RemoteReceiverTrigger<MideaProtocol, MideaData>;
using MideaDumper = RemoteReceiverDumper<MideaProtocol, MideaData>;
template<typename... Ts> class MideaAction : public RemoteTransmitterActionBase<Ts...> {
TEMPLATABLE_VALUE(const uint8_t *, code)
void encode(RemoteTransmitData *dst, Ts... x) override {
MideaData data = this->code_.value(x...);
data.finalize();
MideaProtocol().encode(dst, data);
}
};
} // namespace remote_base
} // namespace esphome

View File

@ -23,17 +23,17 @@ from esphome.const import (
DEVICE_CLASS_VOLTAGE,
ICON_CURRENT_AC,
ICON_FLASH,
LAST_RESET_TYPE_AUTO,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_DEGREES,
UNIT_HERTZ,
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
UNIT_KILOWATT_HOURS,
UNIT_VOLT,
UNIT_VOLT_AMPS,
UNIT_VOLT_AMPS_REACTIVE,
UNIT_VOLT_AMPS_REACTIVE_HOURS,
UNIT_WATT,
UNIT_WATT_HOURS,
)
AUTO_LOAD = ["modbus"]
@ -47,6 +47,7 @@ PHASE_SENSORS = {
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_CURRENT: sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
@ -100,32 +101,28 @@ CONFIG_SCHEMA = (
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_IMPORT_ACTIVE_ENERGY): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_EXPORT_ACTIVE_ENERGY): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_IMPORT_REACTIVE_ENERGY): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE_HOURS,
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_EXPORT_REACTIVE_ENERGY): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE_HOURS,
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
}
)

View File

@ -7,6 +7,8 @@ from esphome.const import (
CONF_PM_2_5,
CONF_RX_ONLY,
CONF_UPDATE_INTERVAL,
DEVICE_CLASS_PM25,
DEVICE_CLASS_PM10,
STATE_CLASS_MEASUREMENT,
UNIT_MICROGRAMS_PER_CUBIC_METER,
ICON_CHEMICAL_WEAPON,
@ -41,12 +43,14 @@ CONFIG_SCHEMA = cv.All(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=1,
device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=1,
device_class=DEVICE_CLASS_PM10,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_RX_ONLY, default=False): cv.boolean,

View File

@ -20,8 +20,8 @@ from esphome.const import (
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_VOLTAGE,
ICON_CURRENT_AC,
LAST_RESET_TYPE_AUTO,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_HERTZ,
UNIT_VOLT,
@ -54,50 +54,43 @@ SENSORS = {
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
CONF_IMPORT_ACTIVE_ENERGY: sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
CONF_EXPORT_ACTIVE_ENERGY: sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
CONF_TOTAL_REACTIVE_ENERGY: sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
CONF_IMPORT_REACTIVE_ENERGY: sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
CONF_EXPORT_REACTIVE_ENERGY: sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
CONF_APPARENT_ENERGY: sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_HOURS,
accuracy_decimals=2,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
CONF_ACTIVE_POWER: sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,

View File

@ -7,6 +7,7 @@ from esphome.const import (
CONF_CO2,
CONF_ID,
ICON_MOLECULE_CO2,
DEVICE_CLASS_CARBON_DIOXIDE,
STATE_CLASS_MEASUREMENT,
UNIT_PARTS_PER_MILLION,
)
@ -41,6 +42,7 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
}

View File

@ -17,7 +17,6 @@ from esphome.const import (
CONF_ICON,
CONF_ID,
CONF_INTERNAL,
CONF_LAST_RESET_TYPE,
CONF_ON_RAW_VALUE,
CONF_ON_VALUE,
CONF_ON_VALUE_RANGE,
@ -31,25 +30,32 @@ from esphome.const import (
CONF_NAME,
CONF_MQTT_ID,
CONF_FORCE_UPDATE,
LAST_RESET_TYPE_AUTO,
LAST_RESET_TYPE_NEVER,
LAST_RESET_TYPE_NONE,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_AQI,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_MONETARY,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_NITROGEN_DIOXIDE,
DEVICE_CLASS_NITROGEN_MONOXIDE,
DEVICE_CLASS_NITROUS_OXIDE,
DEVICE_CLASS_OZONE,
DEVICE_CLASS_PM1,
DEVICE_CLASS_PM10,
DEVICE_CLASS_PM25,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_SULPHUR_DIOXIDE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
DEVICE_CLASS_VOLTAGE,
)
from esphome.core import CORE, coroutine_with_priority
@ -58,21 +64,31 @@ from esphome.util import Registry
CODEOWNERS = ["@esphome/core"]
DEVICE_CLASSES = [
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_AQI,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_MONETARY,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
DEVICE_CLASS_NITROGEN_DIOXIDE,
DEVICE_CLASS_NITROGEN_MONOXIDE,
DEVICE_CLASS_NITROUS_OXIDE,
DEVICE_CLASS_OZONE,
DEVICE_CLASS_PM1,
DEVICE_CLASS_PM10,
DEVICE_CLASS_PM25,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_SULPHUR_DIOXIDE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
DEVICE_CLASS_VOLTAGE,
]
@ -85,15 +101,6 @@ STATE_CLASSES = {
}
validate_state_class = cv.enum(STATE_CLASSES, lower=True, space="_")
LastResetTypes = sensor_ns.enum("LastResetType")
LAST_RESET_TYPES = {
LAST_RESET_TYPE_NONE: LastResetTypes.LAST_RESET_TYPE_NONE,
LAST_RESET_TYPE_NEVER: LastResetTypes.LAST_RESET_TYPE_NEVER,
LAST_RESET_TYPE_AUTO: LastResetTypes.LAST_RESET_TYPE_AUTO,
}
validate_last_reset_type = cv.enum(LAST_RESET_TYPES, lower=True, space="_")
IS_PLATFORM_COMPONENT = True
@ -183,7 +190,9 @@ SENSOR_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend(
cv.Optional(CONF_ACCURACY_DECIMALS): validate_accuracy_decimals,
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
cv.Optional(CONF_STATE_CLASS): validate_state_class,
cv.Optional(CONF_LAST_RESET_TYPE): validate_last_reset_type,
cv.Optional("last_reset_type"): cv.invalid(
"last_reset_type has been removed since 2021.9.0. state_class: total_increasing should be used for total values."
),
cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean,
cv.Optional(CONF_EXPIRE_AFTER): cv.All(
cv.requires_component("mqtt"),
@ -220,7 +229,6 @@ def sensor_schema(
accuracy_decimals: int = _UNDEF,
device_class: str = _UNDEF,
state_class: str = _UNDEF,
last_reset_type: str = _UNDEF,
) -> cv.Schema:
schema = SENSOR_SCHEMA
if unit_of_measurement is not _UNDEF:
@ -253,14 +261,6 @@ def sensor_schema(
schema = schema.extend(
{cv.Optional(CONF_STATE_CLASS, default=state_class): validate_state_class}
)
if last_reset_type is not _UNDEF:
schema = schema.extend(
{
cv.Optional(
CONF_LAST_RESET_TYPE, default=last_reset_type
): validate_last_reset_type
}
)
return schema
@ -511,8 +511,6 @@ async def setup_sensor_core_(var, config):
cg.add(var.set_icon(config[CONF_ICON]))
if CONF_ACCURACY_DECIMALS in config:
cg.add(var.set_accuracy_decimals(config[CONF_ACCURACY_DECIMALS]))
if CONF_LAST_RESET_TYPE in config:
cg.add(var.set_last_reset_type(config[CONF_LAST_RESET_TYPE]))
cg.add(var.set_force_update(config[CONF_FORCE_UPDATE]))
if config.get(CONF_FILTERS): # must exist and not be empty
filters = await build_filters(config[CONF_FILTERS])

View File

@ -18,18 +18,6 @@ const char *state_class_to_string(StateClass state_class) {
}
}
const char *last_reset_type_to_string(LastResetType last_reset_type) {
switch (last_reset_type) {
case LAST_RESET_TYPE_NEVER:
return "never";
case LAST_RESET_TYPE_AUTO:
return "auto";
case LAST_RESET_TYPE_NONE:
default:
return "";
}
}
void Sensor::publish_state(float state) {
this->raw_state = state;
this->raw_callback_.call(state);
@ -80,7 +68,6 @@ void Sensor::set_state_class(const std::string &state_class) {
ESP_LOGW(TAG, "'%s' - Unrecognized state class %s", this->get_name().c_str(), state_class.c_str());
}
}
void Sensor::set_last_reset_type(LastResetType last_reset_type) { this->last_reset_type = last_reset_type; }
std::string Sensor::get_unit_of_measurement() {
if (this->unit_of_measurement_.has_value())
return *this->unit_of_measurement_;

View File

@ -14,10 +14,6 @@ namespace sensor {
ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \
} \
ESP_LOGCONFIG(TAG, "%s State Class: '%s'", prefix, state_class_to_string((obj)->state_class)); \
if ((obj)->state_class == sensor::STATE_CLASS_MEASUREMENT && \
(obj)->last_reset_type != sensor::LAST_RESET_TYPE_NONE) { \
ESP_LOGCONFIG(TAG, "%s Last Reset Type: '%s'", prefix, last_reset_type_to_string((obj)->last_reset_type)); \
} \
ESP_LOGCONFIG(TAG, "%s Unit of Measurement: '%s'", prefix, (obj)->get_unit_of_measurement().c_str()); \
ESP_LOGCONFIG(TAG, "%s Accuracy Decimals: %d", prefix, (obj)->get_accuracy_decimals()); \
if (!(obj)->get_icon().empty()) { \
@ -42,20 +38,6 @@ enum StateClass : uint8_t {
const char *state_class_to_string(StateClass state_class);
/**
* Sensor last reset types
*/
enum LastResetType : uint8_t {
/// This sensor does not support resetting. ie, it is not accumulative
LAST_RESET_TYPE_NONE = 0,
/// This sensor is expected to never reset its value
LAST_RESET_TYPE_NEVER = 1,
/// This sensor may reset and Home Assistant will watch for this
LAST_RESET_TYPE_AUTO = 2,
};
const char *last_reset_type_to_string(LastResetType last_reset_type);
/** Base-class for all sensors.
*
* A sensor has unit of measurement and can use publish_state to send out a new value with the specified accuracy.
@ -174,12 +156,6 @@ class Sensor : public Nameable {
*/
virtual std::string device_class();
// The Last reset type of this sensor
LastResetType last_reset_type{LAST_RESET_TYPE_NONE};
/// Manually set the Home Assistant last reset type for this sensor.
void set_last_reset_type(LastResetType last_reset_type);
/** A unique ID for this sensor, empty for no unique id. See unique ID requirements:
* https://developers.home-assistant.io/docs/en/entity_registry_index.html#unique-id-requirements
*

View File

@ -7,6 +7,8 @@ from esphome.const import (
CONF_ECO2,
CONF_TVOC,
ICON_RADIATOR,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
STATE_CLASS_MEASUREMENT,
UNIT_PARTS_PER_MILLION,
UNIT_PARTS_PER_BILLION,
@ -34,12 +36,14 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ECO2_BASELINE): sensor.sensor_schema(

View File

@ -4,6 +4,7 @@ from esphome.components import i2c, sensor
from esphome.const import (
CONF_ID,
ICON_RADIATOR,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
STATE_CLASS_MEASUREMENT,
)
@ -26,6 +27,7 @@ CONFIG_SCHEMA = (
sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(

View File

@ -78,27 +78,28 @@ void SGP40Component::setup() {
}
void SGP40Component::self_test_() {
ESP_LOGD(TAG, "selfTest started");
ESP_LOGD(TAG, "Self-test started");
if (!this->write_command_(SGP40_CMD_SELF_TEST)) {
this->error_code_ = COMMUNICATION_FAILED;
ESP_LOGD(TAG, "selfTest communicatin failed");
ESP_LOGD(TAG, "Self-test communication failed");
this->mark_failed();
}
this->set_timeout(250, [this]() {
uint16_t reply[1];
if (!this->read_data_(reply, 1)) {
ESP_LOGD(TAG, "selfTest read_data_ failed");
ESP_LOGD(TAG, "Self-test read_data_ failed");
this->mark_failed();
return;
}
if (reply[0] == 0xD400) {
ESP_LOGD(TAG, "selfTest completed");
this->self_test_complete_ = true;
ESP_LOGD(TAG, "Self-test completed");
return;
}
ESP_LOGD(TAG, "selfTest failed");
ESP_LOGD(TAG, "Self-test failed");
this->mark_failed();
});
}
@ -154,6 +155,12 @@ int32_t SGP40Component::measure_voc_index_() {
*/
uint16_t SGP40Component::measure_raw_() {
float humidity = NAN;
if (!this->self_test_complete_) {
ESP_LOGD(TAG, "Self-test not yet complete");
return UINT16_MAX;
}
if (this->humidity_sensor_ != nullptr) {
humidity = this->humidity_sensor_->state;
}

View File

@ -68,6 +68,7 @@ class SGP40Component : public PollingComponent, public sensor::Sensor, public i2
int32_t seconds_since_last_store_;
SGP40Baselines baselines_storage_;
VocAlgorithmParams voc_algorithm_params_;
bool self_test_complete_;
bool store_baseline_;
int32_t state0_;
int32_t state1_;

View File

@ -10,6 +10,10 @@ from esphome.const import (
CONF_PM_10_0,
CONF_TEMPERATURE,
CONF_HUMIDITY,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
DEVICE_CLASS_PM25,
DEVICE_CLASS_PM10,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
STATE_CLASS_MEASUREMENT,
@ -36,6 +40,7 @@ CONFIG_SCHEMA = cv.All(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema(
@ -48,18 +53,21 @@ CONFIG_SCHEMA = cv.All(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_GRAIN,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_GRAIN,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM10,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(

View File

@ -27,7 +27,8 @@ void SM300D2Sensor::update() {
}
uint16_t calculated_checksum = this->sm300d2_checksum_(response);
if (calculated_checksum != response[SM300D2_RESPONSE_LENGTH - 1]) {
if ((calculated_checksum != response[SM300D2_RESPONSE_LENGTH - 1]) &&
(calculated_checksum - 0x80 != response[SM300D2_RESPONSE_LENGTH - 1])) {
ESP_LOGW(TAG, "SM300D2 Checksum doesn't match: 0x%02X!=0x%02X", response[SM300D2_RESPONSE_LENGTH - 1],
calculated_checksum);
this->status_set_warning();

View File

@ -56,9 +56,8 @@ void SNTPComponent::loop() {
if (!time.is_valid())
return;
char buf[128];
time.strftime(buf, sizeof(buf), "%c");
ESP_LOGD(TAG, "Synchronized time: %s", buf);
ESP_LOGD(TAG, "Synchronized time: %d-%d-%d %d:%d:%d", time.year, time.month, time.day_of_month, time.hour,
time.minute, time.second);
this->time_sync_callback_.call();
this->has_time_ = true;
}

View File

@ -16,7 +16,7 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(SNTPComponent),
cv.Optional(CONF_SERVERS, default=DEFAULT_SERVERS): cv.All(
cv.ensure_list(cv.domain), cv.Length(min=1, max=3)
cv.ensure_list(cv.Any(cv.domain, cv.hostname)), cv.Length(min=1, max=3)
),
}
).extend(cv.COMPONENT_SCHEMA)

View File

@ -0,0 +1,28 @@
import esphome.config_validation as cv
import esphome.codegen as cg
CODEOWNERS = ["@esphome/core"]
CONF_IMPLEMENTATION = "implementation"
IMPLEMENTATION_LWIP_TCP = "lwip_tcp"
IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets"
CONFIG_SCHEMA = cv.Schema(
{
cv.SplitDefault(
CONF_IMPLEMENTATION,
esp8266=IMPLEMENTATION_LWIP_TCP,
esp32=IMPLEMENTATION_BSD_SOCKETS,
): cv.one_of(
IMPLEMENTATION_LWIP_TCP, IMPLEMENTATION_BSD_SOCKETS, lower=True, space="_"
),
}
)
async def to_code(config):
impl = config[CONF_IMPLEMENTATION]
if impl == IMPLEMENTATION_LWIP_TCP:
cg.add_define("USE_SOCKET_IMPL_LWIP_TCP")
elif impl == IMPLEMENTATION_BSD_SOCKETS:
cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS")

View File

@ -0,0 +1,105 @@
#include "socket.h"
#include "esphome/core/defines.h"
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
#include <string.h>
namespace esphome {
namespace socket {
std::string format_sockaddr(const struct sockaddr_storage &storage) {
if (storage.ss_family == AF_INET) {
const struct sockaddr_in *addr = reinterpret_cast<const struct sockaddr_in *>(&storage);
char buf[INET_ADDRSTRLEN];
const char *ret = inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf));
if (ret == NULL)
return {};
return std::string{buf};
} else if (storage.ss_family == AF_INET6) {
const struct sockaddr_in6 *addr = reinterpret_cast<const struct sockaddr_in6 *>(&storage);
char buf[INET6_ADDRSTRLEN];
const char *ret = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf));
if (ret == NULL)
return {};
return std::string{buf};
}
return {};
}
class BSDSocketImpl : public Socket {
public:
BSDSocketImpl(int fd) : Socket(), fd_(fd) {}
~BSDSocketImpl() override {
if (!closed_) {
close();
}
}
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
int fd = ::accept(fd_, addr, addrlen);
if (fd == -1)
return {};
return std::unique_ptr<BSDSocketImpl>{new BSDSocketImpl(fd)};
}
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(fd_, addr, addrlen); }
int close() override {
int ret = ::close(fd_);
closed_ = true;
return ret;
}
int shutdown(int how) override { return ::shutdown(fd_, how); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return ::getpeername(fd_, addr, addrlen); }
std::string getpeername() override {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
int err = this->getpeername((struct sockaddr *) &storage, &len);
if (err != 0)
return {};
return format_sockaddr(storage);
}
int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { return ::getsockname(fd_, addr, addrlen); }
std::string getsockname() override {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
int err = this->getsockname((struct sockaddr *) &storage, &len);
if (err != 0)
return {};
return format_sockaddr(storage);
}
int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override {
return ::getsockopt(fd_, level, optname, optval, optlen);
}
int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override {
return ::setsockopt(fd_, level, optname, optval, optlen);
}
int listen(int backlog) override { return ::listen(fd_, backlog); }
ssize_t read(void *buf, size_t len) override { return ::read(fd_, buf, len); }
ssize_t write(const void *buf, size_t len) override { return ::write(fd_, buf, len); }
int setblocking(bool blocking) override {
int fl = ::fcntl(fd_, F_GETFL, 0);
if (blocking) {
fl &= ~O_NONBLOCK;
} else {
fl |= O_NONBLOCK;
}
::fcntl(fd_, F_SETFL, fl);
return 0;
}
protected:
int fd_;
bool closed_ = false;
};
std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
int ret = ::socket(domain, type, protocol);
if (ret == -1)
return nullptr;
return std::unique_ptr<Socket>{new BSDSocketImpl(ret)};
}
} // namespace socket
} // namespace esphome
#endif // USE_SOCKET_IMPL_BSD_SOCKETS

View File

@ -0,0 +1,127 @@
#pragma once
#include "esphome/core/defines.h"
// Helper file to include all socket-related system headers (or use our own
// definitions where system ones don't exist)
#ifdef USE_SOCKET_IMPL_LWIP_TCP
#define LWIP_INTERNAL
#include <sys/types.h>
#include "lwip/inet.h"
#include <stdint.h>
#include <errno.h>
/* Address families. */
#define AF_UNSPEC 0
#define AF_INET 2
#define AF_INET6 10
#define PF_INET AF_INET
#define PF_INET6 AF_INET6
#define PF_UNSPEC AF_UNSPEC
#define IPPROTO_IP 0
#define IPPROTO_TCP 6
#define IPPROTO_IPV6 41
#define IPPROTO_ICMPV6 58
#define TCP_NODELAY 0x01
#define F_GETFL 3
#define F_SETFL 4
#define O_NONBLOCK 1
#define SHUT_RD 0
#define SHUT_WR 1
#define SHUT_RDWR 2
/* Socket protocol types (TCP/UDP/RAW) */
#define SOCK_STREAM 1
#define SOCK_DGRAM 2
#define SOCK_RAW 3
#define SO_REUSEADDR 0x0004 /* Allow local address reuse */
#define SO_KEEPALIVE 0x0008 /* keep connections alive */
#define SO_BROADCAST 0x0020 /* permit to send and to receive broadcast messages (see IP_SOF_BROADCAST option) */
#define SOL_SOCKET 0xfff /* options for socket level */
typedef uint8_t sa_family_t;
typedef uint16_t in_port_t;
struct sockaddr_in {
uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
#define SIN_ZERO_LEN 8
char sin_zero[SIN_ZERO_LEN];
};
struct sockaddr_in6 {
uint8_t sin6_len; /* length of this structure */
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Set of interfaces for scope */
};
struct sockaddr {
uint8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
};
struct sockaddr_storage {
uint8_t s2_len;
sa_family_t ss_family;
char s2_data1[2];
uint32_t s2_data2[3];
uint32_t s2_data3[3];
};
typedef uint32_t socklen_t;
#ifdef ARDUINO_ARCH_ESP8266
// arduino-esp8266 declares a global vars called INADDR_NONE/ANY which are invalid with the define
#ifdef INADDR_ANY
#undef INADDR_ANY
#endif
#ifdef INADDR_NONE
#undef INADDR_NONE
#endif
#define ESPHOME_INADDR_ANY ((uint32_t) 0x00000000UL)
#define ESPHOME_INADDR_NONE ((uint32_t) 0xFFFFFFFFUL)
#else // !ARDUINO_ARCH_ESP8266
#define ESPHOME_INADDR_ANY INADDR_ANY
#define ESPHOME_INADDR_NONE INADDR_NONE
#endif
#endif // USE_SOCKET_IMPL_LWIP_TCP
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdint.h>
#ifdef ARDUINO_ARCH_ESP32
// arduino-esp32 declares a global var called INADDR_NONE which is replaced
// by the define
#ifdef INADDR_NONE
#undef INADDR_NONE
#endif
// not defined for ESP32
typedef uint32_t socklen_t;
#define ESPHOME_INADDR_ANY ((uint32_t) 0x00000000UL)
#define ESPHOME_INADDR_NONE ((uint32_t) 0xFFFFFFFFUL)
#else // !ARDUINO_ARCH_ESP32
#define ESPHOME_INADDR_ANY INADDR_ANY
#define ESPHOME_INADDR_NONE INADDR_NONE
#endif
#endif // USE_SOCKET_IMPL_BSD_SOCKETS

View File

@ -0,0 +1,515 @@
#include "socket.h"
#include "esphome/core/defines.h"
#ifdef USE_SOCKET_IMPL_LWIP_TCP
#include "lwip/ip.h"
#include "lwip/netif.h"
#include "lwip/opt.h"
#include "lwip/tcp.h"
#include <cerrno>
#include <cstring>
#include <queue>
#include "esphome/core/log.h"
namespace esphome {
namespace socket {
static const char *const TAG = "socket.lwip";
// set to 1 to enable verbose lwip logging
#if 0
#define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__)
#else
#define LWIP_LOG(msg, ...)
#endif
class LWIPRawImpl : public Socket {
public:
LWIPRawImpl(struct tcp_pcb *pcb) : pcb_(pcb) {}
~LWIPRawImpl() override {
if (pcb_ != nullptr) {
LWIP_LOG("tcp_abort(%p)", pcb_);
tcp_abort(pcb_);
pcb_ = nullptr;
}
}
void init() {
LWIP_LOG("init(%p)", pcb_);
tcp_arg(pcb_, this);
tcp_accept(pcb_, LWIPRawImpl::s_accept_fn);
tcp_recv(pcb_, LWIPRawImpl::s_recv_fn);
tcp_err(pcb_, LWIPRawImpl::s_err_fn);
}
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
if (pcb_ == nullptr) {
errno = EBADF;
return nullptr;
}
if (accepted_sockets_.empty()) {
errno = EWOULDBLOCK;
return nullptr;
}
std::unique_ptr<LWIPRawImpl> sock = std::move(accepted_sockets_.front());
accepted_sockets_.pop();
if (addr != nullptr) {
sock->getpeername(addr, addrlen);
}
LWIP_LOG("accept(%p)", sock.get());
return std::unique_ptr<Socket>(std::move(sock));
}
int bind(const struct sockaddr *name, socklen_t addrlen) override {
if (pcb_ == nullptr) {
errno = EBADF;
return -1;
}
if (name == nullptr) {
errno = EINVAL;
return 0;
}
ip_addr_t ip;
in_port_t port;
auto family = name->sa_family;
#if LWIP_IPV6
if (family == AF_INET) {
if (addrlen < sizeof(sockaddr_in6)) {
errno = EINVAL;
return -1;
}
auto *addr4 = reinterpret_cast<const sockaddr_in *>(name);
port = ntohs(addr4->sin_port);
ip.type = IPADDR_TYPE_V4;
ip.u_addr.ip4.addr = addr4->sin_addr.s_addr;
} else if (family == AF_INET6) {
if (addrlen < sizeof(sockaddr_in)) {
errno = EINVAL;
return -1;
}
auto *addr6 = reinterpret_cast<const sockaddr_in6 *>(name);
port = ntohs(addr6->sin6_port);
ip.type = IPADDR_TYPE_V6;
memcpy(&ip.u_addr.ip6.addr, &addr6->sin6_addr.un.u8_addr, 16);
} else {
errno = EINVAL;
return -1;
}
#else
if (family != AF_INET) {
errno = EINVAL;
return -1;
}
auto *addr4 = reinterpret_cast<const sockaddr_in *>(name);
port = ntohs(addr4->sin_port);
ip.addr = addr4->sin_addr.s_addr;
#endif
LWIP_LOG("tcp_bind(%p ip=%u port=%u)", pcb_, ip.addr, port);
err_t err = tcp_bind(pcb_, &ip, port);
if (err == ERR_USE) {
LWIP_LOG(" -> err ERR_USE");
errno = EADDRINUSE;
return -1;
}
if (err == ERR_VAL) {
LWIP_LOG(" -> err ERR_VAL");
errno = EINVAL;
return -1;
}
if (err != ERR_OK) {
LWIP_LOG(" -> err %d", err);
errno = EIO;
return -1;
}
return 0;
}
int close() override {
if (pcb_ == nullptr) {
errno = ECONNRESET;
return -1;
}
LWIP_LOG("tcp_close(%p)", pcb_);
err_t err = tcp_close(pcb_);
if (err != ERR_OK) {
LWIP_LOG(" -> err %d", err);
tcp_abort(pcb_);
pcb_ = nullptr;
errno = err == ERR_MEM ? ENOMEM : EIO;
return -1;
}
pcb_ = nullptr;
return 0;
}
int shutdown(int how) override {
if (pcb_ == nullptr) {
errno = ECONNRESET;
return -1;
}
bool shut_rx = false, shut_tx = false;
if (how == SHUT_RD) {
shut_rx = true;
} else if (how == SHUT_WR) {
shut_tx = true;
} else if (how == SHUT_RDWR) {
shut_rx = shut_tx = true;
} else {
errno = EINVAL;
return -1;
}
LWIP_LOG("tcp_shutdown(%p shut_rx=%d shut_tx=%d)", pcb_, shut_rx ? 1 : 0, shut_tx ? 1 : 0);
err_t err = tcp_shutdown(pcb_, shut_rx, shut_tx);
if (err != ERR_OK) {
LWIP_LOG(" -> err %d", err);
errno = err == ERR_MEM ? ENOMEM : EIO;
return -1;
}
return 0;
}
int getpeername(struct sockaddr *name, socklen_t *addrlen) override {
if (pcb_ == nullptr) {
errno = ECONNRESET;
return -1;
}
if (name == nullptr || addrlen == nullptr) {
errno = EINVAL;
return -1;
}
if (*addrlen < sizeof(struct sockaddr_in)) {
errno = EINVAL;
return -1;
}
struct sockaddr_in *addr = reinterpret_cast<struct sockaddr_in *>(name);
addr->sin_family = AF_INET;
*addrlen = addr->sin_len = sizeof(struct sockaddr_in);
addr->sin_port = pcb_->remote_port;
addr->sin_addr.s_addr = pcb_->remote_ip.addr;
return 0;
}
std::string getpeername() override {
if (pcb_ == nullptr) {
errno = ECONNRESET;
return "";
}
char buffer[24];
uint32_t ip4 = pcb_->remote_ip.addr;
snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", (ip4 >> 0) & 0xFF, (ip4 >> 8) & 0xFF, (ip4 >> 16) & 0xFF,
(ip4 >> 24) & 0xFF);
return std::string(buffer);
}
int getsockname(struct sockaddr *name, socklen_t *addrlen) override {
if (pcb_ == nullptr) {
errno = ECONNRESET;
return -1;
}
if (name == nullptr || addrlen == nullptr) {
errno = EINVAL;
return -1;
}
if (*addrlen < sizeof(struct sockaddr_in)) {
errno = EINVAL;
return -1;
}
struct sockaddr_in *addr = reinterpret_cast<struct sockaddr_in *>(name);
addr->sin_family = AF_INET;
*addrlen = addr->sin_len = sizeof(struct sockaddr_in);
addr->sin_port = pcb_->local_port;
addr->sin_addr.s_addr = pcb_->local_ip.addr;
return 0;
}
std::string getsockname() override {
if (pcb_ == nullptr) {
errno = ECONNRESET;
return "";
}
char buffer[24];
uint32_t ip4 = pcb_->local_ip.addr;
snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", (ip4 >> 0) & 0xFF, (ip4 >> 8) & 0xFF, (ip4 >> 16) & 0xFF,
(ip4 >> 24) & 0xFF);
return std::string(buffer);
}
int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override {
if (pcb_ == nullptr) {
errno = ECONNRESET;
return -1;
}
if (optlen == nullptr || optval == nullptr) {
errno = EINVAL;
return -1;
}
if (level == SOL_SOCKET && optname == SO_REUSEADDR) {
if (*optlen < 4) {
errno = EINVAL;
return -1;
}
// lwip doesn't seem to have this feature. Don't send an error
// to prevent warnings
*reinterpret_cast<int *>(optval) = 1;
*optlen = 4;
return 0;
}
if (level == IPPROTO_TCP && optname == TCP_NODELAY) {
if (*optlen < 4) {
errno = EINVAL;
return -1;
}
*reinterpret_cast<int *>(optval) = tcp_nagle_disabled(pcb_);
*optlen = 4;
return 0;
}
errno = EINVAL;
return -1;
}
int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override {
if (pcb_ == nullptr) {
errno = ECONNRESET;
return -1;
}
if (level == SOL_SOCKET && optname == SO_REUSEADDR) {
if (optlen != 4) {
errno = EINVAL;
return -1;
}
// lwip doesn't seem to have this feature. Don't send an error
// to prevent warnings
return 0;
}
if (level == IPPROTO_TCP && optname == TCP_NODELAY) {
if (optlen != 4) {
errno = EINVAL;
return -1;
}
int val = *reinterpret_cast<const int *>(optval);
if (val != 0) {
tcp_nagle_disable(pcb_);
} else {
tcp_nagle_enable(pcb_);
}
return 0;
}
errno = EINVAL;
return -1;
}
int listen(int backlog) override {
if (pcb_ == nullptr) {
errno = EBADF;
return -1;
}
LWIP_LOG("tcp_listen_with_backlog(%p backlog=%d)", pcb_, backlog);
struct tcp_pcb *listen_pcb = tcp_listen_with_backlog(pcb_, backlog);
if (listen_pcb == nullptr) {
tcp_abort(pcb_);
pcb_ = nullptr;
errno = EOPNOTSUPP;
return -1;
}
// tcp_listen reallocates the pcb, replace ours
pcb_ = listen_pcb;
// set callbacks on new pcb
LWIP_LOG("tcp_arg(%p)", pcb_);
tcp_arg(pcb_, this);
tcp_accept(pcb_, LWIPRawImpl::s_accept_fn);
return 0;
}
ssize_t read(void *buf, size_t len) override {
if (pcb_ == nullptr) {
errno = ECONNRESET;
return -1;
}
if (rx_closed_ && rx_buf_ == nullptr) {
errno = ECONNRESET;
return -1;
}
if (len == 0) {
return 0;
}
if (rx_buf_ == nullptr) {
errno = EWOULDBLOCK;
return -1;
}
size_t read = 0;
uint8_t *buf8 = reinterpret_cast<uint8_t *>(buf);
while (len && rx_buf_ != nullptr) {
size_t pb_len = rx_buf_->len;
size_t pb_left = pb_len - rx_buf_offset_;
if (pb_left == 0)
break;
size_t copysize = std::min(len, pb_left);
memcpy(buf8, reinterpret_cast<uint8_t *>(rx_buf_->payload) + rx_buf_offset_, copysize);
if (pb_left == copysize) {
// full pb copied, free it
if (rx_buf_->next == nullptr) {
// last buffer in chain
pbuf_free(rx_buf_);
rx_buf_ = nullptr;
rx_buf_offset_ = 0;
} else {
auto *old_buf = rx_buf_;
rx_buf_ = rx_buf_->next;
pbuf_ref(rx_buf_);
pbuf_free(old_buf);
rx_buf_offset_ = 0;
}
} else {
rx_buf_offset_ += copysize;
}
LWIP_LOG("tcp_recved(%p %u)", pcb_, copysize);
tcp_recved(pcb_, copysize);
buf8 += copysize;
len -= copysize;
read += copysize;
}
return read;
}
ssize_t write(const void *buf, size_t len) override {
if (pcb_ == nullptr) {
errno = ECONNRESET;
return -1;
}
if (len == 0)
return 0;
if (buf == nullptr) {
errno = EINVAL;
return 0;
}
auto space = tcp_sndbuf(pcb_);
if (space == 0) {
errno = EWOULDBLOCK;
return -1;
}
size_t to_send = std::min((size_t) space, len);
LWIP_LOG("tcp_write(%p buf=%p %u)", pcb_, buf, to_send);
err_t err = tcp_write(pcb_, buf, to_send, TCP_WRITE_FLAG_COPY);
if (err == ERR_MEM) {
LWIP_LOG(" -> err ERR_MEM");
errno = EWOULDBLOCK;
return -1;
}
if (err != ERR_OK) {
LWIP_LOG(" -> err %d", err);
errno = ECONNRESET;
return -1;
}
if (tcp_nagle_disabled(pcb_)) {
LWIP_LOG("tcp_output(%p)", pcb_);
err = tcp_output(pcb_);
if (err == ERR_ABRT) {
LWIP_LOG(" -> err ERR_ABRT");
// sometimes lwip returns ERR_ABRT for no apparent reason
// the connection works fine afterwards, and back with ESPAsyncTCP we
// indirectly also ignored this error
// FIXME: figure out where this is returned and what it means in this context
return to_send;
}
if (err != ERR_OK) {
LWIP_LOG(" -> err %d", err);
errno = ECONNRESET;
return -1;
}
}
return to_send;
}
int setblocking(bool blocking) override {
if (pcb_ == nullptr) {
errno = ECONNRESET;
return -1;
}
if (blocking) {
// blocking operation not supported
errno = EINVAL;
return -1;
}
return 0;
}
err_t accept_fn(struct tcp_pcb *newpcb, err_t err) {
LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err);
if (err != ERR_OK || newpcb == nullptr) {
// "An error code if there has been an error accepting. Only return ERR_ABRT if you have
// called tcp_abort from within the callback function!"
// https://www.nongnu.org/lwip/2_1_x/tcp_8h.html#a00517abce6856d6c82f0efebdafb734d
// nothing to do here, we just don't push it to the queue
return ERR_OK;
}
auto *sock = new LWIPRawImpl(newpcb);
sock->init();
accepted_sockets_.emplace(sock);
return ERR_OK;
}
void err_fn(err_t err) {
LWIP_LOG("err(err=%d)", err);
// "If a connection is aborted because of an error, the application is alerted of this event by
// the err callback."
// pcb is already freed when this callback is called
// ERR_RST: connection was reset by remote host
// ERR_ABRT: aborted through tcp_abort or TCP timer
pcb_ = nullptr;
}
err_t recv_fn(struct pbuf *pb, err_t err) {
LWIP_LOG("recv(pb=%p err=%d)", pb, err);
if (err != 0) {
// "An error code if there has been an error receiving Only return ERR_ABRT if you have
// called tcp_abort from within the callback function!"
rx_closed_ = true;
return ERR_OK;
}
if (pb == nullptr) {
rx_closed_ = true;
return ERR_OK;
}
if (rx_buf_ == nullptr) {
// no need to copy because lwIP gave control of it to us
rx_buf_ = pb;
rx_buf_offset_ = 0;
} else {
pbuf_cat(rx_buf_, pb);
}
return ERR_OK;
}
static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) {
LWIPRawImpl *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
return arg_this->accept_fn(newpcb, err);
}
static void s_err_fn(void *arg, err_t err) {
LWIPRawImpl *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
arg_this->err_fn(err);
}
static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) {
LWIPRawImpl *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
return arg_this->recv_fn(pb, err);
}
protected:
struct tcp_pcb *pcb_;
std::queue<std::unique_ptr<LWIPRawImpl>> accepted_sockets_;
bool rx_closed_ = false;
pbuf *rx_buf_ = nullptr;
size_t rx_buf_offset_ = 0;
};
std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
auto *pcb = tcp_new();
if (pcb == nullptr)
return nullptr;
auto *sock = new LWIPRawImpl(pcb);
sock->init();
return std::unique_ptr<Socket>{sock};
}
} // namespace socket
} // namespace esphome
#endif // USE_SOCKET_IMPL_LWIP_TCP

View File

@ -0,0 +1,42 @@
#pragma once
#include <string>
#include <memory>
#include "headers.h"
#include "esphome/core/optional.h"
namespace esphome {
namespace socket {
class Socket {
public:
Socket() = default;
virtual ~Socket() = default;
Socket(const Socket &) = delete;
Socket &operator=(const Socket &) = delete;
virtual std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) = 0;
virtual int bind(const struct sockaddr *addr, socklen_t addrlen) = 0;
virtual int close() = 0;
// not supported yet:
// virtual int connect(const std::string &address) = 0;
// virtual int connect(const struct sockaddr *addr, socklen_t addrlen) = 0;
virtual int shutdown(int how) = 0;
virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0;
virtual std::string getpeername() = 0;
virtual int getsockname(struct sockaddr *addr, socklen_t *addrlen) = 0;
virtual std::string getsockname() = 0;
virtual int getsockopt(int level, int optname, void *optval, socklen_t *optlen) = 0;
virtual int setsockopt(int level, int optname, const void *optval, socklen_t optlen) = 0;
virtual int listen(int backlog) = 0;
virtual ssize_t read(void *buf, size_t len) = 0;
virtual ssize_t write(const void *buf, size_t len) = 0;
virtual int setblocking(bool blocking) = 0;
virtual int loop() { return 0; };
};
std::unique_ptr<Socket> socket(int domain, int type, int protocol);
} // namespace socket
} // namespace esphome

View File

@ -56,7 +56,10 @@ void SpeedFan::loop() {
ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable));
}
}
float SpeedFan::get_setup_priority() const { return setup_priority::DATA; }
// We need a higher priority than the FanState component to make sure that the traits are set
// when that component sets itself up.
float SpeedFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; }
} // namespace speed
} // namespace esphome

View File

@ -13,6 +13,9 @@ from esphome.const import (
CONF_PMC_4_0,
CONF_PMC_10_0,
CONF_PM_SIZE,
DEVICE_CLASS_PM1,
DEVICE_CLASS_PM10,
DEVICE_CLASS_PM25,
STATE_CLASS_MEASUREMENT,
UNIT_MICROGRAMS_PER_CUBIC_METER,
UNIT_COUNTS_PER_CUBIC_METER,
@ -35,12 +38,14 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=2,
device_class=DEVICE_CLASS_PM1,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=2,
device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_4_0): sensor.sensor_schema(
@ -53,6 +58,7 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=2,
device_class=DEVICE_CLASS_PM10,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PMC_0_5): sensor.sensor_schema(

View File

@ -29,7 +29,7 @@ CONFIG_SCHEMA = (
cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema,
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema,
cv.Required(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage,
}
)
@ -49,8 +49,9 @@ async def to_code(config):
reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
cg.add(var.set_reset_pin(reset))
bl = await cg.gpio_pin_expression(config[CONF_BACKLIGHT_PIN])
cg.add(var.set_backlight_pin(bl))
if CONF_BACKLIGHT_PIN in config:
bl = await cg.gpio_pin_expression(config[CONF_BACKLIGHT_PIN])
cg.add(var.set_backlight_pin(bl))
if CONF_LAMBDA in config:
lambda_ = await cg.process_lambda(

View File

View File

@ -0,0 +1,42 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import display, spi
from esphome.const import CONF_ID, CONF_LAMBDA, CONF_WIDTH, CONF_HEIGHT
AUTO_LOAD = ["display"]
CODEOWNERS = ["@marsjan155"]
DEPENDENCIES = ["spi"]
st7920_ns = cg.esphome_ns.namespace("st7920")
ST7920 = st7920_ns.class_(
"ST7920", cg.PollingComponent, display.DisplayBuffer, spi.SPIDevice
)
ST7920Ref = ST7920.operator("ref")
CONFIG_SCHEMA = (
display.FULL_DISPLAY_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(ST7920),
cv.Required(CONF_WIDTH): cv.int_,
cv.Required(CONF_HEIGHT): cv.int_,
}
)
.extend(cv.polling_component_schema("60s"))
.extend(spi.spi_device_schema())
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
if CONF_LAMBDA in config:
lambda_ = await cg.process_lambda(
config[CONF_LAMBDA], [(ST7920Ref, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))
cg.add(var.set_width(config[CONF_WIDTH]))
cg.add(var.set_height(config[CONF_HEIGHT]))
await display.register_display(var, config)

View File

@ -0,0 +1,146 @@
#include "st7920.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/components/display/display_buffer.h"
namespace esphome {
namespace st7920 {
static const char *const TAG = "st7920";
// ST7920 COMMANDS
static const uint8_t LCD_DATA = 0xFA;
static const uint8_t LCD_COMMAND = 0xF8;
static const uint8_t LCD_CLS = 0x01;
static const uint8_t LCD_HOME = 0x02;
static const uint8_t LCD_ADDRINC = 0x06;
static const uint8_t LCD_DISPLAYON = 0x0C;
static const uint8_t LCD_DISPLAYOFF = 0x08;
static const uint8_t LCD_CURSORON = 0x0E;
static const uint8_t LCD_CURSORBLINK = 0x0F;
static const uint8_t LCD_BASIC = 0x30;
static const uint8_t LCD_GFXMODE = 0x36;
static const uint8_t LCD_EXTEND = 0x34;
static const uint8_t LCD_TXTMODE = 0x34;
static const uint8_t LCD_STANDBY = 0x01;
static const uint8_t LCD_SCROLL = 0x03;
static const uint8_t LCD_SCROLLADDR = 0x40;
static const uint8_t LCD_ADDR = 0x80;
static const uint8_t LCD_LINE0 = 0x80;
static const uint8_t LCD_LINE1 = 0x90;
static const uint8_t LCD_LINE2 = 0x88;
static const uint8_t LCD_LINE3 = 0x98;
void ST7920::setup() {
ESP_LOGCONFIG(TAG, "Setting up ST7920...");
this->dump_config();
this->spi_setup();
this->init_internal_(this->get_buffer_length_());
display_init_();
}
void ST7920::command_(uint8_t value) {
this->enable();
this->send_(LCD_COMMAND, value);
this->disable();
}
void ST7920::data_(uint8_t value) {
this->enable();
this->send_(LCD_DATA, value);
this->disable();
}
void ST7920::send_(uint8_t type, uint8_t value) {
this->write_byte(type);
this->write_byte(value & 0xF0);
this->write_byte(value << 4);
}
void ST7920::goto_xy_(uint16_t x, uint16_t y) {
if (y >= 32 && y < 64) {
y -= 32;
x += 8;
} else if (y >= 64 && y < 64 + 32) {
y -= 32;
x += 0;
} else if (y >= 64 + 32 && y < 64 + 64) {
y -= 64;
x += 8;
}
this->command_(LCD_ADDR | y); // 6-bit (0..63)
this->command_(LCD_ADDR | x); // 4-bit (0..15)
}
void HOT ST7920::write_display_data() {
uint8_t i, j, b;
for (j = 0; j < this->get_height_internal() / 2; j++) {
this->goto_xy_(0, j);
this->enable();
for (i = 0; i < 16; i++) { // 16 bytes from line #0+
b = this->buffer_[i + j * 16];
this->send_(LCD_DATA, b);
}
for (i = 0; i < 16; i++) { // 16 bytes from line #32+
b = this->buffer_[i + (j + 32) * 16];
this->send_(LCD_DATA, b);
}
this->disable();
App.feed_wdt();
}
}
void ST7920::fill(Color color) { memset(this->buffer_, color.is_on() ? 0xFF : 0x00, this->get_buffer_length_()); }
void ST7920::dump_config() {
LOG_DISPLAY("", "ST7920", this);
LOG_PIN(" CS Pin: ", this->cs_);
ESP_LOGCONFIG(TAG, " Height: %d", this->height_);
ESP_LOGCONFIG(TAG, " Width: %d", this->width_);
}
float ST7920::get_setup_priority() const { return setup_priority::PROCESSOR; }
void ST7920::update() {
this->clear();
if (this->writer_local_.has_value()) // call lambda function if available
(*this->writer_local_)(*this);
this->write_display_data();
}
int ST7920::get_width_internal() { return this->width_; }
int ST7920::get_height_internal() { return this->height_; }
size_t ST7920::get_buffer_length_() {
return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u;
}
void HOT ST7920::draw_absolute_pixel_internal(int x, int y, Color color) {
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
ESP_LOGW(TAG, "Position out of area: %dx%d", x, y);
return;
}
int width = this->get_width_internal() / 8u;
if (color.is_on()) {
this->buffer_[y * width + x / 8] |= (0x80 >> (x & 7));
} else {
this->buffer_[y * width + x / 8] &= ~(0x80 >> (x & 7));
}
}
void ST7920::display_init_() {
ESP_LOGD(TAG, "Initializing display...");
this->command_(LCD_BASIC); // 8bit mode
this->command_(LCD_BASIC); // 8bit mode
this->command_(LCD_CLS); // clear screen
delay(12); // >10 ms delay
this->command_(LCD_ADDRINC); // cursor increment right no shift
this->command_(LCD_DISPLAYON); // D=1, C=0, B=0
this->command_(LCD_EXTEND); // LCD_EXTEND);
this->command_(LCD_GFXMODE); // LCD_GFXMODE);
this->write_display_data();
}
} // namespace st7920
} // namespace esphome

View File

@ -0,0 +1,50 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/spi/spi.h"
namespace esphome {
namespace st7920 {
class ST7920;
using st7920_writer_t = std::function<void(ST7920 &)>;
class ST7920 : public PollingComponent,
public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
spi::DATA_RATE_1MHZ> {
public:
void set_writer(st7920_writer_t &&writer) { this->writer_local_ = writer; }
void set_height(uint16_t height) { this->height_ = height; }
void set_width(uint16_t width) { this->width_ = width; }
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
void update() override;
void fill(Color color) override;
void write_display_data();
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;
int get_height_internal() override;
int get_width_internal() override;
size_t get_buffer_length_();
void display_init_();
void command_(uint8_t value);
void data_(uint8_t value);
void send_(uint8_t type, uint8_t value);
void goto_xy_(uint16_t x, uint16_t y);
void start_transaction_();
void end_transaction_();
int16_t width_ = 128, height_ = 64;
optional<st7920_writer_t> writer_local_{};
};
} // namespace st7920
} // namespace esphome

View File

@ -6,7 +6,7 @@ namespace t6615 {
static const char *const TAG = "t6615";
static const uint8_t T6615_RESPONSE_BUFFER_LENGTH = 32;
static const uint32_t T6615_TIMEOUT = 1000;
static const uint8_t T6615_MAGIC = 0xFF;
static const uint8_t T6615_ADDR_HOST = 0xFA;
static const uint8_t T6615_ADDR_SENSOR = 0xFE;
@ -19,31 +19,49 @@ static const uint8_t T6615_COMMAND_ENABLE_ABC[] = {0xB7, 0x01};
static const uint8_t T6615_COMMAND_DISABLE_ABC[] = {0xB7, 0x02};
static const uint8_t T6615_COMMAND_SET_ELEVATION[] = {0x03, 0x0F};
void T6615Component::loop() {
if (!this->available())
return;
void T6615Component::send_ppm_command_() {
this->command_time_ = millis();
this->command_ = T6615Command::GET_PPM;
this->write_byte(T6615_MAGIC);
this->write_byte(T6615_ADDR_SENSOR);
this->write_byte(sizeof(T6615_COMMAND_GET_PPM));
this->write_array(T6615_COMMAND_GET_PPM, sizeof(T6615_COMMAND_GET_PPM));
}
// Read header
uint8_t header[3];
this->read_array(header, 3);
if (header[0] != T6615_MAGIC || header[1] != T6615_ADDR_HOST) {
ESP_LOGW(TAG, "Reading data from T6615 failed!");
while (this->available())
this->read(); // Clear the incoming buffer
this->status_set_warning();
void T6615Component::loop() {
if (this->available() < 5) {
if (this->command_ == T6615Command::GET_PPM && millis() - this->command_time_ > T6615_TIMEOUT) {
/* command got eaten, clear the buffer and fire another */
while (this->available())
this->read();
this->send_ppm_command_();
}
return;
}
// Read body
uint8_t length = header[2];
uint8_t response[T6615_RESPONSE_BUFFER_LENGTH];
this->read_array(response, length);
uint8_t response_buffer[6];
/* by the time we get here, we know we have at least five bytes in the buffer */
this->read_array(response_buffer, 5);
// Read header
if (response_buffer[0] != T6615_MAGIC || response_buffer[1] != T6615_ADDR_HOST) {
ESP_LOGW(TAG, "Got bad data from T6615! Magic was %02X and address was %02X", response_buffer[0],
response_buffer[1]);
/* make sure the buffer is empty */
while (this->available())
this->read();
/* try again to read the sensor */
this->send_ppm_command_();
this->status_set_warning();
return;
}
this->status_clear_warning();
switch (this->command_) {
case T6615Command::GET_PPM: {
const uint16_t ppm = encode_uint16(response[0], response[1]);
const uint16_t ppm = encode_uint16(response_buffer[3], response_buffer[4]);
ESP_LOGD(TAG, "T6615 Received CO₂=%uppm", ppm);
this->co2_sensor_->publish_state(ppm);
break;
@ -51,23 +69,19 @@ void T6615Component::loop() {
default:
break;
}
this->command_time_ = 0;
this->command_ = T6615Command::NONE;
}
void T6615Component::update() { this->query_ppm_(); }
void T6615Component::query_ppm_() {
if (this->co2_sensor_ == nullptr || this->command_ != T6615Command::NONE) {
if (this->co2_sensor_ == nullptr ||
(this->command_ != T6615Command::NONE && millis() - this->command_time_ < T6615_TIMEOUT)) {
return;
}
this->command_ = T6615Command::GET_PPM;
this->write_byte(T6615_MAGIC);
this->write_byte(T6615_ADDR_SENSOR);
this->write_byte(sizeof(T6615_COMMAND_GET_PPM));
this->write_array(T6615_COMMAND_GET_PPM, sizeof(T6615_COMMAND_GET_PPM));
this->send_ppm_command_();
}
float T6615Component::get_setup_priority() const { return setup_priority::DATA; }

View File

@ -32,8 +32,10 @@ class T6615Component : public PollingComponent, public uart::UARTDevice {
protected:
void query_ppm_();
void send_ppm_command_();
T6615Command command_ = T6615Command::NONE;
unsigned long command_time_ = 0;
sensor::Sensor *co2_sensor_{nullptr};
};

View File

@ -29,12 +29,16 @@ def validate_min_max(config):
def validate(config):
if CONF_LAMBDA in config:
if CONF_OPTIMISTIC in config:
if config[CONF_OPTIMISTIC]:
raise cv.Invalid("optimistic cannot be used with lambda")
if CONF_INITIAL_VALUE in config:
raise cv.Invalid("initial_value cannot be used with lambda")
if CONF_RESTORE_VALUE in config:
raise cv.Invalid("restore_value cannot be used with lambda")
if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config:
raise cv.Invalid(
"Either optimistic mode must be enabled, or set_action must be set, to handle the number being set."
)
return config
@ -46,7 +50,7 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_MIN_VALUE): cv.float_,
cv.Required(CONF_STEP): cv.positive_float,
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_OPTIMISTIC): cv.boolean,
cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_INITIAL_VALUE): cv.float_,
cv.Optional(CONF_RESTORE_VALUE): cv.boolean,
@ -75,8 +79,7 @@ async def to_code(config):
cg.add(var.set_template(template_))
else:
if CONF_OPTIMISTIC in config:
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
if CONF_INITIAL_VALUE in config:
cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE]))
if CONF_RESTORE_VALUE in config:

View File

@ -19,14 +19,26 @@ TemplateSelect = template_ns.class_(
CONF_SET_ACTION = "set_action"
def validate_initial_value_in_options(config):
if CONF_INITIAL_OPTION in config:
def validate(config):
if CONF_LAMBDA in config:
if config[CONF_OPTIMISTIC]:
raise cv.Invalid("optimistic cannot be used with lambda")
if CONF_INITIAL_OPTION in config:
raise cv.Invalid("initial_value cannot be used with lambda")
if CONF_RESTORE_VALUE in config:
raise cv.Invalid("restore_value cannot be used with lambda")
elif CONF_INITIAL_OPTION in config:
if config[CONF_INITIAL_OPTION] not in config[CONF_OPTIONS]:
raise cv.Invalid(
f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]"
)
else:
config[CONF_INITIAL_OPTION] = config[CONF_OPTIONS][0]
if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config:
raise cv.Invalid(
"Either optimistic mode must be enabled, or set_action must be set, to handle the option being set."
)
return config
@ -38,13 +50,13 @@ CONFIG_SCHEMA = cv.All(
cv.ensure_list(cv.string_strict), cv.Length(min=1)
),
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_OPTIMISTIC): cv.boolean,
cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_INITIAL_OPTION): cv.string_strict,
cv.Optional(CONF_RESTORE_VALUE): cv.boolean,
}
).extend(cv.polling_component_schema("60s")),
validate_initial_value_in_options,
validate,
)
@ -60,9 +72,7 @@ async def to_code(config):
cg.add(var.set_template(template_))
else:
if CONF_OPTIMISTIC in config:
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
cg.add(var.set_initial_option(config[CONF_INITIAL_OPTION]))
if CONF_RESTORE_VALUE in config:

View File

@ -11,7 +11,7 @@ void TemplateSelect::setup() {
return;
std::string value;
ESP_LOGD(TAG, "Setting up Template Number");
ESP_LOGD(TAG, "Setting up Template Select");
if (!this->restore_value_) {
value = this->initial_option_;
ESP_LOGD(TAG, "State from initial: %s", value.c_str());

View File

@ -7,12 +7,13 @@ namespace template_ {
static const char *const TAG = "template.sensor";
void TemplateSensor::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (val.has_value()) {
this->publish_state(*val);
if (this->f_.has_value()) {
auto val = (*this->f_)();
if (val.has_value()) {
this->publish_state(*val);
}
} else if (!isnan(this->get_raw_state())) {
this->publish_state(this->get_raw_state());
}
}
float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; }

View File

@ -16,17 +16,38 @@ from .. import template_ns
TemplateSwitch = template_ns.class_("TemplateSwitch", switch.Switch, cg.Component)
CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(TemplateSwitch),
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean,
cv.Optional(CONF_TURN_OFF_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_RESTORE_STATE, default=False): cv.boolean,
}
).extend(cv.COMPONENT_SCHEMA)
def validate(config):
if (
not config[CONF_OPTIMISTIC]
and CONF_TURN_ON_ACTION not in config
and CONF_TURN_OFF_ACTION not in config
):
raise cv.Invalid(
"Either optimistic mode must be enabled, or turn_on_action or turn_off_action must be set, "
"to handle the switch being set."
)
return config
CONFIG_SCHEMA = cv.All(
switch.SWITCH_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(TemplateSwitch),
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean,
cv.Optional(CONF_TURN_OFF_ACTION): automation.validate_automation(
single=True
),
cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation(
single=True
),
cv.Optional(CONF_RESTORE_STATE, default=False): cv.boolean,
}
).extend(cv.COMPONENT_SCHEMA),
validate,
)
async def to_code(config):

View File

@ -7,12 +7,13 @@ namespace template_ {
static const char *const TAG = "template.text_sensor";
void TemplateTextSensor::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (val.has_value()) {
this->publish_state(*val);
if (this->f_.has_value()) {
auto val = (*this->f_)();
if (val.has_value()) {
this->publish_state(*val);
}
} else if (this->has_state()) {
this->publish_state(this->state);
}
}
float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; }

View File

@ -35,9 +35,8 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
}
auto time = this->now();
char buf[128];
time.strftime(buf, sizeof(buf), "%c");
ESP_LOGD(TAG, "Synchronized time: %s", buf);
ESP_LOGD(TAG, "Synchronized time: %d-%d-%d %d:%d:%d", time.year, time.month, time.day_of_month, time.hour,
time.minute, time.second);
this->time_sync_callback_.call();
}

View File

@ -5,8 +5,8 @@ from esphome.const import (
CONF_ID,
CONF_TIME_ID,
DEVICE_CLASS_ENERGY,
LAST_RESET_TYPE_AUTO,
STATE_CLASS_MEASUREMENT,
CONF_METHOD,
STATE_CLASS_TOTAL_INCREASING,
)
DEPENDENCIES = ["time"]
@ -14,6 +14,12 @@ DEPENDENCIES = ["time"]
CONF_POWER_ID = "power_id"
CONF_MIN_SAVE_INTERVAL = "min_save_interval"
total_daily_energy_ns = cg.esphome_ns.namespace("total_daily_energy")
TotalDailyEnergyMethod = total_daily_energy_ns.enum("TotalDailyEnergyMethod")
TOTAL_DAILY_ENERGY_METHODS = {
"trapezoid": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID,
"left": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_LEFT,
"right": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_RIGHT,
}
TotalDailyEnergy = total_daily_energy_ns.class_(
"TotalDailyEnergy", sensor.Sensor, cg.Component
)
@ -21,8 +27,7 @@ TotalDailyEnergy = total_daily_energy_ns.class_(
CONFIG_SCHEMA = (
sensor.sensor_schema(
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
last_reset_type=LAST_RESET_TYPE_AUTO,
state_class=STATE_CLASS_TOTAL_INCREASING,
)
.extend(
{
@ -32,6 +37,9 @@ CONFIG_SCHEMA = (
cv.Optional(
CONF_MIN_SAVE_INTERVAL, default="0s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_METHOD, default="right"): cv.enum(
TOTAL_DAILY_ENERGY_METHODS, lower=True
),
}
)
.extend(cv.COMPONENT_SCHEMA)
@ -49,3 +57,4 @@ async def to_code(config):
time_ = await cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_time(time_))
cg.add(var.set_min_save_interval(config[CONF_MIN_SAVE_INTERVAL]))
cg.add(var.set_method(config[CONF_METHOD]))

View File

@ -20,7 +20,9 @@ void TotalDailyEnergy::setup() {
this->parent_->add_on_state_callback([this](float state) { this->process_new_state_(state); });
}
void TotalDailyEnergy::dump_config() { LOG_SENSOR("", "Total Daily Energy", this); }
void TotalDailyEnergy::loop() {
auto t = this->time_->now();
if (!t.is_valid())
@ -37,6 +39,7 @@ void TotalDailyEnergy::loop() {
this->publish_state_and_save(0);
}
}
void TotalDailyEnergy::publish_state_and_save(float state) {
this->total_energy_ = state;
this->publish_state(state);
@ -47,13 +50,29 @@ void TotalDailyEnergy::publish_state_and_save(float state) {
this->last_save_ = now;
this->pref_.save(&state);
}
void TotalDailyEnergy::process_new_state_(float state) {
if (isnan(state))
return;
const uint32_t now = millis();
const float old_state = this->last_power_state_;
const float new_state = state;
float delta_hours = (now - this->last_update_) / 1000.0f / 60.0f / 60.0f;
float delta_energy = 0.0f;
switch (this->method_) {
case TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID:
delta_energy = delta_hours * (old_state + new_state) / 2.0;
break;
case TOTAL_DAILY_ENERGY_METHOD_LEFT:
delta_energy = delta_hours * old_state;
break;
case TOTAL_DAILY_ENERGY_METHOD_RIGHT:
delta_energy = delta_hours * new_state;
break;
}
this->last_power_state_ = new_state;
this->last_update_ = now;
this->publish_state_and_save(this->total_energy_ + state * delta_hours);
this->publish_state_and_save(this->total_energy_ + delta_energy);
}
} // namespace total_daily_energy

View File

@ -8,11 +8,18 @@
namespace esphome {
namespace total_daily_energy {
enum TotalDailyEnergyMethod {
TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID = 0,
TOTAL_DAILY_ENERGY_METHOD_LEFT,
TOTAL_DAILY_ENERGY_METHOD_RIGHT,
};
class TotalDailyEnergy : public sensor::Sensor, public Component {
public:
void set_min_save_interval(uint32_t min_interval) { this->min_save_interval_ = min_interval; }
void set_time(time::RealTimeClock *time) { time_ = time; }
void set_parent(Sensor *parent) { parent_ = parent; }
void set_method(TotalDailyEnergyMethod method) { method_ = method; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
@ -29,11 +36,13 @@ class TotalDailyEnergy : public sensor::Sensor, public Component {
ESPPreferenceObject pref_;
time::RealTimeClock *time_;
Sensor *parent_;
TotalDailyEnergyMethod method_;
uint16_t last_day_of_year_{};
uint32_t last_update_{0};
uint32_t last_save_{0};
uint32_t min_save_interval_{0};
float total_energy_{0.0f};
float last_power_state_{0.0f};
};
} // namespace total_daily_energy

View File

@ -84,5 +84,9 @@ void TuyaFan::write_state() {
}
}
// We need a higher priority than the FanState component to make sure that the traits are set
// when that component sets itself up.
float TuyaFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; }
} // namespace tuya
} // namespace esphome

View File

@ -11,6 +11,7 @@ class TuyaFan : public Component {
public:
TuyaFan(Tuya *parent, fan::FanState *fan, int speed_count) : parent_(parent), fan_(fan), speed_count_(speed_count) {}
void setup() override;
float get_setup_priority() const override;
void dump_config() override;
void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; }
void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; }

View File

@ -118,6 +118,11 @@ class UARTComponent : public Component, public Stream {
uint8_t stop_bits_;
uint8_t data_bits_;
UARTParityOptions parity_;
private:
#ifdef ARDUINO_ARCH_ESP8266
static bool serial0InUse;
#endif
};
#ifdef ARDUINO_ARCH_ESP32

View File

@ -73,7 +73,11 @@ void UARTComponent::setup() {
// Use Arduino HardwareSerial UARTs if all used pins match the ones
// preconfigured by the platform. For example if RX disabled but TX pin
// is 1 we still want to use Serial.
#ifdef CONFIG_IDF_TARGET_ESP32C3
if (this->tx_pin_.value_or(21) == 21 && this->rx_pin_.value_or(20) == 20) {
#else
if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) {
#endif
this->hw_serial_ = &Serial;
} else {
this->hw_serial_ = new HardwareSerial(next_uart_num++);

View File

@ -4,11 +4,17 @@
#include "esphome/core/helpers.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
namespace esphome {
namespace uart {
static const char *const TAG = "uart_esp8266";
bool UARTComponent::serial0InUse = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
uint32_t UARTComponent::get_config() {
uint32_t config = 0;
@ -49,15 +55,31 @@ void UARTComponent::setup() {
// is 1 we still want to use Serial.
SerialConfig config = static_cast<SerialConfig>(get_config());
if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) {
if (!UARTComponent::serial0InUse && this->tx_pin_.value_or(1) == 1 &&
this->rx_pin_.value_or(3) == 3
#ifdef USE_LOGGER
// we will use UART0 if logger isn't using it in swapped mode
&& (logger::global_logger->get_hw_serial() == nullptr ||
logger::global_logger->get_uart() != logger::UART_SELECTION_UART0_SWAP)
#endif
) {
this->hw_serial_ = &Serial;
this->hw_serial_->begin(this->baud_rate_, config);
this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
} else if (this->tx_pin_.value_or(15) == 15 && this->rx_pin_.value_or(13) == 13) {
UARTComponent::serial0InUse = true;
} else if (!UARTComponent::serial0InUse && this->tx_pin_.value_or(15) == 15 &&
this->rx_pin_.value_or(13) == 13
#ifdef USE_LOGGER
// we will use UART0 swapped if logger isn't using it in regular mode
&& (logger::global_logger->get_hw_serial() == nullptr ||
logger::global_logger->get_uart() != logger::UART_SELECTION_UART0)
#endif
) {
this->hw_serial_ = &Serial;
this->hw_serial_->begin(this->baud_rate_, config);
this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
this->hw_serial_->swap();
UARTComponent::serial0InUse = true;
} else if (this->tx_pin_.value_or(2) == 2 && this->rx_pin_.value_or(8) == 8) {
this->hw_serial_ = &Serial1;
this->hw_serial_->begin(this->baud_rate_, config);

View File

@ -3,7 +3,7 @@ import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
CONF_ID,
STATE_CLASS_NONE,
STATE_CLASS_TOTAL_INCREASING,
UNIT_SECOND,
ICON_TIMER,
)
@ -16,7 +16,7 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_SECOND,
icon=ICON_TIMER,
accuracy_decimals=0,
state_class=STATE_CLASS_NONE,
state_class=STATE_CLASS_TOTAL_INCREASING,
)
.extend(
{

View File

@ -13,7 +13,7 @@ from esphome.const import (
CONF_USERNAME,
CONF_PASSWORD,
)
from esphome.core import coroutine_with_priority
from esphome.core import CORE, coroutine_with_priority
AUTO_LOAD = ["json", "web_server_base"]
@ -61,9 +61,11 @@ async def to_code(config):
cg.add(var.set_password(config[CONF_AUTH][CONF_PASSWORD]))
if CONF_CSS_INCLUDE in config:
cg.add_define("WEBSERVER_CSS_INCLUDE")
with open(config[CONF_CSS_INCLUDE], "r") as myfile:
path = CORE.relative_config_path(config[CONF_CSS_INCLUDE])
with open(file=path, mode="r", encoding="utf-8") as myfile:
cg.add(var.set_css_include(myfile.read()))
if CONF_JS_INCLUDE in config:
cg.add_define("WEBSERVER_JS_INCLUDE")
with open(config[CONF_JS_INCLUDE], "r") as myfile:
path = CORE.relative_config_path(config[CONF_JS_INCLUDE])
with open(file=path, mode="r", encoding="utf-8") as myfile:
cg.add(var.set_js_include(myfile.read()))

View File

@ -397,14 +397,15 @@ std::string WebServer::fan_json(fan::FanState *obj) {
const auto traits = obj->get_traits();
if (traits.supports_speed()) {
root["speed_level"] = obj->speed;
// NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations)
switch (fan::speed_level_to_enum(obj->speed, traits.supported_speed_count())) {
case fan::FAN_SPEED_LOW:
case fan::FAN_SPEED_LOW: // NOLINT(clang-diagnostic-deprecated-declarations)
root["speed"] = "low";
break;
case fan::FAN_SPEED_MEDIUM:
case fan::FAN_SPEED_MEDIUM: // NOLINT(clang-diagnostic-deprecated-declarations)
root["speed"] = "medium";
break;
case fan::FAN_SPEED_HIGH:
case fan::FAN_SPEED_HIGH: // NOLINT(clang-diagnostic-deprecated-declarations)
root["speed"] = "high";
break;
}
@ -430,7 +431,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
auto call = obj->turn_on();
if (request->hasParam("speed")) {
String speed = request->getParam("speed")->value();
call.set_speed(speed.c_str());
call.set_speed(speed.c_str()); // NOLINT(clang-diagnostic-deprecated-declarations)
}
if (request->hasParam("speed_level")) {
String speed_level = request->getParam("speed_level")->value();

View File

@ -305,7 +305,7 @@ def wifi_network(config, static_ip):
cg.add(ap.set_password(config[CONF_PASSWORD]))
if CONF_EAP in config:
cg.add(ap.set_eap(eap_auth(config[CONF_EAP])))
cg.add_define("ESPHOME_WIFI_WPA2_EAP")
cg.add_define("USE_WIFI_WPA2_EAP")
if CONF_BSSID in config:
cg.add(ap.set_bssid([HexInt(i) for i in config[CONF_BSSID].parts]))
if CONF_HIDDEN in config:

View File

@ -244,6 +244,8 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa
sta.set_ssid(ssid);
sta.set_password(password);
this->set_sta(sta);
this->start_scanning();
}
void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
@ -258,7 +260,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
ESP_LOGV(TAG, " BSSID: Not Set");
}
#ifdef ESPHOME_WIFI_WPA2_EAP
#ifdef USE_WIFI_WPA2_EAP
if (ap.get_eap().has_value()) {
ESP_LOGV(TAG, " WPA2 Enterprise authentication configured:");
EAPAuth eap_config = ap.get_eap().value();
@ -274,7 +276,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
} else {
#endif
ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str());
#ifdef ESPHOME_WIFI_WPA2_EAP
#ifdef USE_WIFI_WPA2_EAP
}
#endif
if (ap.get_channel().has_value()) {
@ -478,7 +480,7 @@ void WiFiComponent::check_scanning_finished() {
// copy manual IP (if set)
connect_params.set_manual_ip(config.get_manual_ip());
#ifdef ESPHOME_WIFI_WPA2_EAP
#ifdef USE_WIFI_WPA2_EAP
// copy EAP parameters (if set)
connect_params.set_eap(config.get_eap());
#endif
@ -638,8 +640,8 @@ void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; }
void WiFiAP::set_bssid(optional<bssid_t> bssid) { this->bssid_ = bssid; }
void WiFiAP::set_password(const std::string &password) { this->password_ = password; }
#ifdef ESPHOME_WIFI_WPA2_EAP
void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = eap_auth; }
#ifdef USE_WIFI_WPA2_EAP
void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
#endif
void WiFiAP::set_channel(optional<uint8_t> channel) { this->channel_ = channel; }
void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = std::move(manual_ip); }
@ -647,7 +649,7 @@ void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
const optional<bssid_t> &WiFiAP::get_bssid() const { return this->bssid_; }
const std::string &WiFiAP::get_password() const { return this->password_; }
#ifdef ESPHOME_WIFI_WPA2_EAP
#ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
#endif
const optional<uint8_t> &WiFiAP::get_channel() const { return this->channel_; }
@ -679,7 +681,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) {
if (config.get_bssid().has_value() && *config.get_bssid() != this->bssid_)
return false;
#ifdef ESPHOME_WIFI_WPA2_EAP
#ifdef USE_WIFI_WPA2_EAP
// BSSID requires auth but no PSK or EAP credentials given
if (this->with_auth_ && (config.get_password().empty() && !config.get_eap().has_value()))
return false;

View File

@ -1,5 +1,6 @@
#pragma once
#include "esphome/core/macros.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/automation.h"
@ -17,7 +18,7 @@
#include <ESP8266WiFiType.h>
#include <ESP8266WiFi.h>
#ifdef ARDUINO_ESP8266_RELEASE_2_3_0
#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0)
extern "C" {
#include <user_interface.h>
};
@ -62,7 +63,7 @@ struct ManualIP {
IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default.
};
#ifdef ESPHOME_WIFI_WPA2_EAP
#ifdef USE_WIFI_WPA2_EAP
struct EAPAuth {
std::string identity; // required for all auth types
std::string username;
@ -72,7 +73,7 @@ struct EAPAuth {
const char *client_cert;
const char *client_key;
};
#endif // ESPHOME_WIFI_WPA2_EAP
#endif // USE_WIFI_WPA2_EAP
using bssid_t = std::array<uint8_t, 6>;
@ -82,9 +83,9 @@ class WiFiAP {
void set_bssid(bssid_t bssid);
void set_bssid(optional<bssid_t> bssid);
void set_password(const std::string &password);
#ifdef ESPHOME_WIFI_WPA2_EAP
#ifdef USE_WIFI_WPA2_EAP
void set_eap(optional<EAPAuth> eap_auth);
#endif // ESPHOME_WIFI_WPA2_EAP
#endif // USE_WIFI_WPA2_EAP
void set_channel(optional<uint8_t> channel);
void set_priority(float priority) { priority_ = priority; }
void set_manual_ip(optional<ManualIP> manual_ip);
@ -92,9 +93,9 @@ class WiFiAP {
const std::string &get_ssid() const;
const optional<bssid_t> &get_bssid() const;
const std::string &get_password() const;
#ifdef ESPHOME_WIFI_WPA2_EAP
#ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &get_eap() const;
#endif // ESPHOME_WIFI_WPA2_EAP
#endif // USE_WIFI_WPA2_EAP
const optional<uint8_t> &get_channel() const;
float get_priority() const { return priority_; }
const optional<ManualIP> &get_manual_ip() const;
@ -104,9 +105,9 @@ class WiFiAP {
std::string ssid_;
optional<bssid_t> bssid_;
std::string password_;
#ifdef ESPHOME_WIFI_WPA2_EAP
#ifdef USE_WIFI_WPA2_EAP
optional<EAPAuth> eap_;
#endif // ESPHOME_WIFI_WPA2_EAP
#endif // USE_WIFI_WPA2_EAP
optional<uint8_t> channel_;
float priority_{0};
optional<ManualIP> manual_ip_;

View File

@ -6,11 +6,12 @@
#include <utility>
#include <algorithm>
#ifdef ESPHOME_WIFI_WPA2_EAP
#ifdef USE_WIFI_WPA2_EAP
#include <esp_wpa2.h>
#endif
#include "lwip/err.h"
#include "lwip/dns.h"
#include "lwip/apps/sntp.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@ -92,6 +93,11 @@ bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
tcpip_adapter_dhcp_status_t dhcp_status;
tcpip_adapter_dhcpc_get_status(TCPIP_ADAPTER_IF_STA, &dhcp_status);
if (!manual_ip.has_value()) {
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
// https://github.com/esphome/issues/issues/2299
sntp_servermode_dhcp(false);
// Use DHCP client
if (dhcp_status != TCPIP_ADAPTER_DHCP_STARTED) {
esp_err_t err = tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_STA);
@ -163,7 +169,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK;
}
#ifdef ESPHOME_WIFI_WPA2_EAP
#ifdef USE_WIFI_WPA2_EAP
if (ap.get_eap().has_value()) {
conf.sta.threshold.authmode = WIFI_AUTH_WPA2_ENTERPRISE;
}
@ -220,7 +226,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
}
// setup enterprise authentication if required
#ifdef ESPHOME_WIFI_WPA2_EAP
#ifdef USE_WIFI_WPA2_EAP
if (ap.get_eap().has_value()) {
// note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
EAPAuth eap = ap.get_eap().value();
@ -264,7 +270,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err);
}
}
#endif // ESPHOME_WIFI_WPA2_EAP
#endif // USE_WIFI_WPA2_EAP
this->wifi_apply_hostname_();

View File

@ -1,4 +1,5 @@
#include "wifi_component.h"
#include "esphome/core/macros.h"
#ifdef ARDUINO_ARCH_ESP8266
@ -6,23 +7,20 @@
#include <utility>
#include <algorithm>
#ifdef ESPHOME_WIFI_WPA2_EAP
#ifdef USE_WIFI_WPA2_EAP
#include <wpa2_enterprise.h>
#endif
#ifdef WIFI_IS_OFF_AT_BOOT // Identifies ESP8266 Arduino 3.0.0
#define ARDUINO_ESP8266_RELEASE_3
#endif
extern "C" {
#include "lwip/err.h"
#include "lwip/dns.h"
#include "lwip/dhcp.h"
#include "lwip/init.h" // LWIP_VERSION_
#include "lwip/apps/sntp.h"
#if LWIP_IPV6
#include "lwip/netif.h" // struct netif
#endif
#ifdef ARDUINO_ESP8266_RELEASE_3
#if ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0)
#include "LwipDhcpServer.h"
#define wifi_softap_set_dhcps_lease(lease) dhcpSoftAP.set_dhcps_lease(lease)
#define wifi_softap_set_dhcps_lease_time(time) dhcpSoftAP.set_dhcps_lease_time(time)
@ -115,6 +113,11 @@ bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
enum dhcp_status dhcp_status = wifi_station_dhcpc_status();
if (!manual_ip.has_value()) {
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
// https://github.com/esphome/issues/issues/2299
sntp_servermode_dhcp(false);
// Use DHCP client
if (dhcp_status != DHCP_STARTED) {
bool ret = wifi_station_dhcpc_start();
@ -229,7 +232,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
conf.bssid_set = 0;
}
#ifndef ARDUINO_ESP8266_RELEASE_2_3_0
#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
if (ap.get_password().empty()) {
conf.threshold.authmode = AUTH_OPEN;
} else {
@ -253,7 +256,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
}
// setup enterprise authentication if required
#ifdef ESPHOME_WIFI_WPA2_EAP
#ifdef USE_WIFI_WPA2_EAP
if (ap.get_eap().has_value()) {
// note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
EAPAuth eap = ap.get_eap().value();
@ -296,7 +299,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", ret);
}
}
#endif // ESPHOME_WIFI_WPA2_EAP
#endif // USE_WIFI_WPA2_EAP
this->wifi_apply_hostname_();
@ -369,65 +372,75 @@ const char *get_op_mode_str(uint8_t mode) {
return "UNKNOWN";
}
}
// Note that this method returns PROGMEM strings, so use LOG_STR_ARG() to access them.
const char *get_disconnect_reason_str(uint8_t reason) {
/* If this were one big switch statement, GCC would generate a lookup table for it. However, the values of the
* REASON_* constants aren't continuous, and GCC will fill in the gap with the default value -- wasting 4 bytes of RAM
* per entry. As there's ~175 default entries, this wastes 700 bytes of RAM.
*/
if (reason <= REASON_CIPHER_SUITE_REJECTED) { // This must be the last constant with a value <200
switch (reason) {
case REASON_AUTH_EXPIRE:
return LOG_STR("Auth Expired");
case REASON_AUTH_LEAVE:
return LOG_STR("Auth Leave");
case REASON_ASSOC_EXPIRE:
return LOG_STR("Association Expired");
case REASON_ASSOC_TOOMANY:
return LOG_STR("Too Many Associations");
case REASON_NOT_AUTHED:
return LOG_STR("Not Authenticated");
case REASON_NOT_ASSOCED:
return LOG_STR("Not Associated");
case REASON_ASSOC_LEAVE:
return LOG_STR("Association Leave");
case REASON_ASSOC_NOT_AUTHED:
return LOG_STR("Association not Authenticated");
case REASON_DISASSOC_PWRCAP_BAD:
return LOG_STR("Disassociate Power Cap Bad");
case REASON_DISASSOC_SUPCHAN_BAD:
return LOG_STR("Disassociate Supported Channel Bad");
case REASON_IE_INVALID:
return LOG_STR("IE Invalid");
case REASON_MIC_FAILURE:
return LOG_STR("Mic Failure");
case REASON_4WAY_HANDSHAKE_TIMEOUT:
return LOG_STR("4-Way Handshake Timeout");
case REASON_GROUP_KEY_UPDATE_TIMEOUT:
return LOG_STR("Group Key Update Timeout");
case REASON_IE_IN_4WAY_DIFFERS:
return LOG_STR("IE In 4-Way Handshake Differs");
case REASON_GROUP_CIPHER_INVALID:
return LOG_STR("Group Cipher Invalid");
case REASON_PAIRWISE_CIPHER_INVALID:
return LOG_STR("Pairwise Cipher Invalid");
case REASON_AKMP_INVALID:
return LOG_STR("AKMP Invalid");
case REASON_UNSUPP_RSN_IE_VERSION:
return LOG_STR("Unsupported RSN IE version");
case REASON_INVALID_RSN_IE_CAP:
return LOG_STR("Invalid RSN IE Cap");
case REASON_802_1X_AUTH_FAILED:
return LOG_STR("802.1x Authentication Failed");
case REASON_CIPHER_SUITE_REJECTED:
return LOG_STR("Cipher Suite Rejected");
}
}
switch (reason) {
case REASON_AUTH_EXPIRE:
return "Auth Expired";
case REASON_AUTH_LEAVE:
return "Auth Leave";
case REASON_ASSOC_EXPIRE:
return "Association Expired";
case REASON_ASSOC_TOOMANY:
return "Too Many Associations";
case REASON_NOT_AUTHED:
return "Not Authenticated";
case REASON_NOT_ASSOCED:
return "Not Associated";
case REASON_ASSOC_LEAVE:
return "Association Leave";
case REASON_ASSOC_NOT_AUTHED:
return "Association not Authenticated";
case REASON_DISASSOC_PWRCAP_BAD:
return "Disassociate Power Cap Bad";
case REASON_DISASSOC_SUPCHAN_BAD:
return "Disassociate Supported Channel Bad";
case REASON_IE_INVALID:
return "IE Invalid";
case REASON_MIC_FAILURE:
return "Mic Failure";
case REASON_4WAY_HANDSHAKE_TIMEOUT:
return "4-Way Handshake Timeout";
case REASON_GROUP_KEY_UPDATE_TIMEOUT:
return "Group Key Update Timeout";
case REASON_IE_IN_4WAY_DIFFERS:
return "IE In 4-Way Handshake Differs";
case REASON_GROUP_CIPHER_INVALID:
return "Group Cipher Invalid";
case REASON_PAIRWISE_CIPHER_INVALID:
return "Pairwise Cipher Invalid";
case REASON_AKMP_INVALID:
return "AKMP Invalid";
case REASON_UNSUPP_RSN_IE_VERSION:
return "Unsupported RSN IE version";
case REASON_INVALID_RSN_IE_CAP:
return "Invalid RSN IE Cap";
case REASON_802_1X_AUTH_FAILED:
return "802.1x Authentication Failed";
case REASON_CIPHER_SUITE_REJECTED:
return "Cipher Suite Rejected";
case REASON_BEACON_TIMEOUT:
return "Beacon Timeout";
return LOG_STR("Beacon Timeout");
case REASON_NO_AP_FOUND:
return "AP Not Found";
return LOG_STR("AP Not Found");
case REASON_AUTH_FAIL:
return "Authentication Failed";
return LOG_STR("Authentication Failed");
case REASON_ASSOC_FAIL:
return "Association Failed";
return LOG_STR("Association Failed");
case REASON_HANDSHAKE_TIMEOUT:
return "Handshake Failed";
return LOG_STR("Handshake Failed");
case REASON_UNSPECIFIED:
default:
return "Unspecified";
return LOG_STR("Unspecified");
}
}
@ -451,7 +464,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
} else {
ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
format_mac_addr(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason)));
}
break;
}
@ -495,7 +508,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
break;
}
#ifndef ARDUINO_ESP8266_RELEASE_2_3_0
#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
case EVENT_OPMODE_CHANGED: {
auto it = event->event_info.opmode_changed;
ESP_LOGV(TAG, "Event: Changed Mode old=%s new=%s", get_op_mode_str(it.old_opmode),
@ -580,7 +593,7 @@ bool WiFiComponent::wifi_scan_start_() {
config.bssid = nullptr;
config.channel = 0;
config.show_hidden = 1;
#ifndef ARDUINO_ESP8266_RELEASE_2_3_0
#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
config.scan_type = WIFI_SCAN_TYPE_ACTIVE;
if (FIRST_SCAN) {
config.scan_time.active.min = 100;
@ -659,7 +672,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
return false;
}
#ifdef ARDUINO_ESP8266_RELEASE_3
#if ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0)
dhcpSoftAP.begin(&info);
#endif

View File

@ -42,6 +42,7 @@ void WLEDLightEffect::blank_all_leds_(light::AddressableLight &it) {
for (int led = it.size(); led-- > 0;) {
it[led].set(Color::BLACK);
}
it.schedule_show();
}
void WLEDLightEffect::apply(light::AddressableLight &it, const Color &current_color) {
@ -134,6 +135,7 @@ bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *p
blank_at_ = millis() + DEFAULT_BLANK_TIME;
}
it.schedule_show();
return true;
}

View File

@ -9,6 +9,7 @@ from esphome.const import (
CONF_CO2,
CONF_TEMPERATURE,
CONF_HUMIDITY,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
@ -35,6 +36,7 @@ CONFIG_SCHEMA = cv.Schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(

View File

@ -333,6 +333,8 @@ def validate_config(config, command_line_substitutions):
result.add_error(err)
return result
CORE.raw_config = config
# 1. Load substitutions
if CONF_SUBSTITUTIONS in config:
from esphome.components import substitutions
@ -348,6 +350,8 @@ def validate_config(config, command_line_substitutions):
result.add_error(err)
return result
CORE.raw_config = config
# 1.1. Check for REPLACEME special value
try:
recursive_check_replaceme(config)

View File

@ -33,7 +33,6 @@ from esphome.const import (
CONF_UPDATE_INTERVAL,
CONF_TYPE_ID,
CONF_TYPE,
CONF_PACKAGES,
)
from esphome.core import (
CORE,
@ -836,10 +835,11 @@ pressure = float_with_unit("pressure", "(bar|Bar)", optional_unit=True)
def temperature(value):
err = None
try:
return _temperature_c(value)
except Invalid as orig_err: # noqa
pass
except Invalid as orig_err:
err = orig_err
try:
kelvin = _temperature_k(value)
@ -853,7 +853,7 @@ def temperature(value):
except Invalid:
pass
raise orig_err # noqa
raise err
_color_temperature_mireds = float_with_unit("Color Temperature", r"(mireds|Mireds)")
@ -1454,11 +1454,7 @@ class OnlyWith(Optional):
@property
def default(self):
# pylint: disable=unsupported-membership-test
if self._component in CORE.raw_config or (
CONF_PACKAGES in CORE.raw_config
and self._component
in {list(x.keys())[0] for x in CORE.raw_config[CONF_PACKAGES].values()}
):
if self._component in CORE.raw_config:
return self._default
return vol.UNDEFINED
@ -1628,3 +1624,17 @@ def url(value):
if not parsed.scheme or not parsed.netloc:
raise Invalid("Expected a URL scheme and host")
return parsed.geturl()
def git_ref(value):
if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None:
raise Invalid("Not a valid git ref")
return value
def source_refresh(value: str):
if value.lower() == "always":
return source_refresh("0s")
if value.lower() == "never":
return source_refresh("1000y")
return positive_time_period_seconds(value)

View File

@ -1,6 +1,6 @@
"""Constants used by esphome."""
__version__ = "2021.8.2"
__version__ = "2021.9.0"
ESP_PLATFORM_ESP32 = "ESP32"
ESP_PLATFORM_ESP8266 = "ESP8266"
@ -69,6 +69,7 @@ CONF_ATTENUATION = "attenuation"
CONF_ATTRIBUTE = "attribute"
CONF_AUTH = "auth"
CONF_AUTO_MODE = "auto_mode"
CONF_AUTOCONF = "autoconf"
CONF_AUTOMATION_ID = "automation_id"
CONF_AVAILABILITY = "availability"
CONF_AWAY = "away"
@ -78,6 +79,7 @@ CONF_BASELINE = "baseline"
CONF_BATTERY_LEVEL = "battery_level"
CONF_BATTERY_VOLTAGE = "battery_voltage"
CONF_BAUD_RATE = "baud_rate"
CONF_BEEPER = "beeper"
CONF_BELOW = "below"
CONF_BINARY = "binary"
CONF_BINARY_SENSOR = "binary_sensor"
@ -165,6 +167,7 @@ CONF_DAYS_OF_WEEK = "days_of_week"
CONF_DC_PIN = "dc_pin"
CONF_DEASSERT_RTS_DTR = "deassert_rts_dtr"
CONF_DEBOUNCE = "debounce"
CONF_DECAY_MODE = "decay_mode"
CONF_DECELERATION = "deceleration"
CONF_DEFAULT_MODE = "default_mode"
CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = "default_target_temperature_high"
@ -234,12 +237,14 @@ CONF_FAN_WITH_COOLING = "fan_with_cooling"
CONF_FAN_WITH_HEATING = "fan_with_heating"
CONF_FAST_CONNECT = "fast_connect"
CONF_FILE = "file"
CONF_FILES = "files"
CONF_FILTER = "filter"
CONF_FILTER_OUT = "filter_out"
CONF_FILTERS = "filters"
CONF_FINGER_ID = "finger_id"
CONF_FINGERPRINT_COUNT = "fingerprint_count"
CONF_FLASH_LENGTH = "flash_length"
CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length"
CONF_FLOW_CONTROL_PIN = "flow_control_pin"
CONF_FOR = "for"
CONF_FORCE_UPDATE = "force_update"
@ -277,6 +282,9 @@ CONF_HUMIDITY = "humidity"
CONF_HYSTERESIS = "hysteresis"
CONF_I2C = "i2c"
CONF_I2C_ID = "i2c_id"
CONF_IBEACON_MAJOR = "ibeacon_major"
CONF_IBEACON_MINOR = "ibeacon_minor"
CONF_IBEACON_UUID = "ibeacon_uuid"
CONF_ICON = "icon"
CONF_ID = "id"
CONF_IDENTITY = "identity"
@ -317,7 +325,6 @@ CONF_KEY = "key"
CONF_LAMBDA = "lambda"
CONF_LAST_CONFIDENCE = "last_confidence"
CONF_LAST_FINGER_ID = "last_finger_id"
CONF_LAST_RESET_TYPE = "last_reset_type"
CONF_LATITUDE = "latitude"
CONF_LENGTH = "length"
CONF_LEVEL = "level"
@ -420,6 +427,7 @@ CONF_ON_PRESS = "on_press"
CONF_ON_RAW_VALUE = "on_raw_value"
CONF_ON_RELEASE = "on_release"
CONF_ON_SHUTDOWN = "on_shutdown"
CONF_ON_SPEED_SET = "on_speed_set"
CONF_ON_STATE = "on_state"
CONF_ON_TAG = "on_tag"
CONF_ON_TAG_REMOVED = "on_tag_removed"
@ -506,6 +514,8 @@ CONF_PROTOCOL = "protocol"
CONF_PULL_MODE = "pull_mode"
CONF_PULSE_LENGTH = "pulse_length"
CONF_QOS = "qos"
CONF_RADON = "radon"
CONF_RADON_LONG_TERM = "radon_long_term"
CONF_RANDOM = "random"
CONF_RANGE = "range"
CONF_RANGE_FROM = "range_from"
@ -518,8 +528,10 @@ CONF_REACTIVE_POWER = "reactive_power"
CONF_REBOOT_TIMEOUT = "reboot_timeout"
CONF_RECEIVE_TIMEOUT = "receive_timeout"
CONF_RED = "red"
CONF_REF = "ref"
CONF_REFERENCE_RESISTANCE = "reference_resistance"
CONF_REFERENCE_TEMPERATURE = "reference_temperature"
CONF_REFRESH = "refresh"
CONF_REPEAT = "repeat"
CONF_REPOSITORY = "repository"
CONF_RESET_PIN = "reset_pin"
@ -605,6 +617,10 @@ CONF_SUPPLEMENTAL_COOLING_ACTION = "supplemental_cooling_action"
CONF_SUPPLEMENTAL_COOLING_DELTA = "supplemental_cooling_delta"
CONF_SUPPLEMENTAL_HEATING_ACTION = "supplemental_heating_action"
CONF_SUPPLEMENTAL_HEATING_DELTA = "supplemental_heating_delta"
CONF_SUPPORTED_FAN_MODES = "supported_fan_modes"
CONF_SUPPORTED_MODES = "supported_modes"
CONF_SUPPORTED_PRESETS = "supported_presets"
CONF_SUPPORTED_SWING_MODES = "supported_swing_modes"
CONF_SUPPORTS_COOL = "supports_cool"
CONF_SUPPORTS_HEAT = "supports_heat"
CONF_SWING_BOTH_ACTION = "swing_both_action"
@ -731,6 +747,7 @@ ICON_PERCENT = "mdi:percent"
ICON_POWER = "mdi:power"
ICON_PULSE = "mdi:pulse"
ICON_RADIATOR = "mdi:radiator"
ICON_RADIOACTIVE = "mdi:radioactive"
ICON_RESTART = "mdi:restart"
ICON_ROTATE_RIGHT = "mdi:rotate-right"
ICON_RULER = "mdi:ruler"
@ -752,6 +769,7 @@ ICON_WEATHER_WINDY = "mdi:weather-windy"
ICON_WIFI = "mdi:wifi"
UNIT_AMPERE = "A"
UNIT_BECQUEREL_PER_CUBIC_METER = "Bq/m³"
UNIT_CELSIUS = "°C"
UNIT_COUNT_DECILITRE = "/dL"
UNIT_COUNTS_PER_CUBIC_METER = "#/m³"
@ -768,6 +786,10 @@ UNIT_KELVIN = "K"
UNIT_KILOGRAM = "kg"
UNIT_KILOMETER = "km"
UNIT_KILOMETER_PER_HOUR = "km/h"
UNIT_KILOVOLT_AMPS_REACTIVE = "kVAr"
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVArh"
UNIT_KILOWATT = "kW"
UNIT_KILOWATT_HOURS = "kWh"
UNIT_LUX = "lx"
UNIT_METER = "m"
UNIT_METER_PER_SECOND_SQUARED = "m/s²"
@ -798,7 +820,6 @@ DEVICE_CLASS_COLD = "cold"
DEVICE_CLASS_CONNECTIVITY = "connectivity"
DEVICE_CLASS_DOOR = "door"
DEVICE_CLASS_GARAGE_DOOR = "garage_door"
DEVICE_CLASS_GAS = "gas"
DEVICE_CLASS_HEAT = "heat"
DEVICE_CLASS_LIGHT = "light"
DEVICE_CLASS_LOCK = "lock"
@ -813,25 +834,37 @@ DEVICE_CLASS_PROBLEM = "problem"
DEVICE_CLASS_SAFETY = "safety"
DEVICE_CLASS_SMOKE = "smoke"
DEVICE_CLASS_SOUND = "sound"
DEVICE_CLASS_UPDATE = "update"
DEVICE_CLASS_VIBRATION = "vibration"
DEVICE_CLASS_WINDOW = "window"
# device classes of both binary_sensor and sensor component
DEVICE_CLASS_EMPTY = ""
DEVICE_CLASS_BATTERY = "battery"
DEVICE_CLASS_GAS = "gas"
DEVICE_CLASS_POWER = "power"
# device classes of sensor component
DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide"
DEVICE_CLASS_AQI = "aqi"
DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide"
DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide"
DEVICE_CLASS_CURRENT = "current"
DEVICE_CLASS_ENERGY = "energy"
DEVICE_CLASS_HUMIDITY = "humidity"
DEVICE_CLASS_ILLUMINANCE = "illuminance"
DEVICE_CLASS_MONETARY = "monetary"
DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength"
DEVICE_CLASS_TEMPERATURE = "temperature"
DEVICE_CLASS_NITROGEN_DIOXIDE = "nitrogen_dioxide"
DEVICE_CLASS_NITROGEN_MONOXIDE = "nitrogen_monoxide"
DEVICE_CLASS_NITROUS_OXIDE = "nitrous_oxide"
DEVICE_CLASS_OZONE = "ozone"
DEVICE_CLASS_PM1 = "pm1"
DEVICE_CLASS_PM10 = "pm10"
DEVICE_CLASS_PM25 = "pm25"
DEVICE_CLASS_POWER_FACTOR = "power_factor"
DEVICE_CLASS_PRESSURE = "pressure"
DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength"
DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide"
DEVICE_CLASS_TEMPERATURE = "temperature"
DEVICE_CLASS_TIMESTAMP = "timestamp"
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds"
DEVICE_CLASS_VOLTAGE = "voltage"
# state classes
@ -842,10 +875,3 @@ STATE_CLASS_MEASUREMENT = "measurement"
# The state represents a total that only increases, a decrease is considered a reset.
STATE_CLASS_TOTAL_INCREASING = "total_increasing"
# This sensor does not support resetting. ie, it is not accumulative
LAST_RESET_TYPE_NONE = ""
# This sensor is expected to never reset its value
LAST_RESET_TYPE_NEVER = "never"
# This sensor may reset and Home Assistant will watch for this
LAST_RESET_TYPE_AUTO = "auto"

View File

@ -19,7 +19,7 @@ void Application::register_component_(Component *comp) {
for (auto *c : this->components_) {
if (comp == c) {
ESP_LOGW(TAG, "Component already registered! (%p)", c);
ESP_LOGW(TAG, "Component %s already registered! (%p)", c->get_component_source(), c);
return;
}
}
@ -66,23 +66,19 @@ void Application::setup() {
}
void Application::loop() {
uint32_t new_app_state = 0;
const uint32_t start = millis();
this->scheduler.call();
for (Component *component : this->looping_components_) {
component->call();
{
WarnIfComponentBlockingGuard guard{component};
component->call();
}
new_app_state |= component->get_component_state();
this->app_state_ |= new_app_state;
this->feed_wdt();
}
this->app_state_ = new_app_state;
const uint32_t end = millis();
if (end - start > 200) {
ESP_LOGV(TAG, "A component took a long time in a loop() cycle (%.2f s).", (end - start) / 1e3f);
ESP_LOGV(TAG, "Components should block for at most 20-30ms in loop().");
}
const uint32_t now = millis();
if (HighFrequencyLoopRequester::is_high_frequency()) {

View File

@ -20,6 +20,7 @@ const float PROCESSOR = 400.0;
const float BLUETOOTH = 350.0f;
const float AFTER_BLUETOOTH = 300.0f;
const float WIFI = 250.0f;
const float BEFORE_CONNECTION = 220.0f;
const float AFTER_WIFI = 200.0f;
const float AFTER_CONNECTION = 100.0f;
const float LATE = -100.0f;
@ -92,8 +93,13 @@ void Component::call() {
break;
}
}
const char *Component::get_component_source() const {
if (this->component_source_ == nullptr)
return "<unknown>";
return this->component_source_;
}
void Component::mark_failed() {
ESP_LOGE(TAG, "Component was marked as failed.");
ESP_LOGE(TAG, "Component %s was marked as failed.", this->get_component_source());
this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_FAILED;
this->status_set_error();
@ -190,4 +196,18 @@ uint32_t Nameable::get_object_id_hash() { return this->object_id_hash_; }
bool Nameable::is_disabled_by_default() const { return this->disabled_by_default_; }
void Nameable::set_disabled_by_default(bool disabled_by_default) { this->disabled_by_default_ = disabled_by_default; }
WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component) {
component_ = component;
started_ = millis();
}
WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {
uint32_t now = millis();
if (now - started_ > 50) {
const char *src = component_ == nullptr ? "<null>" : component_->get_component_source();
ESP_LOGV(TAG, "Component %s took a long time for an operation (%.2f s).", src, (now - started_) / 1e3f);
ESP_LOGV(TAG, "Components should block for at most 20-30ms.");
;
}
}
} // namespace esphome

View File

@ -29,6 +29,8 @@ extern const float PROCESSOR;
extern const float BLUETOOTH;
extern const float AFTER_BLUETOOTH;
extern const float WIFI;
/// For components that should be initialized after WiFi and before API is connected.
extern const float BEFORE_CONNECTION;
/// For components that should be initialized after WiFi is connected.
extern const float AFTER_WIFI;
/// For components that should be initialized after a data connection (API/MQTT) is connected.
@ -38,8 +40,12 @@ extern const float LATE;
} // namespace setup_priority
static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL;
#define LOG_UPDATE_INTERVAL(this) \
if (this->get_update_interval() < 100) { \
if (this->get_update_interval() == SCHEDULER_DONT_RUN) { \
ESP_LOGCONFIG(TAG, " Update Interval: never"); \
} else if (this->get_update_interval() < 100) { \
ESP_LOGCONFIG(TAG, " Update Interval: %.3fs", this->get_update_interval() / 1000.0f); \
} else { \
ESP_LOGCONFIG(TAG, " Update Interval: %.1fs", this->get_update_interval() / 1000.0f); \
@ -130,6 +136,17 @@ class Component {
bool has_overridden_loop() const;
/** Set where this component was loaded from for some debug messages.
*
* This is set by the ESPHome core, and should not be called manually.
*/
void set_component_source(const char *source) { component_source_ = source; }
/** Get the integration where this component was declared as a string.
*
* Returns "<unknown>" if source not set
*/
const char *get_component_source() const;
protected:
virtual void call_loop();
virtual void call_setup();
@ -201,6 +218,7 @@ class Component {
uint32_t component_state_{0x0000}; ///< State of this component.
float setup_priority_override_{NAN};
const char *component_source_ = nullptr;
};
/** This class simplifies creating components that periodically check a state.
@ -276,4 +294,14 @@ class Nameable {
bool disabled_by_default_{false};
};
class WarnIfComponentBlockingGuard {
public:
WarnIfComponentBlockingGuard(Component *component);
~WarnIfComponentBlockingGuard();
protected:
uint32_t started_;
Component *component_;
};
} // namespace esphome

View File

@ -1,31 +1,61 @@
#pragma once
// This file is auto-generated! Do not edit!
// This file is not used by the runtime, instead, a version is generated during
// compilation with only the relevant feature flags for the current build.
//
// This file is only used by static analyzers and IDEs.
// Informative flags
#define ESPHOME_BOARD "dummy_board"
#define ESPHOME_PROJECT_NAME "dummy project"
#define ESPHOME_PROJECT_VERSION "v2"
// Feature flags
#define USE_ADC_SENSOR_VCC
#define USE_API
#define USE_LOGGER
#define USE_BINARY_SENSOR
#define USE_SENSOR
#define USE_SWITCH
#define USE_WIFI
#define USE_STATUS_LED
#define USE_TEXT_SENSOR
#define USE_FAN
#define USE_COVER
#define USE_LIGHT
#define USE_CAPTIVE_PORTAL
#define USE_CLIMATE
#define USE_NUMBER
#define USE_SELECT
#define USE_MQTT
#define USE_POWER_SUPPLY
#define USE_COVER
#define USE_DEEP_SLEEP
#define USE_ESP8266_PREFERENCES_FLASH
#define USE_FAN
#define USE_HOMEASSISTANT_TIME
#define USE_I2C_MULTIPLEXER
#define USE_JSON
#define USE_LIGHT
#define USE_LOGGER
#define USE_MDNS
#define USE_MQTT
#define USE_NUMBER
#define USE_OTA_STATE_CALLBACK
#define USE_POWER_SUPPLY
#define USE_PROMETHEUS
#define USE_SELECT
#define USE_SENSOR
#define USE_STATUS_LED
#define USE_SWITCH
#define USE_TEXT_SENSOR
#define USE_TFT_UPLOAD
#define USE_TIME
#define USE_WIFI
#define USE_WIFI_WPA2_EAP
#ifdef ARDUINO_ARCH_ESP32
#define USE_ESP32_CAMERA
#define USE_ESP32_BLE_SERVER
#define USE_ESP32_CAMERA
#define USE_ETHERNET
#define USE_IMPROV
#endif
#define USE_TIME
#define USE_DEEP_SLEEP
#define USE_CAPTIVE_PORTAL
#define ESPHOME_BOARD "dummy_board"
#define USE_MDNS
#ifdef ARDUINO_ARCH_ESP8266
#define USE_SOCKET_IMPL_LWIP_TCP
#else
#define USE_SOCKET_IMPL_BSD_SOCKETS
#endif
#define USE_API_PLAINTEXT
#define USE_API_NOISE
// Disabled feature flags
//#define USE_BSEC // Requires a library with proprietary license.

View File

@ -1,4 +1,5 @@
#include "esphome/core/esphal.h"
#include "esphome/core/macros.h"
#include "esphome/core/helpers.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
@ -298,7 +299,7 @@ void force_link_symbols() {
} // namespace esphome
#ifdef ARDUINO_ESP8266_RELEASE_2_3_0
#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0)
// Fix 2.3.0 std missing memchr
extern "C" {
void *memchr(const void *s, int c, size_t n) {

View File

@ -55,6 +55,15 @@ double random_double() { return random_uint32() / double(UINT32_MAX); }
float random_float() { return float(random_double()); }
void fill_random(uint8_t *data, size_t len) {
#ifdef ARDUINO_ARCH_ESP32
esp_fill_random(data, len);
#else
int err = os_get_random(data, len);
assert(err == 0);
#endif
}
static uint32_t fast_random_seed = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void fast_random_set_seed(uint32_t seed) { fast_random_seed = seed; }

View File

@ -109,6 +109,8 @@ double random_double();
/// Returns a random float between 0 and 1. Essentially just casts random_double() to a float.
float random_float();
void fill_random(uint8_t *data, size_t len);
void fast_random_set_seed(uint32_t seed);
uint32_t fast_random_32();
uint16_t fast_random_16();

View File

@ -7,6 +7,7 @@
#include "WString.h"
#endif
#include "esphome/core/macros.h"
// avoid esp-idf redefining our macros
#include "esphome/core/esphal.h"
@ -162,4 +163,28 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
#define ONOFF(b) ((b) ? "ON" : "OFF")
#define TRUEFALSE(b) ((b) ? "TRUE" : "FALSE")
#ifdef USE_STORE_LOG_STR_IN_FLASH
#define LOG_STR(s) PSTR(s)
// From Arduino 2.5 onwards, we can pass a PSTR() to printf(). For previous versions, emulate support
// by copying the message to a local buffer first. String length is limited to 63 characters.
// https://github.com/esp8266/Arduino/commit/6280e98b0360f85fdac2b8f10707fffb4f6e6e31
#include <core_version.h>
#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 5, 0)
#define LOG_STR_ARG(s) \
({ \
char __buf[64]; \
__buf[63] = '\0'; \
strncpy_P(__buf, s, 63); \
__buf; \
})
#else
#define LOG_STR_ARG(s) (s)
#endif
#else
#define LOG_STR(s) (s)
#define LOG_STR_ARG(s) (s)
#endif
} // namespace esphome

56
esphome/core/macros.h Normal file
View File

@ -0,0 +1,56 @@
#pragma once
#define VERSION_CODE(major, minor, patch) ((major) << 16 | (minor) << 8 | (patch))
#if defined(ARDUINO_ARCH_ESP8266)
#include <core_version.h>
#if defined(ARDUINO_ESP8266_MAJOR) && defined(ARDUINO_ESP8266_MINOR) && defined(ARDUINO_ESP8266_REVISION) // v3.0.1+
#define ARDUINO_VERSION_CODE VERSION_CODE(ARDUINO_ESP8266_MAJOR, ARDUINO_ESP8266_MINOR, ARDUINO_ESP8266_REVISION)
#elif ARDUINO_ESP8266_GIT_VER == 0xefb0341a // version defines were screwed up in v3.0.0
#define ARDUINO_VERSION_CODE VERSION_CODE(3, 0, 0)
#elif defined(ARDUINO_ESP8266_RELEASE_2_7_4)
#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 4)
#elif defined(ARDUINO_ESP8266_RELEASE_2_7_3)
#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 3)
#elif defined(ARDUINO_ESP8266_RELEASE_2_7_2)
#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 2)
#elif defined(ARDUINO_ESP8266_RELEASE_2_7_1)
#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 1)
#elif defined(ARDUINO_ESP8266_RELEASE_2_7_0)
#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 0)
#elif defined(ARDUINO_ESP8266_RELEASE_2_6_3)
#define ARDUINO_VERSION_CODE VERSION_CODE(2, 6, 3)
#elif defined(ARDUINO_ESP8266_RELEASE_2_6_2)
#define ARDUINO_VERSION_CODE VERSION_CODE(2, 6, 2)
#elif defined(ARDUINO_ESP8266_RELEASE_2_6_1)
#define ARDUINO_VERSION_CODE VERSION_CODE(2, 6, 1)
#elif defined(ARDUINO_ESP8266_RELEASE_2_5_2)
#define ARDUINO_VERSION_CODE VERSION_CODE(2, 5, 2)
#elif defined(ARDUINO_ESP8266_RELEASE_2_5_1)
#define ARDUINO_VERSION_CODE VERSION_CODE(2, 5, 1)
#elif defined(ARDUINO_ESP8266_RELEASE_2_5_0)
#define ARDUINO_VERSION_CODE VERSION_CODE(2, 5, 0)
#elif defined(ARDUINO_ESP8266_RELEASE_2_4_2)
#define ARDUINO_VERSION_CODE VERSION_CODE(2, 4, 2)
#elif defined(ARDUINO_ESP8266_RELEASE_2_4_1)
#define ARDUINO_VERSION_CODE VERSION_CODE(2, 4, 1)
#elif defined(ARDUINO_ESP8266_RELEASE_2_4_0)
#define ARDUINO_VERSION_CODE VERSION_CODE(2, 4, 0)
#elif defined(ARDUINO_ESP8266_RELEASE_2_3_0)
#define ARDUINO_VERSION_CODE VERSION_CODE(2, 3, 0)
#else
#warning "Could not determine Arduino framework version, update esphome/core/macros.h!"
#endif
#elif defined(ARDUINO_ARCH_ESP32)
#if defined(IDF_VER) // identifies v2, needed since v1 doesn't have the esp_arduino_version.h header
#include <esp_arduino_version.h>
#define ARDUINO_VERSION_CODE \
VERSION_CODE(ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATH)
#else
#define ARDUINO_VERSION_CODE VERSION_CODE(1, 0, 0) // there are no defines identifying minor/patch version
#endif
#endif

View File

@ -7,7 +7,6 @@ namespace esphome {
static const char *const TAG = "scheduler";
static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL;
static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10;
// Uncomment to debug scheduler
@ -155,7 +154,10 @@ void ICACHE_RAM_ATTR HOT Scheduler::call() {
// Warning: During f(), a lot of stuff can happen, including:
// - timeouts/intervals get added, potentially invalidating vector pointers
// - timeouts/intervals get cancelled
item->f();
{
WarnIfComponentBlockingGuard guard{item->component};
item->f();
}
}
{

View File

@ -1,3 +1,9 @@
#pragma once
// This file is auto-generated! Do not edit!
// This file is not used by the runtime, instead, a version is generated during
// compilation with only the version for the current build. This is kept in its
// own file so that not all files have to be recompiled for each new release.
//
// This file is only used by static analyzers and IDEs.
#define ESPHOME_VERSION "dev"

View File

@ -1,3 +1,5 @@
import logging
from esphome.const import (
CONF_INVERTED,
CONF_MODE,
@ -15,6 +17,9 @@ from esphome.cpp_types import App, GPIOPin
from esphome.util import Registry, RegistryEntry
_LOGGER = logging.getLogger(__name__)
async def gpio_pin_expression(conf):
"""Generate an expression for the given pin option.
@ -42,6 +47,8 @@ async def register_component(var, config):
:param var: The variable representing the component.
:param config: The configuration for the component.
"""
import inspect
id_ = str(var.base)
if id_ not in CORE.component_ids:
raise ValueError(
@ -54,6 +61,32 @@ async def register_component(var, config):
add(var.set_setup_priority(config[CONF_SETUP_PRIORITY]))
if CONF_UPDATE_INTERVAL in config:
add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))
# Set component source by inspecting the stack and getting the callee module
# https://stackoverflow.com/a/1095621
name = None
try:
for frm in inspect.stack()[1:]:
mod = inspect.getmodule(frm[0])
if mod is None:
continue
name = mod.__name__
if name.startswith("esphome.components."):
name = name[len("esphome.components.") :]
break
if name == "esphome.automation":
name = "automation"
# continue looking further up in stack in case we find a better one
if name == "esphome.coroutine":
# Only works for async-await coroutine syntax
break
except (KeyError, AttributeError, IndexError) as e:
_LOGGER.warning(
"Error while finding name of component, please report this", exc_info=e
)
if name is not None:
add(var.set_component_source(name))
add(App.register_component(var))
return var

View File

@ -41,7 +41,7 @@ from .util import password_hash
# pylint: disable=unused-import, wrong-import-order
from typing import Optional # noqa
from esphome.zeroconf import DashboardStatus, Zeroconf
from esphome.zeroconf import DashboardStatus, EsphomeZeroconf
_LOGGER = logging.getLogger(__name__)
@ -431,7 +431,7 @@ class DashboardEntry:
@property
def name(self):
if self.storage is None:
return self.filename[: -len(".yaml")]
return self.filename.replace(".yml", "").replace(".yaml", "")
return self.storage.name
@property
@ -501,7 +501,7 @@ def _ping_func(filename, address):
class MDNSStatusThread(threading.Thread):
def run(self):
zc = Zeroconf()
zc = EsphomeZeroconf()
def on_update(dat):
for key, b in dat.items():
@ -600,7 +600,7 @@ class EditRequestHandler(BaseHandler):
content = ""
if os.path.isfile(filename):
# pylint: disable=no-value-for-parameter
with open(filename, "r") as f:
with open(file=filename, mode="r", encoding="utf-8") as f:
content = f.read()
self.write(content)
@ -608,7 +608,7 @@ class EditRequestHandler(BaseHandler):
@bind_config
def post(self, configuration=None):
# pylint: disable=no-value-for-parameter
with open(settings.rel_path(configuration), "wb") as f:
with open(file=settings.rel_path(configuration), mode="wb") as f:
f.write(self.request.body)
self.set_status(200)

74
esphome/git.py Normal file
View File

@ -0,0 +1,74 @@
from pathlib import Path
import subprocess
import hashlib
import logging
from datetime import datetime
from esphome.core import CORE, TimePeriodSeconds
import esphome.config_validation as cv
_LOGGER = logging.getLogger(__name__)
def run_git_command(cmd, cwd=None):
try:
ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False)
except FileNotFoundError as err:
raise cv.Invalid(
"git is not installed but required for external_components.\n"
"Please see https://git-scm.com/book/en/v2/Getting-Started-Installing-Git for installing git"
) from err
if ret.returncode != 0 and ret.stderr:
err_str = ret.stderr.decode("utf-8")
lines = [x.strip() for x in err_str.splitlines()]
if lines[-1].startswith("fatal:"):
raise cv.Invalid(lines[-1][len("fatal: ") :])
raise cv.Invalid(err_str)
def _compute_destination_path(key: str, domain: str) -> Path:
base_dir = Path(CORE.config_dir) / ".esphome" / domain
h = hashlib.new("sha256")
h.update(key.encode())
return base_dir / h.hexdigest()[:8]
def clone_or_update(
*, url: str, ref: str = None, refresh: TimePeriodSeconds, domain: str
) -> Path:
key = f"{url}@{ref}"
repo_dir = _compute_destination_path(key, domain)
if not repo_dir.is_dir():
_LOGGER.info("Cloning %s", key)
_LOGGER.debug("Location: %s", repo_dir)
cmd = ["git", "clone", "--depth=1"]
if ref is not None:
cmd += ["--branch", ref]
cmd += ["--", url, str(repo_dir)]
run_git_command(cmd)
else:
# Check refresh needed
file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD")
# On first clone, FETCH_HEAD does not exists
if not file_timestamp.exists():
file_timestamp = Path(repo_dir / ".git" / "HEAD")
age = datetime.now() - datetime.fromtimestamp(file_timestamp.stat().st_mtime)
if age.total_seconds() > refresh.total_seconds:
_LOGGER.info("Updating %s", key)
_LOGGER.debug("Location: %s", repo_dir)
# Stash local changes (if any)
run_git_command(
["git", "stash", "push", "--include-untracked"], str(repo_dir)
)
# Fetch remote ref
cmd = ["git", "fetch", "--", "origin"]
if ref is not None:
cmd.append(ref)
run_git_command(cmd, str(repo_dir))
# Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch)
run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir))
return repo_dir

View File

@ -97,10 +97,10 @@ def is_ip_address(host):
def _resolve_with_zeroconf(host):
from esphome.core import EsphomeError
from esphome.zeroconf import Zeroconf
from esphome.zeroconf import EsphomeZeroconf
try:
zc = Zeroconf()
zc = EsphomeZeroconf()
except Exception as err:
raise EsphomeError(
"Cannot start mDNS sockets, is this a docker container without "
@ -276,11 +276,11 @@ def file_compare(path1: os.PathLike, path2: os.PathLike) -> bool:
# A dict of types that need to be converted to heaptypes before a class can be added
# to the object
_TYPE_OVERLOADS = {
int: type("EInt", (int,), dict()),
float: type("EFloat", (float,), dict()),
str: type("EStr", (str,), dict()),
dict: type("EDict", (str,), dict()),
list: type("EList", (list,), dict()),
int: type("EInt", (int,), {}),
float: type("EFloat", (float,), {}),
str: type("EStr", (str,), {}),
dict: type("EDict", (str,), {}),
list: type("EList", (list,), {}),
}
# cache created classes here

View File

@ -260,8 +260,8 @@ def filter_yaml_files(files):
f
for f in files
if (
os.path.splitext(f)[1] == ".yaml"
and os.path.basename(f) != "secrets.yaml"
os.path.splitext(f)[1] in (".yaml", ".yml")
and os.path.basename(f) not in ("secrets.yaml", "secrets.yml")
and not os.path.basename(f).startswith(".")
)
]

View File

@ -481,5 +481,5 @@ GITIGNORE_CONTENT = """# Gitignore settings for ESPHome
def write_gitignore():
path = CORE.relative_config_path(".gitignore")
if not os.path.isfile(path):
with open(path, "w") as f:
with open(file=path, mode="w", encoding="utf-8") as f:
f.write(GITIGNORE_CONTENT)

Some files were not shown because too many files have changed in this diff Show More