mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	| @@ -24,7 +24,7 @@ TYPE_LINT = 'lint' | |||||||
| TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT] | TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT] | ||||||
|  |  | ||||||
|  |  | ||||||
| BASE_VERSION = "3.6.0" | BASE_VERSION = "4.2.0" | ||||||
|  |  | ||||||
|  |  | ||||||
| parser = argparse.ArgumentParser() | parser = argparse.ArgumentParser() | ||||||
|   | |||||||
| @@ -256,7 +256,7 @@ def show_logs(config, args, port): | |||||||
|         run_miniterm(config, port) |         run_miniterm(config, port) | ||||||
|         return 0 |         return 0 | ||||||
|     if get_port_type(port) == "NETWORK" and "api" in config: |     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) |         return run_logs(config, port) | ||||||
|     if get_port_type(port) == "MQTT" and "mqtt" in config: |     if get_port_type(port) == "MQTT" and "mqtt" in config: | ||||||
| @@ -483,75 +483,9 @@ def parse_args(argv): | |||||||
|         metavar=("key", "value"), |         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( |     parser = argparse.ArgumentParser( | ||||||
|         description=f"ESPHome v{const.__version__}", parents=[options_parser] |         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 = argparse.ArgumentParser(add_help=False) | ||||||
|     mqtt_options.add_argument("--topic", help="Manually set the MQTT topic.") |     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="+" |         "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): | def run_esphome(argv): | ||||||
| @@ -715,7 +725,7 @@ def run_esphome(argv): | |||||||
|             "and will be removed in the future. " |             "and will be removed in the future. " | ||||||
|         ) |         ) | ||||||
|         _LOGGER.warning("Please instead use:") |         _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): |     if sys.version_info < (3, 7, 0): | ||||||
|         _LOGGER.error( |         _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 | #endif | ||||||
|  |  | ||||||
| #ifdef USE_FAN | #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) { | bool APIConnection::send_fan_state(fan::FanState *fan) { | ||||||
|   if (!this->state_subscription_) |   if (!this->state_subscription_) | ||||||
|     return false; |     return false; | ||||||
| @@ -291,13 +294,13 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { | |||||||
|     // Prefer level |     // Prefer level | ||||||
|     call.set_speed(msg.speed_level); |     call.set_speed(msg.speed_level); | ||||||
|   } else if (msg.has_speed) { |   } 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())); |     call.set_speed(fan::speed_enum_to_level(static_cast<fan::FanSpeed>(msg.speed), traits.supported_speed_count())); | ||||||
|   } |   } | ||||||
|   if (msg.has_direction) |   if (msg.has_direction) | ||||||
|     call.set_direction(static_cast<fan::FanDirection>(msg.direction)); |     call.set_direction(static_cast<fan::FanDirection>(msg.direction)); | ||||||
|   call.perform(); |   call.perform(); | ||||||
| } | } | ||||||
|  | #pragma GCC diagnostic pop | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_LIGHT | #ifdef USE_LIGHT | ||||||
|   | |||||||
| @@ -61,11 +61,15 @@ const char *api_error_to_str(APIError err) { | |||||||
|     return "HANDSHAKESTATE_SETUP_FAILED"; |     return "HANDSHAKESTATE_SETUP_FAILED"; | ||||||
|   } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) { |   } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) { | ||||||
|     return "HANDSHAKESTATE_SPLIT_FAILED"; |     return "HANDSHAKESTATE_SPLIT_FAILED"; | ||||||
|  |   } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) { | ||||||
|  |     return "BAD_HANDSHAKE_ERROR_BYTE"; | ||||||
|   } |   } | ||||||
|   return "UNKNOWN"; |   return "UNKNOWN"; | ||||||
| } | } | ||||||
|  |  | ||||||
| #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__) | #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 | #ifdef USE_API_NOISE | ||||||
| static const char *const PROLOGUE_INIT = "NoiseAPIInit"; | static const char *const PROLOGUE_INIT = "NoiseAPIInit"; | ||||||
| @@ -236,7 +240,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   // uncomment for even more debugging |   // 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_); |   frame->msg = std::move(rx_buf_); | ||||||
|   // consume msg |   // consume msg | ||||||
|   rx_buf_ = {}; |   rx_buf_ = {}; | ||||||
| @@ -265,6 +271,14 @@ APIError APINoiseFrameHelper::state_action_() { | |||||||
|     // waiting for client hello |     // waiting for client hello | ||||||
|     ParsedFrame frame; |     ParsedFrame frame; | ||||||
|     aerr = try_read_frame_(&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) |     if (aerr != APIError::OK) | ||||||
|       return aerr; |       return aerr; | ||||||
|     // ignore contents, may be used in future for flags |     // ignore contents, may be used in future for flags | ||||||
| @@ -308,11 +322,11 @@ APIError APINoiseFrameHelper::state_action_() { | |||||||
|  |  | ||||||
|       if (frame.msg.empty()) { |       if (frame.msg.empty()) { | ||||||
|         send_explicit_handshake_reject_("Empty handshake message"); |         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) { |       } else if (frame.msg[0] != 0x00) { | ||||||
|         HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]); |         HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]); | ||||||
|         send_explicit_handshake_reject_("Bad handshake error byte"); |         send_explicit_handshake_reject_("Bad handshake error byte"); | ||||||
|         return APIError::BAD_HANDSHAKE_PACKET_LEN; |         return APIError::BAD_HANDSHAKE_ERROR_BYTE; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       NoiseBuffer mbuf; |       NoiseBuffer mbuf; | ||||||
| @@ -320,7 +334,6 @@ APIError APINoiseFrameHelper::state_action_() { | |||||||
|       noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1); |       noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1); | ||||||
|       err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); |       err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); | ||||||
|       if (err != 0) { |       if (err != 0) { | ||||||
|         // TODO: explicit rejection |  | ||||||
|         state_ = State::FAILED; |         state_ = State::FAILED; | ||||||
|         HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str()); |         HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str()); | ||||||
|         if (err == NOISE_ERROR_MAC_FAILURE) { |         if (err == NOISE_ERROR_MAC_FAILURE) { | ||||||
| @@ -368,12 +381,16 @@ APIError APINoiseFrameHelper::state_action_() { | |||||||
| } | } | ||||||
| void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { | void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { | ||||||
|   std::vector<uint8_t> data; |   std::vector<uint8_t> data; | ||||||
|   data.reserve(reason.size() + 1); |   data.resize(reason.length() + 1); | ||||||
|   data[0] = 0x01;  // failure |   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]; |     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()); |   write_frame_(data.data(), data.size()); | ||||||
|  |   state_ = orig_state; | ||||||
| } | } | ||||||
|  |  | ||||||
| APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { | APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { | ||||||
| @@ -516,7 +533,9 @@ APIError APINoiseFrameHelper::write_raw_(const uint8_t *data, size_t len) { | |||||||
|   APIError aerr; |   APIError aerr; | ||||||
|  |  | ||||||
|   // uncomment for even more debugging |   // 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()) { |   if (!tx_buf_.empty()) { | ||||||
|     // try to empty tx_buf_ first |     // try to empty tx_buf_ first | ||||||
| @@ -799,7 +818,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   // uncomment for even more debugging |   // 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_); |   frame->msg = std::move(rx_buf_); | ||||||
|   // consume msg |   // consume msg | ||||||
|   rx_buf_ = {}; |   rx_buf_ = {}; | ||||||
| @@ -882,7 +903,9 @@ APIError APIPlaintextFrameHelper::write_raw_(const uint8_t *data, size_t len) { | |||||||
|   APIError aerr; |   APIError aerr; | ||||||
|  |  | ||||||
|   // uncomment for even more debugging |   // 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()) { |   if (!tx_buf_.empty()) { | ||||||
|     // try to empty tx_buf_ first |     // try to empty tx_buf_ first | ||||||
|   | |||||||
| @@ -51,6 +51,7 @@ enum class APIError : int { | |||||||
|   OUT_OF_MEMORY = 1018, |   OUT_OF_MEMORY = 1018, | ||||||
|   HANDSHAKESTATE_SETUP_FAILED = 1019, |   HANDSHAKESTATE_SETUP_FAILED = 1019, | ||||||
|   HANDSHAKESTATE_SPLIT_FAILED = 1020, |   HANDSHAKESTATE_SPLIT_FAILED = 1020, | ||||||
|  |   BAD_HANDSHAKE_ERROR_BYTE = 1021, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const char *api_error_to_str(APIError err); | const char *api_error_to_str(APIError err); | ||||||
| @@ -125,6 +126,7 @@ class APINoiseFrameHelper : public APIFrameHelper { | |||||||
|     DATA = 5, |     DATA = 5, | ||||||
|     CLOSED = 6, |     CLOSED = 6, | ||||||
|     FAILED = 7, |     FAILED = 7, | ||||||
|  |     EXPLICIT_REJECT = 8, | ||||||
|   } state_ = State::INITIALIZE; |   } state_ = State::INITIALIZE; | ||||||
| }; | }; | ||||||
| #endif  // USE_API_NOISE | #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 esphome { | ||||||
| namespace fan { | 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) { | 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 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); |   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) { | int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) { | ||||||
|   const auto enum_level = static_cast<int>(speed) + 1; |   const auto enum_level = static_cast<int>(speed) + 1; | ||||||
|   const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels); |   const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels); | ||||||
|   | |||||||
| @@ -4,8 +4,16 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace fan { | 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); | 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); | int speed_enum_to_level(FanSpeed speed, int supported_speed_levels); | ||||||
|  |  | ||||||
|  | #pragma GCC diagnostic pop | ||||||
|  |  | ||||||
| }  // namespace fan | }  // namespace fan | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -67,6 +67,8 @@ void FanStateCall::perform() const { | |||||||
|   this->state_->state_callback_.call(); |   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) { | FanStateCall &FanStateCall::set_speed(const char *legacy_speed) { | ||||||
|   const auto supported_speed_count = this->state_->get_traits().supported_speed_count(); |   const auto supported_speed_count = this->state_->get_traits().supported_speed_count(); | ||||||
|   if (strcasecmp(legacy_speed, "low") == 0) { |   if (strcasecmp(legacy_speed, "low") == 0) { | ||||||
|   | |||||||
| @@ -156,7 +156,7 @@ class StrobeLightEffect : public LightEffect { | |||||||
|  |  | ||||||
|     if (!color.is_on()) { |     if (!color.is_on()) { | ||||||
|       // Don't turn the light off, otherwise the light effect will be stopped |       // 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_state(true); | ||||||
|     } |     } | ||||||
|     call.set_publish(false); |     call.set_publish(false); | ||||||
|   | |||||||
| @@ -100,6 +100,7 @@ bool MQTTFanComponent::publish_state() { | |||||||
|   auto traits = this->state_->get_traits(); |   auto traits = this->state_->get_traits(); | ||||||
|   if (traits.supports_speed()) { |   if (traits.supports_speed()) { | ||||||
|     const char *payload; |     const char *payload; | ||||||
|  |     // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) | ||||||
|     switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) { |     switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) { | ||||||
|       case FAN_SPEED_LOW: {  // NOLINT(clang-diagnostic-deprecated-declarations) |       case FAN_SPEED_LOW: {  // NOLINT(clang-diagnostic-deprecated-declarations) | ||||||
|         payload = "low"; |         payload = "low"; | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend( | |||||||
|     { |     { | ||||||
|         cv.GenerateID(): cv.declare_id(SNTPComponent), |         cv.GenerateID(): cv.declare_id(SNTPComponent), | ||||||
|         cv.Optional(CONF_SERVERS, default=DEFAULT_SERVERS): cv.All( |         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) | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ namespace t6615 { | |||||||
|  |  | ||||||
| static const char *const TAG = "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_MAGIC = 0xFF; | ||||||
| static const uint8_t T6615_ADDR_HOST = 0xFA; | static const uint8_t T6615_ADDR_HOST = 0xFA; | ||||||
| static const uint8_t T6615_ADDR_SENSOR = 0xFE; | 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_DISABLE_ABC[] = {0xB7, 0x02}; | ||||||
| static const uint8_t T6615_COMMAND_SET_ELEVATION[] = {0x03, 0x0F}; | static const uint8_t T6615_COMMAND_SET_ELEVATION[] = {0x03, 0x0F}; | ||||||
|  |  | ||||||
| void T6615Component::loop() { | void T6615Component::send_ppm_command_() { | ||||||
|   if (!this->available()) |   this->command_time_ = millis(); | ||||||
|     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)); | ||||||
|  | } | ||||||
|  |  | ||||||
|   // Read header | void T6615Component::loop() { | ||||||
|   uint8_t header[3]; |   if (this->available() < 5) { | ||||||
|   this->read_array(header, 3); |     if (this->command_ == T6615Command::GET_PPM && millis() - this->command_time_ > T6615_TIMEOUT) { | ||||||
|   if (header[0] != T6615_MAGIC || header[1] != T6615_ADDR_HOST) { |       /* command got eaten, clear the buffer and fire another */ | ||||||
|     ESP_LOGW(TAG, "Reading data from T6615 failed!"); |  | ||||||
|       while (this->available()) |       while (this->available()) | ||||||
|       this->read();  // Clear the incoming buffer |         this->read(); | ||||||
|     this->status_set_warning(); |       this->send_ppm_command_(); | ||||||
|  |     } | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Read body |   uint8_t response_buffer[6]; | ||||||
|   uint8_t length = header[2]; |  | ||||||
|   uint8_t response[T6615_RESPONSE_BUFFER_LENGTH]; |   /* by the time we get here, we know we have at least five bytes in the buffer */ | ||||||
|   this->read_array(response, length); |   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(); |   this->status_clear_warning(); | ||||||
|  |  | ||||||
|   switch (this->command_) { |   switch (this->command_) { | ||||||
|     case T6615Command::GET_PPM: { |     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); |       ESP_LOGD(TAG, "T6615 Received CO₂=%uppm", ppm); | ||||||
|       this->co2_sensor_->publish_state(ppm); |       this->co2_sensor_->publish_state(ppm); | ||||||
|       break; |       break; | ||||||
| @@ -51,23 +69,19 @@ void T6615Component::loop() { | |||||||
|     default: |     default: | ||||||
|       break; |       break; | ||||||
|   } |   } | ||||||
|  |   this->command_time_ = 0; | ||||||
|   this->command_ = T6615Command::NONE; |   this->command_ = T6615Command::NONE; | ||||||
| } | } | ||||||
|  |  | ||||||
| void T6615Component::update() { this->query_ppm_(); } | void T6615Component::update() { this->query_ppm_(); } | ||||||
|  |  | ||||||
| void T6615Component::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; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   this->command_ = T6615Command::GET_PPM; |   this->send_ppm_command_(); | ||||||
|  |  | ||||||
|   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)); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| float T6615Component::get_setup_priority() const { return setup_priority::DATA; } | float T6615Component::get_setup_priority() const { return setup_priority::DATA; } | ||||||
|   | |||||||
| @@ -32,8 +32,10 @@ class T6615Component : public PollingComponent, public uart::UARTDevice { | |||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   void query_ppm_(); |   void query_ppm_(); | ||||||
|  |   void send_ppm_command_(); | ||||||
|  |  | ||||||
|   T6615Command command_ = T6615Command::NONE; |   T6615Command command_ = T6615Command::NONE; | ||||||
|  |   unsigned long command_time_ = 0; | ||||||
|  |  | ||||||
|   sensor::Sensor *co2_sensor_{nullptr}; |   sensor::Sensor *co2_sensor_{nullptr}; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -397,6 +397,7 @@ std::string WebServer::fan_json(fan::FanState *obj) { | |||||||
|     const auto traits = obj->get_traits(); |     const auto traits = obj->get_traits(); | ||||||
|     if (traits.supports_speed()) { |     if (traits.supports_speed()) { | ||||||
|       root["speed_level"] = obj->speed; |       root["speed_level"] = obj->speed; | ||||||
|  |       // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) | ||||||
|       switch (fan::speed_level_to_enum(obj->speed, traits.supported_speed_count())) { |       switch (fan::speed_level_to_enum(obj->speed, traits.supported_speed_count())) { | ||||||
|         case fan::FAN_SPEED_LOW:  // NOLINT(clang-diagnostic-deprecated-declarations) |         case fan::FAN_SPEED_LOW:  // NOLINT(clang-diagnostic-deprecated-declarations) | ||||||
|           root["speed"] = "low"; |           root["speed"] = "low"; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| """Constants used by esphome.""" | """Constants used by esphome.""" | ||||||
|  |  | ||||||
| __version__ = "2021.9.0b3" | __version__ = "2021.9.0b4" | ||||||
|  |  | ||||||
| ESP_PLATFORM_ESP32 = "ESP32" | ESP_PLATFORM_ESP32 = "ESP32" | ||||||
| ESP_PLATFORM_ESP8266 = "ESP8266" | ESP_PLATFORM_ESP8266 = "ESP8266" | ||||||
|   | |||||||
| @@ -4,9 +4,6 @@ import time | |||||||
| from typing import Dict, Optional | from typing import Dict, Optional | ||||||
|  |  | ||||||
| from zeroconf import ( | from zeroconf import ( | ||||||
|     _CLASS_IN, |  | ||||||
|     _FLAGS_QR_QUERY, |  | ||||||
|     _TYPE_A, |  | ||||||
|     DNSAddress, |     DNSAddress, | ||||||
|     DNSOutgoing, |     DNSOutgoing, | ||||||
|     DNSRecord, |     DNSRecord, | ||||||
| @@ -15,6 +12,10 @@ from zeroconf import ( | |||||||
|     Zeroconf, |     Zeroconf, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | _CLASS_IN = 1 | ||||||
|  | _FLAGS_QR_QUERY = 0x0000  # query | ||||||
|  | _TYPE_A = 1 | ||||||
|  |  | ||||||
|  |  | ||||||
| class HostResolver(RecordUpdateListener): | class HostResolver(RecordUpdateListener): | ||||||
|     def __init__(self, name: str): |     def __init__(self, name: str): | ||||||
|   | |||||||
| @@ -3,12 +3,11 @@ PyYAML==5.4.1 | |||||||
| paho-mqtt==1.5.1 | paho-mqtt==1.5.1 | ||||||
| colorama==0.4.4 | colorama==0.4.4 | ||||||
| tornado==6.1 | tornado==6.1 | ||||||
| protobuf==3.17.3 |  | ||||||
| tzlocal==2.1 | tzlocal==2.1 | ||||||
| pytz==2021.1 | pytz==2021.1 | ||||||
| pyserial==3.5 | pyserial==3.5 | ||||||
| ifaddr==0.1.7 | platformio==5.2.0 | ||||||
| platformio==5.1.1 |  | ||||||
| esptool==3.1 | esptool==3.1 | ||||||
| click==7.1.2 | click==7.1.2 | ||||||
| esphome-dashboard==20210908.0 | esphome-dashboard==20210908.0 | ||||||
|  | aioesphomeapi==9.0.0 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user