mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 06:33:51 +00:00 
			
		
		
		
	| @@ -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() | ||||
|   | ||||
| @@ -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
											
										
									
								
							| @@ -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 | ||||
| @@ -243,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; | ||||
| @@ -291,13 +294,13 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { | ||||
|     // Prefer level | ||||
|     call.set_speed(msg.speed_level); | ||||
|   } else if (msg.has_speed) { | ||||
|     // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) | ||||
|     call.set_speed(fan::speed_enum_to_level(static_cast<fan::FanSpeed>(msg.speed), traits.supported_speed_count())); | ||||
|   } | ||||
|   if (msg.has_direction) | ||||
|     call.set_direction(static_cast<fan::FanDirection>(msg.direction)); | ||||
|   call.perform(); | ||||
| } | ||||
| #pragma GCC diagnostic pop | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LIGHT | ||||
|   | ||||
| @@ -61,11 +61,15 @@ const char *api_error_to_str(APIError err) { | ||||
|     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"; | ||||
| @@ -236,7 +240,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { | ||||
|   } | ||||
|  | ||||
|   // uncomment for even more debugging | ||||
|   // ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); | ||||
| #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_ = {}; | ||||
| @@ -265,6 +271,14 @@ APIError APINoiseFrameHelper::state_action_() { | ||||
|     // 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 | ||||
| @@ -308,11 +322,11 @@ APIError APINoiseFrameHelper::state_action_() { | ||||
|  | ||||
|       if (frame.msg.empty()) { | ||||
|         send_explicit_handshake_reject_("Empty handshake message"); | ||||
|         return APIError::BAD_HANDSHAKE_PACKET_LEN; | ||||
|         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_PACKET_LEN; | ||||
|         return APIError::BAD_HANDSHAKE_ERROR_BYTE; | ||||
|       } | ||||
|  | ||||
|       NoiseBuffer mbuf; | ||||
| @@ -320,7 +334,6 @@ APIError APINoiseFrameHelper::state_action_() { | ||||
|       noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1); | ||||
|       err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); | ||||
|       if (err != 0) { | ||||
|         // TODO: explicit rejection | ||||
|         state_ = State::FAILED; | ||||
|         HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str()); | ||||
|         if (err == NOISE_ERROR_MAC_FAILURE) { | ||||
| @@ -368,12 +381,16 @@ APIError APINoiseFrameHelper::state_action_() { | ||||
| } | ||||
| void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { | ||||
|   std::vector<uint8_t> data; | ||||
|   data.reserve(reason.size() + 1); | ||||
|   data.resize(reason.length() + 1); | ||||
|   data[0] = 0x01;  // failure | ||||
|   for (size_t i = 0; i < reason.size(); i++) { | ||||
|   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) { | ||||
| @@ -516,7 +533,9 @@ APIError APINoiseFrameHelper::write_raw_(const uint8_t *data, size_t len) { | ||||
|   APIError aerr; | ||||
|  | ||||
|   // uncomment for even more debugging | ||||
|   // ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str()); | ||||
| #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 | ||||
| @@ -799,7 +818,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { | ||||
|   } | ||||
|  | ||||
|   // uncomment for even more debugging | ||||
|   // ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); | ||||
| #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_ = {}; | ||||
| @@ -882,7 +903,9 @@ APIError APIPlaintextFrameHelper::write_raw_(const uint8_t *data, size_t len) { | ||||
|   APIError aerr; | ||||
|  | ||||
|   // uncomment for even more debugging | ||||
|   // ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str()); | ||||
| #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 | ||||
|   | ||||
| @@ -51,6 +51,7 @@ enum class APIError : int { | ||||
|   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); | ||||
| @@ -125,6 +126,7 @@ class APINoiseFrameHelper : public APIFrameHelper { | ||||
|     DATA = 5, | ||||
|     CLOSED = 6, | ||||
|     FAILED = 7, | ||||
|     EXPLICIT_REJECT = 8, | ||||
|   } state_ = State::INITIALIZE; | ||||
| }; | ||||
| #endif  // USE_API_NOISE | ||||
|   | ||||
							
								
								
									
										73
									
								
								esphome/components/api/client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								esphome/components/api/client.py
									
									
									
									
									
										Normal 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 = 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)) | ||||
| @@ -4,14 +4,15 @@ | ||||
| namespace esphome { | ||||
| namespace fan { | ||||
|  | ||||
| // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) | ||||
| // 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); | ||||
|   return static_cast<FanSpeed>(legacy_level - 1);  // NOLINT(clang-diagnostic-deprecated-declarations) | ||||
|   return static_cast<FanSpeed>(legacy_level - 1); | ||||
| } | ||||
|  | ||||
| // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) | ||||
| int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) { | ||||
|   const auto enum_level = static_cast<int>(speed) + 1; | ||||
|   const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels); | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -100,6 +100,7 @@ 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: {  // NOLINT(clang-diagnostic-deprecated-declarations) | ||||
|         payload = "low"; | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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!"); | ||||
| 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();  // Clear the incoming buffer | ||||
|     this->status_set_warning(); | ||||
|         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; } | ||||
|   | ||||
| @@ -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}; | ||||
| }; | ||||
|   | ||||
| @@ -397,6 +397,7 @@ 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:  // NOLINT(clang-diagnostic-deprecated-declarations) | ||||
|           root["speed"] = "low"; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| """Constants used by esphome.""" | ||||
|  | ||||
| __version__ = "2021.9.0b3" | ||||
| __version__ = "2021.9.0b4" | ||||
|  | ||||
| ESP_PLATFORM_ESP32 = "ESP32" | ||||
| ESP_PLATFORM_ESP8266 = "ESP8266" | ||||
|   | ||||
| @@ -4,9 +4,6 @@ import time | ||||
| from typing import Dict, Optional | ||||
|  | ||||
| from zeroconf import ( | ||||
|     _CLASS_IN, | ||||
|     _FLAGS_QR_QUERY, | ||||
|     _TYPE_A, | ||||
|     DNSAddress, | ||||
|     DNSOutgoing, | ||||
|     DNSRecord, | ||||
| @@ -15,6 +12,10 @@ from zeroconf import ( | ||||
|     Zeroconf, | ||||
| ) | ||||
|  | ||||
| _CLASS_IN = 1 | ||||
| _FLAGS_QR_QUERY = 0x0000  # query | ||||
| _TYPE_A = 1 | ||||
|  | ||||
|  | ||||
| class HostResolver(RecordUpdateListener): | ||||
|     def __init__(self, name: str): | ||||
|   | ||||
| @@ -3,12 +3,11 @@ PyYAML==5.4.1 | ||||
| paho-mqtt==1.5.1 | ||||
| colorama==0.4.4 | ||||
| tornado==6.1 | ||||
| protobuf==3.17.3 | ||||
| tzlocal==2.1 | ||||
| pytz==2021.1 | ||||
| pyserial==3.5 | ||||
| ifaddr==0.1.7 | ||||
| platformio==5.1.1 | ||||
| platformio==5.2.0 | ||||
| esptool==3.1 | ||||
| click==7.1.2 | ||||
| esphome-dashboard==20210908.0 | ||||
| aioesphomeapi==9.0.0 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user