mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Merge branch 'beta' into bump-2021.9.0
This commit is contained in:
		
							
								
								
									
										11
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								CODEOWNERS
									
									
									
									
									
								
							| @@ -14,6 +14,8 @@ esphome/core/* @esphome/core | |||||||
| esphome/components/ac_dimmer/* @glmnet | esphome/components/ac_dimmer/* @glmnet | ||||||
| esphome/components/adc/* @esphome/core | esphome/components/adc/* @esphome/core | ||||||
| esphome/components/addressable_light/* @justfalter | esphome/components/addressable_light/* @justfalter | ||||||
|  | esphome/components/airthings_ble/* @jeromelaban | ||||||
|  | esphome/components/airthings_wave_plus/* @jeromelaban | ||||||
| esphome/components/am43/* @buxtronix | esphome/components/am43/* @buxtronix | ||||||
| esphome/components/am43/cover/* @buxtronix | esphome/components/am43/cover/* @buxtronix | ||||||
| esphome/components/animation/* @syndlex | esphome/components/animation/* @syndlex | ||||||
| @@ -29,6 +31,7 @@ esphome/components/ble_client/* @buxtronix | |||||||
| esphome/components/bme680_bsec/* @trvrnrth | esphome/components/bme680_bsec/* @trvrnrth | ||||||
| esphome/components/canbus/* @danielschramm @mvturnho | esphome/components/canbus/* @danielschramm @mvturnho | ||||||
| esphome/components/captive_portal/* @OttoWinter | esphome/components/captive_portal/* @OttoWinter | ||||||
|  | esphome/components/ccs811/* @habbie | ||||||
| esphome/components/climate/* @esphome/core | esphome/components/climate/* @esphome/core | ||||||
| esphome/components/climate_ir/* @glmnet | esphome/components/climate_ir/* @glmnet | ||||||
| esphome/components/color_temperature/* @jesserockz | esphome/components/color_temperature/* @jesserockz | ||||||
| @@ -52,6 +55,8 @@ esphome/components/globals/* @esphome/core | |||||||
| esphome/components/gpio/* @esphome/core | esphome/components/gpio/* @esphome/core | ||||||
| esphome/components/gps/* @coogle | esphome/components/gps/* @coogle | ||||||
| esphome/components/havells_solar/* @sourabhjaiswal | esphome/components/havells_solar/* @sourabhjaiswal | ||||||
|  | esphome/components/hbridge/fan/* @WeekendWarrior | ||||||
|  | esphome/components/hbridge/light/* @DotNetDann | ||||||
| esphome/components/hitachi_ac424/* @sourabhjaiswal | esphome/components/hitachi_ac424/* @sourabhjaiswal | ||||||
| esphome/components/homeassistant/* @OttoWinter | esphome/components/homeassistant/* @OttoWinter | ||||||
| esphome/components/hrxl_maxsonar_wr/* @netmikey | esphome/components/hrxl_maxsonar_wr/* @netmikey | ||||||
| @@ -75,8 +80,7 @@ esphome/components/mcp23x17_base/* @jesserockz | |||||||
| esphome/components/mcp23xxx_base/* @jesserockz | esphome/components/mcp23xxx_base/* @jesserockz | ||||||
| esphome/components/mcp2515/* @danielschramm @mvturnho | esphome/components/mcp2515/* @danielschramm @mvturnho | ||||||
| esphome/components/mcp9808/* @k7hpn | esphome/components/mcp9808/* @k7hpn | ||||||
| esphome/components/midea_ac/* @dudanov | esphome/components/midea/* @dudanov | ||||||
| esphome/components/midea_dongle/* @dudanov |  | ||||||
| esphome/components/mitsubishi/* @RubyBailey | esphome/components/mitsubishi/* @RubyBailey | ||||||
| esphome/components/network/* @esphome/core | esphome/components/network/* @esphome/core | ||||||
| esphome/components/nextion/* @senexcrenshaw | esphome/components/nextion/* @senexcrenshaw | ||||||
| @@ -90,6 +94,7 @@ esphome/components/ota/* @esphome/core | |||||||
| esphome/components/output/* @esphome/core | esphome/components/output/* @esphome/core | ||||||
| esphome/components/pid/* @OttoWinter | esphome/components/pid/* @OttoWinter | ||||||
| esphome/components/pipsolar/* @andreashergert1984 | esphome/components/pipsolar/* @andreashergert1984 | ||||||
|  | esphome/components/pm1006/* @habbie | ||||||
| esphome/components/pmsa003i/* @sjtrny | esphome/components/pmsa003i/* @sjtrny | ||||||
| esphome/components/pn532/* @OttoWinter @jesserockz | esphome/components/pn532/* @OttoWinter @jesserockz | ||||||
| esphome/components/pn532_i2c/* @OttoWinter @jesserockz | esphome/components/pn532_i2c/* @OttoWinter @jesserockz | ||||||
| @@ -115,6 +120,7 @@ esphome/components/sht4x/* @sjtrny | |||||||
| esphome/components/shutdown/* @esphome/core | esphome/components/shutdown/* @esphome/core | ||||||
| esphome/components/sim800l/* @glmnet | esphome/components/sim800l/* @glmnet | ||||||
| esphome/components/sm2135/* @BoukeHaarsma23 | esphome/components/sm2135/* @BoukeHaarsma23 | ||||||
|  | esphome/components/socket/* @esphome/core | ||||||
| esphome/components/spi/* @esphome/core | esphome/components/spi/* @esphome/core | ||||||
| esphome/components/ssd1322_base/* @kbx81 | esphome/components/ssd1322_base/* @kbx81 | ||||||
| esphome/components/ssd1322_spi/* @kbx81 | esphome/components/ssd1322_spi/* @kbx81 | ||||||
| @@ -129,6 +135,7 @@ esphome/components/ssd1351_base/* @kbx81 | |||||||
| esphome/components/ssd1351_spi/* @kbx81 | esphome/components/ssd1351_spi/* @kbx81 | ||||||
| esphome/components/st7735/* @SenexCrenshaw | esphome/components/st7735/* @SenexCrenshaw | ||||||
| esphome/components/st7789v/* @kbx81 | esphome/components/st7789v/* @kbx81 | ||||||
|  | esphome/components/st7920/* @marsjan155 | ||||||
| esphome/components/substitutions/* @esphome/core | esphome/components/substitutions/* @esphome/core | ||||||
| esphome/components/sun/* @OttoWinter | esphome/components/sun/* @OttoWinter | ||||||
| esphome/components/switch/* @esphome/core | esphome/components/switch/* @esphome/core | ||||||
|   | |||||||
| @@ -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 |  | ||||||
| @@ -55,6 +55,7 @@ ESP8266_BOARD_PINS = { | |||||||
|     "espectro": {"LED": 15, "BUTTON": 2}, |     "espectro": {"LED": 15, "BUTTON": 2}, | ||||||
|     "espino": {"LED": 2, "LED_RED": 2, "LED_GREEN": 4, "LED_BLUE": 5, "BUTTON": 0}, |     "espino": {"LED": 2, "LED_RED": 2, "LED_GREEN": 4, "LED_BLUE": 5, "BUTTON": 0}, | ||||||
|     "espinotee": {"LED": 16}, |     "espinotee": {"LED": 16}, | ||||||
|  |     "espmxdevkit": {}, | ||||||
|     "espresso_lite_v1": {"LED": 16}, |     "espresso_lite_v1": {"LED": 16}, | ||||||
|     "espresso_lite_v2": {"LED": 2}, |     "espresso_lite_v2": {"LED": 2}, | ||||||
|     "gen4iod": {}, |     "gen4iod": {}, | ||||||
| @@ -105,6 +106,10 @@ ESP8266_BOARD_PINS = { | |||||||
|     }, |     }, | ||||||
|     "phoenix_v1": {"LED": 16}, |     "phoenix_v1": {"LED": 16}, | ||||||
|     "phoenix_v2": {"LED": 2}, |     "phoenix_v2": {"LED": 2}, | ||||||
|  |     "sonoff_basic": {}, | ||||||
|  |     "sonoff_s20": {}, | ||||||
|  |     "sonoff_sv": {}, | ||||||
|  |     "sonoff_th": {}, | ||||||
|     "sparkfunBlynk": "thing", |     "sparkfunBlynk": "thing", | ||||||
|     "thing": {"LED": 5, "SDA": 2, "SCL": 14}, |     "thing": {"LED": 5, "SDA": 2, "SCL": 14}, | ||||||
|     "thingdev": "thing", |     "thingdev": "thing", | ||||||
| @@ -166,6 +171,7 @@ ESP8266_FLASH_SIZES = { | |||||||
|     "espectro": FLASH_SIZE_4_MB, |     "espectro": FLASH_SIZE_4_MB, | ||||||
|     "espino": FLASH_SIZE_4_MB, |     "espino": FLASH_SIZE_4_MB, | ||||||
|     "espinotee": FLASH_SIZE_4_MB, |     "espinotee": FLASH_SIZE_4_MB, | ||||||
|  |     "espmxdevkit": FLASH_SIZE_1_MB, | ||||||
|     "espresso_lite_v1": FLASH_SIZE_4_MB, |     "espresso_lite_v1": FLASH_SIZE_4_MB, | ||||||
|     "espresso_lite_v2": FLASH_SIZE_4_MB, |     "espresso_lite_v2": FLASH_SIZE_4_MB, | ||||||
|     "gen4iod": FLASH_SIZE_512_KB, |     "gen4iod": FLASH_SIZE_512_KB, | ||||||
| @@ -178,6 +184,10 @@ ESP8266_FLASH_SIZES = { | |||||||
|     "oak": FLASH_SIZE_4_MB, |     "oak": FLASH_SIZE_4_MB, | ||||||
|     "phoenix_v1": FLASH_SIZE_4_MB, |     "phoenix_v1": FLASH_SIZE_4_MB, | ||||||
|     "phoenix_v2": FLASH_SIZE_4_MB, |     "phoenix_v2": FLASH_SIZE_4_MB, | ||||||
|  |     "sonoff_basic": FLASH_SIZE_1_MB, | ||||||
|  |     "sonoff_s20": FLASH_SIZE_1_MB, | ||||||
|  |     "sonoff_sv": FLASH_SIZE_1_MB, | ||||||
|  |     "sonoff_th": FLASH_SIZE_1_MB, | ||||||
|     "sparkfunBlynk": FLASH_SIZE_4_MB, |     "sparkfunBlynk": FLASH_SIZE_4_MB, | ||||||
|     "thing": FLASH_SIZE_512_KB, |     "thing": FLASH_SIZE_512_KB, | ||||||
|     "thingdev": FLASH_SIZE_512_KB, |     "thingdev": FLASH_SIZE_512_KB, | ||||||
| @@ -291,6 +301,7 @@ ESP32_BOARD_PINS = { | |||||||
|         "SW2": 2, |         "SW2": 2, | ||||||
|         "SW3": 0, |         "SW3": 0, | ||||||
|     }, |     }, | ||||||
|  |     "az-delivery-devkit-v4": {}, | ||||||
|     "bpi-bit": { |     "bpi-bit": { | ||||||
|         "BUTTON_A": 35, |         "BUTTON_A": 35, | ||||||
|         "BUTTON_B": 27, |         "BUTTON_B": 27, | ||||||
| @@ -320,6 +331,8 @@ ESP32_BOARD_PINS = { | |||||||
|         "RGB_LED": 4, |         "RGB_LED": 4, | ||||||
|         "TEMPERATURE_SENSOR": 34, |         "TEMPERATURE_SENSOR": 34, | ||||||
|     }, |     }, | ||||||
|  |     "briki_abc_esp32": {}, | ||||||
|  |     "briki_mbc-wb_esp32": {}, | ||||||
|     "d-duino-32": { |     "d-duino-32": { | ||||||
|         "D1": 5, |         "D1": 5, | ||||||
|         "D10": 1, |         "D10": 1, | ||||||
| @@ -380,11 +393,58 @@ ESP32_BOARD_PINS = { | |||||||
|     "esp32cam": {}, |     "esp32cam": {}, | ||||||
|     "esp32dev": {}, |     "esp32dev": {}, | ||||||
|     "esp32doit-devkit-v1": {"LED": 2}, |     "esp32doit-devkit-v1": {"LED": 2}, | ||||||
|  |     "esp32doit-espduino": {"TX0": 1, "RX0": 3, "CMD": 11, "CLK": 6, "SD0": 7, "SD1": 8}, | ||||||
|     "esp32thing": {"BUTTON": 0, "LED": 5, "SS": 2}, |     "esp32thing": {"BUTTON": 0, "LED": 5, "SS": 2}, | ||||||
|  |     "esp32thing_plus": { | ||||||
|  |         "SDA": 23, | ||||||
|  |         "SCL": 22, | ||||||
|  |         "SS": 33, | ||||||
|  |         "MOSI": 18, | ||||||
|  |         "MISO": 19, | ||||||
|  |         "SCK": 5, | ||||||
|  |         "A0": 26, | ||||||
|  |         "A1": 25, | ||||||
|  |         "A2": 34, | ||||||
|  |         "A3": 39, | ||||||
|  |         "A4": 36, | ||||||
|  |         "A5": 4, | ||||||
|  |         "A6": 14, | ||||||
|  |         "A7": 32, | ||||||
|  |         "A8": 15, | ||||||
|  |         "A9": 33, | ||||||
|  |         "A10": 27, | ||||||
|  |         "A11": 12, | ||||||
|  |         "A12": 13, | ||||||
|  |     }, | ||||||
|     "esp32vn-iot-uno": {}, |     "esp32vn-iot-uno": {}, | ||||||
|     "espea32": {"BUTTON": 0, "LED": 5}, |     "espea32": {"BUTTON": 0, "LED": 5}, | ||||||
|     "espectro32": {"LED": 15, "SD_SS": 33}, |     "espectro32": {"LED": 15, "SD_SS": 33}, | ||||||
|     "espino32": {"BUTTON": 0, "LED": 16}, |     "espino32": {"BUTTON": 0, "LED": 16}, | ||||||
|  |     "etboard": { | ||||||
|  |         "LED_BUILTIN": 5, | ||||||
|  |         "TX": 34, | ||||||
|  |         "RX": 35, | ||||||
|  |         "SS": 29, | ||||||
|  |         "MOSI": 37, | ||||||
|  |         "MISO": 31, | ||||||
|  |         "SCK": 30, | ||||||
|  |         "A0": 36, | ||||||
|  |         "A1": 39, | ||||||
|  |         "A2": 32, | ||||||
|  |         "A3": 33, | ||||||
|  |         "A4": 34, | ||||||
|  |         "A5": 35, | ||||||
|  |         "A6": 25, | ||||||
|  |         "A7": 26, | ||||||
|  |         "D2": 27, | ||||||
|  |         "D3": 14, | ||||||
|  |         "D4": 12, | ||||||
|  |         "D5": 13, | ||||||
|  |         "D6": 15, | ||||||
|  |         "D7": 16, | ||||||
|  |         "D8": 17, | ||||||
|  |         "D9": 4, | ||||||
|  |     }, | ||||||
|     "featheresp32": { |     "featheresp32": { | ||||||
|         "A0": 26, |         "A0": 26, | ||||||
|         "A1": 25, |         "A1": 25, | ||||||
| @@ -434,6 +494,18 @@ ESP32_BOARD_PINS = { | |||||||
|         "SW4": 21, |         "SW4": 21, | ||||||
|     }, |     }, | ||||||
|     "frogboard": {}, |     "frogboard": {}, | ||||||
|  |     "healtypi4": { | ||||||
|  |         "KEY_BUILTIN": 17, | ||||||
|  |         "ADS1292_DRDY_PIN": 26, | ||||||
|  |         "ADS1292_CS_PIN": 13, | ||||||
|  |         "ADS1292_START_PIN": 14, | ||||||
|  |         "ADS1292_PWDN_PIN": 27, | ||||||
|  |         "AFE4490_CS_PIN": 21, | ||||||
|  |         "AFE4490_DRDY_PIN": 39, | ||||||
|  |         "AFE4490_PWDN_PIN": 4, | ||||||
|  |         "PUSH_BUTTON": 17, | ||||||
|  |         "SLIDE_SWITCH": 16, | ||||||
|  |     }, | ||||||
|     "heltec_wifi_kit_32": { |     "heltec_wifi_kit_32": { | ||||||
|         "A1": 37, |         "A1": 37, | ||||||
|         "A2": 38, |         "A2": 38, | ||||||
| @@ -444,6 +516,7 @@ ESP32_BOARD_PINS = { | |||||||
|         "SDA_OLED": 4, |         "SDA_OLED": 4, | ||||||
|         "Vext": 21, |         "Vext": 21, | ||||||
|     }, |     }, | ||||||
|  |     "heltec_wifi_kit_32_v2": "heltec_wifi_kit_32", | ||||||
|     "heltec_wifi_lora_32": { |     "heltec_wifi_lora_32": { | ||||||
|         "BUTTON": 0, |         "BUTTON": 0, | ||||||
|         "DIO0": 26, |         "DIO0": 26, | ||||||
| @@ -489,8 +562,68 @@ ESP32_BOARD_PINS = { | |||||||
|         "SS": 18, |         "SS": 18, | ||||||
|         "Vext": 21, |         "Vext": 21, | ||||||
|     }, |     }, | ||||||
|  |     "heltec_wireless_stick_lite": { | ||||||
|  |         "LED_BUILTIN": 25, | ||||||
|  |         "KEY_BUILTIN": 0, | ||||||
|  |         "SS": 18, | ||||||
|  |         "MOSI": 27, | ||||||
|  |         "MISO": 19, | ||||||
|  |         "SCK": 5, | ||||||
|  |         "Vext": 21, | ||||||
|  |         "LED": 25, | ||||||
|  |         "RST_LoRa": 14, | ||||||
|  |         "DIO0": 26, | ||||||
|  |         "DIO1": 35, | ||||||
|  |         "DIO2": 34, | ||||||
|  |     }, | ||||||
|  |     "honeylemon": { | ||||||
|  |         "LED_BUILTIN": 2, | ||||||
|  |         "BUILTIN_KEY": 0, | ||||||
|  |     }, | ||||||
|     "hornbill32dev": {"BUTTON": 0, "LED": 13}, |     "hornbill32dev": {"BUTTON": 0, "LED": 13}, | ||||||
|     "hornbill32minima": {"SS": 2}, |     "hornbill32minima": {"SS": 2}, | ||||||
|  |     "imbrios-logsens-v1p1": { | ||||||
|  |         "LED_BUILTIN": 33, | ||||||
|  |         "UART2_TX": 17, | ||||||
|  |         "UART2_RX": 16, | ||||||
|  |         "UART2_RTS": 4, | ||||||
|  |         "CAN_TX": 17, | ||||||
|  |         "CAN_RX": 16, | ||||||
|  |         "CAN_TXDE": 4, | ||||||
|  |         "SS": 15, | ||||||
|  |         "MOSI": 13, | ||||||
|  |         "MISO": 12, | ||||||
|  |         "SCK": 14, | ||||||
|  |         "SPI_SS1": 23, | ||||||
|  |         "BUZZER_CTRL": 19, | ||||||
|  |         "SD_CARD_DETECT": 35, | ||||||
|  |         "SW2_BUILDIN": 0, | ||||||
|  |         "SW3_BUILDIN": 36, | ||||||
|  |         "SW4_BUILDIN": 34, | ||||||
|  |         "LED1_BUILDIN": 32, | ||||||
|  |         "LED2_BUILDIN": 33, | ||||||
|  |     }, | ||||||
|  |     "inex_openkb": { | ||||||
|  |         "LED_BUILTIN": 16, | ||||||
|  |         "LDR_PIN": 36, | ||||||
|  |         "SW1": 16, | ||||||
|  |         "SW2": 14, | ||||||
|  |         "BT_LED": 17, | ||||||
|  |         "WIFI_LED": 2, | ||||||
|  |         "NTP_LED": 15, | ||||||
|  |         "IOT_LED": 12, | ||||||
|  |         "BUZZER": 13, | ||||||
|  |         "INPUT1": 32, | ||||||
|  |         "INPUT2": 33, | ||||||
|  |         "INPUT3": 34, | ||||||
|  |         "INPUT4": 35, | ||||||
|  |         "OUTPUT1": 26, | ||||||
|  |         "OUTPUT2": 27, | ||||||
|  |         "SDA0": 21, | ||||||
|  |         "SCL0": 22, | ||||||
|  |         "SDA1": 4, | ||||||
|  |         "SCL1": 5, | ||||||
|  |     }, | ||||||
|     "intorobot": { |     "intorobot": { | ||||||
|         "A1": 39, |         "A1": 39, | ||||||
|         "A2": 35, |         "A2": 35, | ||||||
| @@ -528,6 +661,40 @@ ESP32_BOARD_PINS = { | |||||||
|     "iotaap_magnolia": {}, |     "iotaap_magnolia": {}, | ||||||
|     "iotbusio": {}, |     "iotbusio": {}, | ||||||
|     "iotbusproteus": {}, |     "iotbusproteus": {}, | ||||||
|  |     "kits-edu": {}, | ||||||
|  |     "labplus_mpython": { | ||||||
|  |         "SDA": 23, | ||||||
|  |         "SCL": 22, | ||||||
|  |         "P0": 33, | ||||||
|  |         "P1": 32, | ||||||
|  |         "P2": 35, | ||||||
|  |         "P3": 34, | ||||||
|  |         "P4": 39, | ||||||
|  |         "P5": 0, | ||||||
|  |         "P6": 16, | ||||||
|  |         "P7": 17, | ||||||
|  |         "P8": 26, | ||||||
|  |         "P9": 25, | ||||||
|  |         "P10": 36, | ||||||
|  |         "P11": 2, | ||||||
|  |         "P13": 18, | ||||||
|  |         "P14": 19, | ||||||
|  |         "P15": 21, | ||||||
|  |         "P16": 5, | ||||||
|  |         "P19": 22, | ||||||
|  |         "P20": 23, | ||||||
|  |         "P": 27, | ||||||
|  |         "Y": 14, | ||||||
|  |         "T": 12, | ||||||
|  |         "H": 13, | ||||||
|  |         "O": 15, | ||||||
|  |         "N": 4, | ||||||
|  |         "BTN_A": 0, | ||||||
|  |         "BTN_B": 2, | ||||||
|  |         "SOUND": 36, | ||||||
|  |         "LIGHT": 39, | ||||||
|  |         "BUZZER": 16, | ||||||
|  |     }, | ||||||
|     "lolin32": {"LED": 5}, |     "lolin32": {"LED": 5}, | ||||||
|     "lolin32_lite": {"LED": 22}, |     "lolin32_lite": {"LED": 22}, | ||||||
|     "lolin_d32": {"LED": 5, "_VBAT": 35}, |     "lolin_d32": {"LED": 5, "_VBAT": 35}, | ||||||
| @@ -554,6 +721,16 @@ ESP32_BOARD_PINS = { | |||||||
|         "SDA": 12, |         "SDA": 12, | ||||||
|         "SS": 18, |         "SS": 18, | ||||||
|     }, |     }, | ||||||
|  |     "m5stack-atom": { | ||||||
|  |         "SDA": 26, | ||||||
|  |         "SCL": 32, | ||||||
|  |         "ADC1": 35, | ||||||
|  |         "ADC2": 36, | ||||||
|  |         "SS": 19, | ||||||
|  |         "MOSI": 33, | ||||||
|  |         "MISO": 23, | ||||||
|  |         "SCK": 22, | ||||||
|  |     }, | ||||||
|     "m5stack-core-esp32": { |     "m5stack-core-esp32": { | ||||||
|         "ADC1": 35, |         "ADC1": 35, | ||||||
|         "ADC2": 36, |         "ADC2": 36, | ||||||
| @@ -580,6 +757,26 @@ ESP32_BOARD_PINS = { | |||||||
|         "RXD2": 16, |         "RXD2": 16, | ||||||
|         "TXD2": 17, |         "TXD2": 17, | ||||||
|     }, |     }, | ||||||
|  |     "m5stack-core2": { | ||||||
|  |         "SDA": 32, | ||||||
|  |         "SCL": 33, | ||||||
|  |         "SS": 5, | ||||||
|  |         "MOSI": 23, | ||||||
|  |         "MISO": 38, | ||||||
|  |         "SCK": 18, | ||||||
|  |         "ADC1": 35, | ||||||
|  |         "ADC2": 36, | ||||||
|  |     }, | ||||||
|  |     "m5stack-coreink": { | ||||||
|  |         "SDA": 32, | ||||||
|  |         "SCL": 33, | ||||||
|  |         "SS": 9, | ||||||
|  |         "MOSI": 23, | ||||||
|  |         "MISO": 34, | ||||||
|  |         "SCK": 18, | ||||||
|  |         "ADC1": 35, | ||||||
|  |         "ADC2": 36, | ||||||
|  |     }, | ||||||
|     "m5stack-fire": { |     "m5stack-fire": { | ||||||
|         "ADC1": 35, |         "ADC1": 35, | ||||||
|         "ADC2": 36, |         "ADC2": 36, | ||||||
| @@ -630,6 +827,17 @@ ESP32_BOARD_PINS = { | |||||||
|         "RXD2": 16, |         "RXD2": 16, | ||||||
|         "TXD2": 17, |         "TXD2": 17, | ||||||
|     }, |     }, | ||||||
|  |     "m5stack-timer-cam": { | ||||||
|  |         "LED_BUILTIN": 2, | ||||||
|  |         "SDA": 4, | ||||||
|  |         "SCL": 13, | ||||||
|  |         "SS": 5, | ||||||
|  |         "MOSI": 23, | ||||||
|  |         "MISO": 19, | ||||||
|  |         "SCK": 18, | ||||||
|  |         "ADC1": 35, | ||||||
|  |         "ADC2": 36, | ||||||
|  |     }, | ||||||
|     "m5stick-c": { |     "m5stick-c": { | ||||||
|         "ADC1": 35, |         "ADC1": 35, | ||||||
|         "ADC2": 36, |         "ADC2": 36, | ||||||
| @@ -664,6 +872,17 @@ ESP32_BOARD_PINS = { | |||||||
|         "RIGHT_PUTTON": 34, |         "RIGHT_PUTTON": 34, | ||||||
|         "YELLOW_LED": 18, |         "YELLOW_LED": 18, | ||||||
|     }, |     }, | ||||||
|  |     "mgbot-iotik32a": { | ||||||
|  |         "LED_BUILTIN": 4, | ||||||
|  |         "TX2": 17, | ||||||
|  |         "RX2": 16, | ||||||
|  |     }, | ||||||
|  |     "mgbot-iotik32b": { | ||||||
|  |         "LED_BUILTIN": 18, | ||||||
|  |         "IR": 27, | ||||||
|  |         "TX2": 17, | ||||||
|  |         "RX2": 16, | ||||||
|  |     }, | ||||||
|     "mhetesp32devkit": {"LED": 2}, |     "mhetesp32devkit": {"LED": 2}, | ||||||
|     "mhetesp32minikit": {"LED": 2}, |     "mhetesp32minikit": {"LED": 2}, | ||||||
|     "microduino-core-esp32": { |     "microduino-core-esp32": { | ||||||
| @@ -740,6 +959,7 @@ ESP32_BOARD_PINS = { | |||||||
|     }, |     }, | ||||||
|     "node32s": {}, |     "node32s": {}, | ||||||
|     "nodemcu-32s": {"BUTTON": 0, "LED": 2}, |     "nodemcu-32s": {"BUTTON": 0, "LED": 2}, | ||||||
|  |     "nscreen-32": {}, | ||||||
|     "odroid_esp32": {"ADC1": 35, "ADC2": 36, "LED": 2, "SCL": 4, "SDA": 15, "SS": 22}, |     "odroid_esp32": {"ADC1": 35, "ADC2": 36, "LED": 2, "SCL": 4, "SDA": 15, "SS": 22}, | ||||||
|     "onehorse32dev": {"A1": 37, "A2": 38, "BUTTON": 0, "LED": 5}, |     "onehorse32dev": {"A1": 37, "A2": 38, "BUTTON": 0, "LED": 5}, | ||||||
|     "oroca_edubot": { |     "oroca_edubot": { | ||||||
| @@ -766,6 +986,10 @@ ESP32_BOARD_PINS = { | |||||||
|         "VBAT": 35, |         "VBAT": 35, | ||||||
|     }, |     }, | ||||||
|     "pico32": {}, |     "pico32": {}, | ||||||
|  |     "piranha_esp32": { | ||||||
|  |         "LED_BUILTIN": 2, | ||||||
|  |         "KEY_BUILTIN": 0, | ||||||
|  |     }, | ||||||
|     "pocket_32": {"LED": 16}, |     "pocket_32": {"LED": 16}, | ||||||
|     "pycom_gpy": { |     "pycom_gpy": { | ||||||
|         "A1": 37, |         "A1": 37, | ||||||
| @@ -778,7 +1002,14 @@ ESP32_BOARD_PINS = { | |||||||
|         "SDA": 12, |         "SDA": 12, | ||||||
|         "SS": 17, |         "SS": 17, | ||||||
|     }, |     }, | ||||||
|  |     "qchip": "heltec_wifi_kit_32", | ||||||
|     "quantum": {}, |     "quantum": {}, | ||||||
|  |     "s_odi_ultra": { | ||||||
|  |         "LED_BUILTIN": 2, | ||||||
|  |         "LED_BUILTINB": 4, | ||||||
|  |     }, | ||||||
|  |     "sensesiot_weizen": {}, | ||||||
|  |     "sg-o_airMon": {}, | ||||||
|     "sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16}, |     "sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16}, | ||||||
|     "tinypico": {}, |     "tinypico": {}, | ||||||
|     "ttgo-lora32-v1": { |     "ttgo-lora32-v1": { | ||||||
| @@ -790,6 +1021,26 @@ ESP32_BOARD_PINS = { | |||||||
|         "SCK": 5, |         "SCK": 5, | ||||||
|         "SS": 18, |         "SS": 18, | ||||||
|     }, |     }, | ||||||
|  |     "ttgo-lora32-v2": { | ||||||
|  |         "LED_BUILTIN": 22, | ||||||
|  |         "KEY_BUILTIN": 0, | ||||||
|  |         "SS": 18, | ||||||
|  |         "MOSI": 27, | ||||||
|  |         "MISO": 19, | ||||||
|  |         "SCK": 5, | ||||||
|  |         "A1": 37, | ||||||
|  |         "A2": 38, | ||||||
|  |     }, | ||||||
|  |     "ttgo-lora32-v21": { | ||||||
|  |         "LED_BUILTIN": 25, | ||||||
|  |         "KEY_BUILTIN": 0, | ||||||
|  |         "SS": 18, | ||||||
|  |         "MOSI": 27, | ||||||
|  |         "MISO": 19, | ||||||
|  |         "SCK": 5, | ||||||
|  |         "A1": 37, | ||||||
|  |         "A2": 38, | ||||||
|  |     }, | ||||||
|     "ttgo-t-beam": {"BUTTON": 39, "LED": 14, "MOSI": 27, "SCK": 5, "SS": 18}, |     "ttgo-t-beam": {"BUTTON": 39, "LED": 14, "MOSI": 27, "SCK": 5, "SS": 18}, | ||||||
|     "ttgo-t-watch": {"BUTTON": 36, "MISO": 2, "MOSI": 15, "SCK": 14, "SS": 13}, |     "ttgo-t-watch": {"BUTTON": 36, "MISO": 2, "MOSI": 15, "SCK": 14, "SS": 13}, | ||||||
|     "ttgo-t1": {"LED": 22, "MISO": 2, "MOSI": 15, "SCK": 14, "SCL": 23, "SS": 13}, |     "ttgo-t1": {"LED": 22, "MISO": 2, "MOSI": 15, "SCK": 14, "SCL": 23, "SS": 13}, | ||||||
| @@ -855,6 +1106,32 @@ ESP32_BOARD_PINS = { | |||||||
|         "T5": 5, |         "T5": 5, | ||||||
|         "T6": 4, |         "T6": 4, | ||||||
|     }, |     }, | ||||||
|  |     "wifiduino32": { | ||||||
|  |         "LED_BUILTIN": 2, | ||||||
|  |         "KEY_BUILTIN": 0, | ||||||
|  |         "SDA": 5, | ||||||
|  |         "SCL": 16, | ||||||
|  |         "A0": 27, | ||||||
|  |         "A1": 14, | ||||||
|  |         "A2": 12, | ||||||
|  |         "A3": 35, | ||||||
|  |         "A4": 13, | ||||||
|  |         "A5": 4, | ||||||
|  |         "D0": 3, | ||||||
|  |         "D1": 1, | ||||||
|  |         "D2": 17, | ||||||
|  |         "D3": 15, | ||||||
|  |         "D4": 32, | ||||||
|  |         "D5": 33, | ||||||
|  |         "D6": 25, | ||||||
|  |         "D7": 26, | ||||||
|  |         "D8": 23, | ||||||
|  |         "D9": 22, | ||||||
|  |         "D10": 21, | ||||||
|  |         "D11": 19, | ||||||
|  |         "D12": 18, | ||||||
|  |         "D13": 2, | ||||||
|  |     }, | ||||||
|     "xinabox_cw02": {"LED": 27}, |     "xinabox_cw02": {"LED": 27}, | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -44,6 +44,7 @@ void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) { | |||||||
|   for (int led = it.size(); led-- > 0;) { |   for (int led = it.size(); led-- > 0;) { | ||||||
|     it[led].set(Color::BLACK); |     it[led].set(Color::BLACK); | ||||||
|   } |   } | ||||||
|  |   it.schedule_show(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void AdalightLightEffect::apply(light::AddressableLight &it, const Color ¤t_color) { | void AdalightLightEffect::apply(light::AddressableLight &it, const Color ¤t_color) { | ||||||
| @@ -133,6 +134,7 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL | |||||||
|     it[led].set(Color(led_data[0], led_data[1], led_data[2], white)); |     it[led].set(Color(led_data[0], led_data[1], led_data[2], white)); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   it.schedule_show(); | ||||||
|   return CONSUMED; |   return CONSUMED; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								esphome/components/airthings_ble/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/airthings_ble/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import esp32_ble_tracker | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | DEPENDENCIES = ["esp32_ble_tracker"] | ||||||
|  | CODEOWNERS = ["@jeromelaban"] | ||||||
|  |  | ||||||
|  | airthings_ble_ns = cg.esphome_ns.namespace("airthings_ble") | ||||||
|  | AirthingsListener = airthings_ble_ns.class_( | ||||||
|  |     "AirthingsListener", esp32_ble_tracker.ESPBTDeviceListener | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(AirthingsListener), | ||||||
|  |     } | ||||||
|  | ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     yield esp32_ble_tracker.register_ble_device(var, config) | ||||||
							
								
								
									
										33
									
								
								esphome/components/airthings_ble/airthings_listener.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								esphome/components/airthings_ble/airthings_listener.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | #include "airthings_listener.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | #ifdef ARDUINO_ARCH_ESP32 | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace airthings_ble { | ||||||
|  |  | ||||||
|  | static const char *TAG = "airthings_ble"; | ||||||
|  |  | ||||||
|  | bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { | ||||||
|  |   for (auto &it : device.get_manufacturer_datas()) { | ||||||
|  |     if (it.uuid == esp32_ble_tracker::ESPBTUUID::from_uint32(0x0334)) { | ||||||
|  |       if (it.data.size() < 4) | ||||||
|  |         continue; | ||||||
|  |  | ||||||
|  |       uint32_t sn = it.data[0]; | ||||||
|  |       sn |= ((uint32_t) it.data[1] << 8); | ||||||
|  |       sn |= ((uint32_t) it.data[2] << 16); | ||||||
|  |       sn |= ((uint32_t) it.data[3] << 24); | ||||||
|  |  | ||||||
|  |       ESP_LOGD(TAG, "Found AirThings device Serial:%u (MAC: %s)", sn, device.address_str().c_str()); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace airthings_ble | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif | ||||||
							
								
								
									
										20
									
								
								esphome/components/airthings_ble/airthings_listener.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								esphome/components/airthings_ble/airthings_listener.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #ifdef ARDUINO_ARCH_ESP32 | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||||
|  | #include <BLEDevice.h> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace airthings_ble { | ||||||
|  |  | ||||||
|  | class AirthingsListener : public esp32_ble_tracker::ESPBTDeviceListener { | ||||||
|  |  public: | ||||||
|  |   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace airthings_ble | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif | ||||||
							
								
								
									
										1
									
								
								esphome/components/airthings_wave_plus/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/airthings_wave_plus/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | CODEOWNERS = ["@jeromelaban"] | ||||||
							
								
								
									
										142
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | |||||||
|  | #include "airthings_wave_plus.h" | ||||||
|  |  | ||||||
|  | #ifdef ARDUINO_ARCH_ESP32 | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace airthings_wave_plus { | ||||||
|  |  | ||||||
|  | void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||||
|  |                                             esp_ble_gattc_cb_param_t *param) { | ||||||
|  |   switch (event) { | ||||||
|  |     case ESP_GATTC_OPEN_EVT: { | ||||||
|  |       if (param->open.status == ESP_GATT_OK) { | ||||||
|  |         ESP_LOGI(TAG, "Connected successfully!"); | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     case ESP_GATTC_DISCONNECT_EVT: { | ||||||
|  |       ESP_LOGW(TAG, "Disconnected!"); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||||
|  |       this->handle = 0; | ||||||
|  |       auto chr = this->parent()->get_characteristic(service_uuid, sensors_data_characteristic_uuid); | ||||||
|  |       if (chr == nullptr) { | ||||||
|  |         ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid.to_string().c_str(), | ||||||
|  |                  sensors_data_characteristic_uuid.to_string().c_str()); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |       this->handle = chr->handle; | ||||||
|  |       this->node_state = espbt::ClientState::Established; | ||||||
|  |  | ||||||
|  |       request_read_values_(); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     case ESP_GATTC_READ_CHAR_EVT: { | ||||||
|  |       if (param->read.conn_id != this->parent()->conn_id) | ||||||
|  |         break; | ||||||
|  |       if (param->read.status != ESP_GATT_OK) { | ||||||
|  |         ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |       if (param->read.handle == this->handle) { | ||||||
|  |         read_sensors_(param->read.value, param->read.value_len); | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     default: | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) { | ||||||
|  |   auto value = (WavePlusReadings *) raw_value; | ||||||
|  |  | ||||||
|  |   if (sizeof(WavePlusReadings) <= value_len) { | ||||||
|  |     ESP_LOGD(TAG, "version = %d", value->version); | ||||||
|  |  | ||||||
|  |     if (value->version == 1) { | ||||||
|  |       ESP_LOGD(TAG, "ambient light = %d", value->ambientLight); | ||||||
|  |  | ||||||
|  |       this->humidity_sensor_->publish_state(value->humidity / 2.0f); | ||||||
|  |       if (is_valid_radon_value_(value->radon)) { | ||||||
|  |         this->radon_sensor_->publish_state(value->radon); | ||||||
|  |       } | ||||||
|  |       if (is_valid_radon_value_(value->radon_lt)) { | ||||||
|  |         this->radon_long_term_sensor_->publish_state(value->radon_lt); | ||||||
|  |       } | ||||||
|  |       this->temperature_sensor_->publish_state(value->temperature / 100.0f); | ||||||
|  |       this->pressure_sensor_->publish_state(value->pressure / 50.0f); | ||||||
|  |       if (is_valid_co2_value_(value->co2)) { | ||||||
|  |         this->co2_sensor_->publish_state(value->co2); | ||||||
|  |       } | ||||||
|  |       if (is_valid_voc_value_(value->voc)) { | ||||||
|  |         this->tvoc_sensor_->publish_state(value->voc); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // This instance must not stay connected | ||||||
|  |       // so other clients can connect to it (e.g. the | ||||||
|  |       // mobile app). | ||||||
|  |       parent()->set_enabled(false); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool AirthingsWavePlus::is_valid_radon_value_(short radon) { return 0 <= radon && radon <= 16383; } | ||||||
|  |  | ||||||
|  | bool AirthingsWavePlus::is_valid_voc_value_(short voc) { return 0 <= voc && voc <= 16383; } | ||||||
|  |  | ||||||
|  | bool AirthingsWavePlus::is_valid_co2_value_(short co2) { return 0 <= co2 && co2 <= 16383; } | ||||||
|  |  | ||||||
|  | void AirthingsWavePlus::loop() {} | ||||||
|  |  | ||||||
|  | void AirthingsWavePlus::update() { | ||||||
|  |   if (this->node_state != espbt::ClientState::Established) { | ||||||
|  |     if (!parent()->enabled) { | ||||||
|  |       ESP_LOGW(TAG, "Reconnecting to device"); | ||||||
|  |       parent()->set_enabled(true); | ||||||
|  |       parent()->connect(); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGW(TAG, "Connection in progress"); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AirthingsWavePlus::request_read_values_() { | ||||||
|  |   auto status = | ||||||
|  |       esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE); | ||||||
|  |   if (status) { | ||||||
|  |     ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AirthingsWavePlus::dump_config() { | ||||||
|  |   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "Radon", this->radon_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "Radon Long Term", this->radon_long_term_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "Pressure", this->pressure_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "CO2", this->co2_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | AirthingsWavePlus::AirthingsWavePlus() : PollingComponent(10000) { | ||||||
|  |   auto service_bt = *BLEUUID::fromString(std::string("b42e1c08-ade7-11e4-89d3-123b93f75cba")).getNative(); | ||||||
|  |   auto characteristic_bt = *BLEUUID::fromString(std::string("b42e2a68-ade7-11e4-89d3-123b93f75cba")).getNative(); | ||||||
|  |  | ||||||
|  |   service_uuid = espbt::ESPBTUUID::from_uuid(service_bt); | ||||||
|  |   sensors_data_characteristic_uuid = espbt::ESPBTUUID::from_uuid(characteristic_bt); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AirthingsWavePlus::setup() {} | ||||||
|  |  | ||||||
|  | }  // namespace airthings_wave_plus | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif  // ARDUINO_ARCH_ESP32 | ||||||
							
								
								
									
										79
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/components/ble_client/ble_client.h" | ||||||
|  | #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||||
|  | #include "esphome/components/sensor/sensor.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include <algorithm> | ||||||
|  | #include <iterator> | ||||||
|  |  | ||||||
|  | #ifdef ARDUINO_ARCH_ESP32 | ||||||
|  | #include <esp_gattc_api.h> | ||||||
|  | #include <BLEDevice.h> | ||||||
|  |  | ||||||
|  | using namespace esphome::ble_client; | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace airthings_wave_plus { | ||||||
|  |  | ||||||
|  | static const char *TAG = "airthings_wave_plus"; | ||||||
|  |  | ||||||
|  | class AirthingsWavePlus : public PollingComponent, public BLEClientNode { | ||||||
|  |  public: | ||||||
|  |   AirthingsWavePlus(); | ||||||
|  |  | ||||||
|  |   void setup() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |   void update() override; | ||||||
|  |   void loop() override; | ||||||
|  |  | ||||||
|  |   void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||||
|  |                            esp_ble_gattc_cb_param_t *param) override; | ||||||
|  |  | ||||||
|  |   void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } | ||||||
|  |   void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; } | ||||||
|  |   void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } | ||||||
|  |   void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } | ||||||
|  |   void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } | ||||||
|  |   void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } | ||||||
|  |   void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   bool is_valid_radon_value_(short radon); | ||||||
|  |   bool is_valid_voc_value_(short voc); | ||||||
|  |   bool is_valid_co2_value_(short co2); | ||||||
|  |  | ||||||
|  |   void read_sensors_(uint8_t *value, uint16_t value_len); | ||||||
|  |   void request_read_values_(); | ||||||
|  |  | ||||||
|  |   sensor::Sensor *temperature_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *radon_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *radon_long_term_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *humidity_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *pressure_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *co2_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *tvoc_sensor_{nullptr}; | ||||||
|  |  | ||||||
|  |   uint16_t handle; | ||||||
|  |   espbt::ESPBTUUID service_uuid; | ||||||
|  |   espbt::ESPBTUUID sensors_data_characteristic_uuid; | ||||||
|  |  | ||||||
|  |   struct WavePlusReadings { | ||||||
|  |     uint8_t version; | ||||||
|  |     uint8_t humidity; | ||||||
|  |     uint8_t ambientLight; | ||||||
|  |     uint8_t unused01; | ||||||
|  |     uint16_t radon; | ||||||
|  |     uint16_t radon_lt; | ||||||
|  |     uint16_t temperature; | ||||||
|  |     uint16_t pressure; | ||||||
|  |     uint16_t co2; | ||||||
|  |     uint16_t voc; | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace airthings_wave_plus | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif  // ARDUINO_ARCH_ESP32 | ||||||
							
								
								
									
										116
									
								
								esphome/components/airthings_wave_plus/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								esphome/components/airthings_wave_plus/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import sensor, ble_client | ||||||
|  |  | ||||||
|  | from esphome.const import ( | ||||||
|  |     DEVICE_CLASS_CARBON_DIOXIDE, | ||||||
|  |     DEVICE_CLASS_HUMIDITY, | ||||||
|  |     DEVICE_CLASS_TEMPERATURE, | ||||||
|  |     DEVICE_CLASS_PRESSURE, | ||||||
|  |     STATE_CLASS_MEASUREMENT, | ||||||
|  |     UNIT_PERCENT, | ||||||
|  |     UNIT_CELSIUS, | ||||||
|  |     UNIT_HECTOPASCAL, | ||||||
|  |     ICON_RADIOACTIVE, | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_RADON, | ||||||
|  |     CONF_RADON_LONG_TERM, | ||||||
|  |     CONF_HUMIDITY, | ||||||
|  |     CONF_TVOC, | ||||||
|  |     CONF_CO2, | ||||||
|  |     CONF_PRESSURE, | ||||||
|  |     CONF_TEMPERATURE, | ||||||
|  |     UNIT_BECQUEREL_PER_CUBIC_METER, | ||||||
|  |     UNIT_PARTS_PER_MILLION, | ||||||
|  |     UNIT_PARTS_PER_BILLION, | ||||||
|  |     ICON_RADIATOR, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | DEPENDENCIES = ["ble_client"] | ||||||
|  |  | ||||||
|  | airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus") | ||||||
|  | AirthingsWavePlus = airthings_wave_plus_ns.class_( | ||||||
|  |     "AirthingsWavePlus", cg.PollingComponent, ble_client.BLEClientNode | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = ( | ||||||
|  |     cv.Schema( | ||||||
|  |         { | ||||||
|  |             cv.GenerateID(): cv.declare_id(AirthingsWavePlus), | ||||||
|  |             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_PERCENT, | ||||||
|  |                 device_class=DEVICE_CLASS_HUMIDITY, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_RADON): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, | ||||||
|  |                 icon=ICON_RADIOACTIVE, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, | ||||||
|  |                 icon=ICON_RADIOACTIVE, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_CELSIUS, | ||||||
|  |                 accuracy_decimals=2, | ||||||
|  |                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_PRESSURE): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_HECTOPASCAL, | ||||||
|  |                 accuracy_decimals=1, | ||||||
|  |                 device_class=DEVICE_CLASS_PRESSURE, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_CO2): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_PARTS_PER_MILLION, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_CARBON_DIOXIDE, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_TVOC): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_PARTS_PER_BILLION, | ||||||
|  |                 icon=ICON_RADIATOR, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |     .extend(cv.polling_component_schema("5mins")) | ||||||
|  |     .extend(ble_client.BLE_CLIENT_SCHEMA) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |  | ||||||
|  |     await ble_client.register_ble_node(var, config) | ||||||
|  |  | ||||||
|  |     if CONF_HUMIDITY in config: | ||||||
|  |         sens = await sensor.new_sensor(config[CONF_HUMIDITY]) | ||||||
|  |         cg.add(var.set_humidity(sens)) | ||||||
|  |     if CONF_RADON in config: | ||||||
|  |         sens = await sensor.new_sensor(config[CONF_RADON]) | ||||||
|  |         cg.add(var.set_radon(sens)) | ||||||
|  |     if CONF_RADON_LONG_TERM in config: | ||||||
|  |         sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM]) | ||||||
|  |         cg.add(var.set_radon_long_term(sens)) | ||||||
|  |     if CONF_TEMPERATURE in config: | ||||||
|  |         sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) | ||||||
|  |         cg.add(var.set_temperature(sens)) | ||||||
|  |     if CONF_PRESSURE in config: | ||||||
|  |         sens = await sensor.new_sensor(config[CONF_PRESSURE]) | ||||||
|  |         cg.add(var.set_pressure(sens)) | ||||||
|  |     if CONF_CO2 in config: | ||||||
|  |         sens = await sensor.new_sensor(config[CONF_CO2]) | ||||||
|  |         cg.add(var.set_co2(sens)) | ||||||
|  |     if CONF_TVOC in config: | ||||||
|  |         sens = await sensor.new_sensor(config[CONF_TVOC]) | ||||||
|  |         cg.add(var.set_tvoc(sens)) | ||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | import base64 | ||||||
|  |  | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome import automation | from esphome import automation | ||||||
| @@ -6,6 +8,7 @@ from esphome.const import ( | |||||||
|     CONF_DATA, |     CONF_DATA, | ||||||
|     CONF_DATA_TEMPLATE, |     CONF_DATA_TEMPLATE, | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
|  |     CONF_KEY, | ||||||
|     CONF_PASSWORD, |     CONF_PASSWORD, | ||||||
|     CONF_PORT, |     CONF_PORT, | ||||||
|     CONF_REBOOT_TIMEOUT, |     CONF_REBOOT_TIMEOUT, | ||||||
| @@ -19,7 +22,7 @@ from esphome.const import ( | |||||||
| from esphome.core import coroutine_with_priority | from esphome.core import coroutine_with_priority | ||||||
|  |  | ||||||
| DEPENDENCIES = ["network"] | DEPENDENCIES = ["network"] | ||||||
| AUTO_LOAD = ["async_tcp"] | AUTO_LOAD = ["socket"] | ||||||
| CODEOWNERS = ["@OttoWinter"] | CODEOWNERS = ["@OttoWinter"] | ||||||
|  |  | ||||||
| api_ns = cg.esphome_ns.namespace("api") | api_ns = cg.esphome_ns.namespace("api") | ||||||
| @@ -41,6 +44,22 @@ SERVICE_ARG_NATIVE_TYPES = { | |||||||
|     "float[]": cg.std_vector.template(float), |     "float[]": cg.std_vector.template(float), | ||||||
|     "string[]": cg.std_vector.template(cg.std_string), |     "string[]": cg.std_vector.template(cg.std_string), | ||||||
| } | } | ||||||
|  | CONF_ENCRYPTION = "encryption" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_encryption_key(value): | ||||||
|  |     value = cv.string_strict(value) | ||||||
|  |     try: | ||||||
|  |         decoded = base64.b64decode(value, validate=True) | ||||||
|  |     except ValueError as err: | ||||||
|  |         raise cv.Invalid("Invalid key format, please check it's using base64") from err | ||||||
|  |  | ||||||
|  |     if len(decoded) != 32: | ||||||
|  |         raise cv.Invalid("Encryption key must be base64 and 32 bytes long") | ||||||
|  |  | ||||||
|  |     # Return original data for roundtrip conversion | ||||||
|  |     return value | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.Schema( | CONFIG_SCHEMA = cv.Schema( | ||||||
|     { |     { | ||||||
| @@ -63,6 +82,11 @@ CONFIG_SCHEMA = cv.Schema( | |||||||
|                 ), |                 ), | ||||||
|             } |             } | ||||||
|         ), |         ), | ||||||
|  |         cv.Optional(CONF_ENCRYPTION): cv.Schema( | ||||||
|  |             { | ||||||
|  |                 cv.Required(CONF_KEY): validate_encryption_key, | ||||||
|  |             } | ||||||
|  |         ), | ||||||
|     } |     } | ||||||
| ).extend(cv.COMPONENT_SCHEMA) | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
| @@ -92,6 +116,15 @@ async def to_code(config): | |||||||
|         cg.add(var.register_user_service(trigger)) |         cg.add(var.register_user_service(trigger)) | ||||||
|         await automation.build_automation(trigger, func_args, conf) |         await automation.build_automation(trigger, func_args, conf) | ||||||
|  |  | ||||||
|  |     if CONF_ENCRYPTION in config: | ||||||
|  |         conf = config[CONF_ENCRYPTION] | ||||||
|  |         decoded = base64.b64decode(conf[CONF_KEY]) | ||||||
|  |         cg.add(var.set_noise_psk(list(decoded))) | ||||||
|  |         cg.add_define("USE_API_NOISE") | ||||||
|  |         cg.add_library("esphome/noise-c", "0.1.1") | ||||||
|  |     else: | ||||||
|  |         cg.add_define("USE_API_PLAINTEXT") | ||||||
|  |  | ||||||
|     cg.add_define("USE_API") |     cg.add_define("USE_API") | ||||||
|     cg.add_global(api_ns.using) |     cg.add_global(api_ns.using) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -473,7 +473,8 @@ message ListEntitiesSensorResponse { | |||||||
|   bool force_update = 8; |   bool force_update = 8; | ||||||
|   string device_class = 9; |   string device_class = 9; | ||||||
|   SensorStateClass state_class = 10; |   SensorStateClass state_class = 10; | ||||||
|   SensorLastResetType last_reset_type = 11; |   // Last reset type removed in 2021.9.0 | ||||||
|  |   SensorLastResetType legacy_last_reset_type = 11; | ||||||
|   bool disabled_by_default = 12; |   bool disabled_by_default = 12; | ||||||
| } | } | ||||||
| message SensorStateResponse { | message SensorStateResponse { | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #include "esphome/core/util.h" | #include "esphome/core/util.h" | ||||||
| #include "esphome/core/version.h" | #include "esphome/core/version.h" | ||||||
|  | #include <cerrno> | ||||||
|  |  | ||||||
| #ifdef USE_DEEP_SLEEP | #ifdef USE_DEEP_SLEEP | ||||||
| #include "esphome/components/deep_sleep/deep_sleep_component.h" | #include "esphome/components/deep_sleep/deep_sleep_component.h" | ||||||
| @@ -18,120 +19,94 @@ namespace api { | |||||||
|  |  | ||||||
| static const char *const TAG = "api.connection"; | static const char *const TAG = "api.connection"; | ||||||
|  |  | ||||||
| APIConnection::APIConnection(AsyncClient *client, APIServer *parent) | APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) | ||||||
|     : client_(client), parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { |     : parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { | ||||||
|   this->client_->onError([](void *s, AsyncClient *c, int8_t error) { ((APIConnection *) s)->on_error_(error); }, this); |   this->proto_write_buffer_.reserve(64); | ||||||
|   this->client_->onDisconnect([](void *s, AsyncClient *c) { ((APIConnection *) s)->on_disconnect_(); }, this); |  | ||||||
|   this->client_->onTimeout([](void *s, AsyncClient *c, uint32_t time) { ((APIConnection *) s)->on_timeout_(time); }, |  | ||||||
|                            this); |  | ||||||
|   this->client_->onData([](void *s, AsyncClient *c, void *buf, |  | ||||||
|                            size_t len) { ((APIConnection *) s)->on_data_(reinterpret_cast<uint8_t *>(buf), len); }, |  | ||||||
|                         this); |  | ||||||
|  |  | ||||||
|   this->send_buffer_.reserve(64); | #if defined(USE_API_PLAINTEXT) | ||||||
|   this->recv_buffer_.reserve(32); |   helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))}; | ||||||
|   this->client_info_ = this->client_->remoteIP().toString().c_str(); | #elif defined(USE_API_NOISE) | ||||||
|  |   helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; | ||||||
|  | #else | ||||||
|  | #error "No frame helper defined" | ||||||
|  | #endif | ||||||
|  | } | ||||||
|  | void APIConnection::start() { | ||||||
|   this->last_traffic_ = millis(); |   this->last_traffic_ = millis(); | ||||||
| } |  | ||||||
| APIConnection::~APIConnection() { delete this->client_; } |  | ||||||
| void APIConnection::on_error_(int8_t error) { this->remove_ = true; } |  | ||||||
| void APIConnection::on_disconnect_() { this->remove_ = true; } |  | ||||||
| void APIConnection::on_timeout_(uint32_t time) { this->on_fatal_error(); } |  | ||||||
| void APIConnection::on_data_(uint8_t *buf, size_t len) { |  | ||||||
|   if (len == 0 || buf == nullptr) |  | ||||||
|     return; |  | ||||||
|   this->recv_buffer_.insert(this->recv_buffer_.end(), buf, buf + len); |  | ||||||
| } |  | ||||||
| void APIConnection::parse_recv_buffer_() { |  | ||||||
|   if (this->recv_buffer_.empty() || this->remove_) |  | ||||||
|     return; |  | ||||||
|  |  | ||||||
|   while (!this->recv_buffer_.empty()) { |   APIError err = helper_->init(); | ||||||
|     if (this->recv_buffer_[0] != 0x00) { |   if (err != APIError::OK) { | ||||||
|       ESP_LOGW(TAG, "Invalid preamble from %s", this->client_info_.c_str()); |     on_fatal_error(); | ||||||
|       this->on_fatal_error(); |     ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|     uint32_t i = 1; |   client_info_ = helper_->getpeername(); | ||||||
|     const uint32_t size = this->recv_buffer_.size(); |   helper_->set_log_info(client_info_); | ||||||
|     uint32_t consumed; |  | ||||||
|     auto msg_size_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed); |  | ||||||
|     if (!msg_size_varint.has_value()) |  | ||||||
|       // not enough data there yet |  | ||||||
|       return; |  | ||||||
|     i += consumed; |  | ||||||
|     uint32_t msg_size = msg_size_varint->as_uint32(); |  | ||||||
|  |  | ||||||
|     auto msg_type_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed); |  | ||||||
|     if (!msg_type_varint.has_value()) |  | ||||||
|       // not enough data there yet |  | ||||||
|       return; |  | ||||||
|     i += consumed; |  | ||||||
|     uint32_t msg_type = msg_type_varint->as_uint32(); |  | ||||||
|  |  | ||||||
|     if (size - i < msg_size) |  | ||||||
|       // message body not fully received |  | ||||||
|       return; |  | ||||||
|  |  | ||||||
|     uint8_t *msg = &this->recv_buffer_[i]; |  | ||||||
|     this->read_message(msg_size, msg_type, msg); |  | ||||||
|     if (this->remove_) |  | ||||||
|       return; |  | ||||||
|     // pop front |  | ||||||
|     uint32_t total = i + msg_size; |  | ||||||
|     this->recv_buffer_.erase(this->recv_buffer_.begin(), this->recv_buffer_.begin() + total); |  | ||||||
|     this->last_traffic_ = millis(); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void APIConnection::disconnect_client() { |  | ||||||
|   this->client_->close(); |  | ||||||
|   this->remove_ = true; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| void APIConnection::loop() { | void APIConnection::loop() { | ||||||
|   if (this->remove_) |   if (this->remove_) | ||||||
|     return; |     return; | ||||||
|  |  | ||||||
|   if (this->next_close_) { |  | ||||||
|     this->disconnect_client(); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (!network_is_connected()) { |   if (!network_is_connected()) { | ||||||
|     // when network is disconnected force disconnect immediately |     // when network is disconnected force disconnect immediately | ||||||
|     // don't wait for timeout |     // don't wait for timeout | ||||||
|     this->on_fatal_error(); |     this->on_fatal_error(); | ||||||
|  |     ESP_LOGW(TAG, "%s: Network unavailable, disconnecting", client_info_.c_str()); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (this->client_->disconnected()) { |   if (this->next_close_) { | ||||||
|     // failsafe for disconnect logic |     // requested a disconnect | ||||||
|     this->on_disconnect_(); |     this->helper_->close(); | ||||||
|  |     this->remove_ = true; | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   APIError err = helper_->loop(); | ||||||
|  |   if (err != APIError::OK) { | ||||||
|  |     on_fatal_error(); | ||||||
|  |     ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   ReadPacketBuffer buffer; | ||||||
|  |   err = helper_->read_packet(&buffer); | ||||||
|  |   if (err == APIError::WOULD_BLOCK) { | ||||||
|  |     // pass | ||||||
|  |   } else if (err != APIError::OK) { | ||||||
|  |     on_fatal_error(); | ||||||
|  |     if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { | ||||||
|  |       ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str()); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); | ||||||
|  |     } | ||||||
|  |     return; | ||||||
|  |   } else { | ||||||
|  |     this->last_traffic_ = millis(); | ||||||
|  |     // read a packet | ||||||
|  |     this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); | ||||||
|  |     if (this->remove_) | ||||||
|       return; |       return; | ||||||
|   } |   } | ||||||
|   this->parse_recv_buffer_(); |  | ||||||
|  |  | ||||||
|   this->list_entities_iterator_.advance(); |   this->list_entities_iterator_.advance(); | ||||||
|   this->initial_state_iterator_.advance(); |   this->initial_state_iterator_.advance(); | ||||||
|  |  | ||||||
|   const uint32_t keepalive = 60000; |   const uint32_t keepalive = 60000; | ||||||
|  |   const uint32_t now = millis(); | ||||||
|   if (this->sent_ping_) { |   if (this->sent_ping_) { | ||||||
|     // Disconnect if not responded within 2.5*keepalive |     // Disconnect if not responded within 2.5*keepalive | ||||||
|     if (millis() - this->last_traffic_ > (keepalive * 5) / 2) { |     if (now - this->last_traffic_ > (keepalive * 5) / 2) { | ||||||
|       ESP_LOGW(TAG, "'%s' didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str()); |       on_fatal_error(); | ||||||
|       this->disconnect_client(); |       ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str()); | ||||||
|     } |     } | ||||||
|   } else if (millis() - this->last_traffic_ > keepalive) { |   } else if (now - this->last_traffic_ > keepalive) { | ||||||
|     this->sent_ping_ = true; |     this->sent_ping_ = true; | ||||||
|     this->send_ping_request(PingRequest()); |     this->send_ping_request(PingRequest()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| #ifdef USE_ESP32_CAMERA | #ifdef USE_ESP32_CAMERA | ||||||
|   if (this->image_reader_.available()) { |   if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) { | ||||||
|     uint32_t space = this->client_->space(); |     uint32_t to_send = std::min((size_t) 1024, this->image_reader_.available()); | ||||||
|     // reserve 15 bytes for metadata, and at least 64 bytes of data |  | ||||||
|     if (space >= 15 + 64) { |  | ||||||
|       uint32_t to_send = std::min(space - 15, this->image_reader_.available()); |  | ||||||
|     auto buffer = this->create_buffer(); |     auto buffer = this->create_buffer(); | ||||||
|     // fixed32 key = 1; |     // fixed32 key = 1; | ||||||
|     buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); |     buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); | ||||||
| @@ -149,14 +124,41 @@ void APIConnection::loop() { | |||||||
|       this->image_reader_.return_image(); |       this->image_reader_.return_image(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   } |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  |   if (state_subs_at_ != -1) { | ||||||
|  |     const auto &subs = this->parent_->get_state_subs(); | ||||||
|  |     if (state_subs_at_ >= subs.size()) { | ||||||
|  |       state_subs_at_ = -1; | ||||||
|  |     } else { | ||||||
|  |       auto &it = subs[state_subs_at_]; | ||||||
|  |       SubscribeHomeAssistantStateResponse resp; | ||||||
|  |       resp.entity_id = it.entity_id; | ||||||
|  |       resp.attribute = it.attribute.value(); | ||||||
|  |       if (this->send_subscribe_home_assistant_state_response(resp)) { | ||||||
|  |         state_subs_at_++; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| std::string get_default_unique_id(const std::string &component_type, Nameable *nameable) { | std::string get_default_unique_id(const std::string &component_type, Nameable *nameable) { | ||||||
|   return App.get_name() + component_type + nameable->get_object_id(); |   return App.get_name() + component_type + nameable->get_object_id(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) { | ||||||
|  |   // remote initiated disconnect_client | ||||||
|  |   // don't close yet, we still need to send the disconnect response | ||||||
|  |   // close will happen on next loop | ||||||
|  |   ESP_LOGD(TAG, "%s requested disconnected", client_info_.c_str()); | ||||||
|  |   this->next_close_ = true; | ||||||
|  |   DisconnectResponse resp; | ||||||
|  |   return resp; | ||||||
|  | } | ||||||
|  | void APIConnection::on_disconnect_response(const DisconnectResponse &value) { | ||||||
|  |   // pass | ||||||
|  | } | ||||||
|  |  | ||||||
| #ifdef USE_BINARY_SENSOR | #ifdef USE_BINARY_SENSOR | ||||||
| bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state) { | bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state) { | ||||||
|   if (!this->state_subscription_) |   if (!this->state_subscription_) | ||||||
| @@ -241,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; | ||||||
| @@ -295,6 +300,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { | |||||||
|     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 | ||||||
| @@ -417,7 +423,6 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { | |||||||
|   msg.force_update = sensor->get_force_update(); |   msg.force_update = sensor->get_force_update(); | ||||||
|   msg.device_class = sensor->get_device_class(); |   msg.device_class = sensor->get_device_class(); | ||||||
|   msg.state_class = static_cast<enums::SensorStateClass>(sensor->state_class); |   msg.state_class = static_cast<enums::SensorStateClass>(sensor->state_class); | ||||||
|   msg.last_reset_type = static_cast<enums::SensorLastResetType>(sensor->last_reset_type); |  | ||||||
|   msg.disabled_by_default = sensor->is_disabled_by_default(); |   msg.disabled_by_default = sensor->is_disabled_by_default(); | ||||||
|  |  | ||||||
|   return this->send_list_entities_sensor_response(msg); |   return this->send_list_entities_sensor_response(msg); | ||||||
| @@ -709,8 +714,8 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin | |||||||
| } | } | ||||||
|  |  | ||||||
| HelloResponse APIConnection::hello(const HelloRequest &msg) { | HelloResponse APIConnection::hello(const HelloRequest &msg) { | ||||||
|   this->client_info_ = msg.client_info + " (" + this->client_->remoteIP().toString().c_str(); |   this->client_info_ = msg.client_info + " (" + this->helper_->getpeername() + ")"; | ||||||
|   this->client_info_ += ")"; |   this->helper_->set_log_info(client_info_); | ||||||
|   ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str()); |   ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str()); | ||||||
|  |  | ||||||
|   HelloResponse resp; |   HelloResponse resp; | ||||||
| @@ -727,7 +732,7 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { | |||||||
|   // bool invalid_password = 1; |   // bool invalid_password = 1; | ||||||
|   resp.invalid_password = !correct; |   resp.invalid_password = !correct; | ||||||
|   if (correct) { |   if (correct) { | ||||||
|     ESP_LOGD(TAG, "Client '%s' connected successfully!", this->client_info_.c_str()); |     ESP_LOGD(TAG, "%s: Connected successfully", this->client_info_.c_str()); | ||||||
|     this->connection_state_ = ConnectionState::AUTHENTICATED; |     this->connection_state_ = ConnectionState::AUTHENTICATED; | ||||||
|  |  | ||||||
| #ifdef USE_HOMEASSISTANT_TIME | #ifdef USE_HOMEASSISTANT_TIME | ||||||
| @@ -745,9 +750,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { | |||||||
|   resp.mac_address = get_mac_address_pretty(); |   resp.mac_address = get_mac_address_pretty(); | ||||||
|   resp.esphome_version = ESPHOME_VERSION; |   resp.esphome_version = ESPHOME_VERSION; | ||||||
|   resp.compilation_time = App.get_compilation_time(); |   resp.compilation_time = App.get_compilation_time(); | ||||||
| #ifdef ARDUINO_BOARD |   resp.model = ESPHOME_BOARD; | ||||||
|   resp.model = ARDUINO_BOARD; |  | ||||||
| #endif |  | ||||||
| #ifdef USE_DEEP_SLEEP | #ifdef USE_DEEP_SLEEP | ||||||
|   resp.has_deep_sleep = deep_sleep::global_has_deep_sleep; |   resp.has_deep_sleep = deep_sleep::global_has_deep_sleep; | ||||||
| #endif | #endif | ||||||
| @@ -775,57 +778,39 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) { | void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) { | ||||||
|   for (auto &it : this->parent_->get_state_subs()) { |   state_subs_at_ = 0; | ||||||
|     SubscribeHomeAssistantStateResponse resp; |  | ||||||
|     resp.entity_id = it.entity_id; |  | ||||||
|     resp.attribute = it.attribute.value(); |  | ||||||
|     if (!this->send_subscribe_home_assistant_state_response(resp)) { |  | ||||||
|       this->on_fatal_error(); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) { | bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) { | ||||||
|   if (this->remove_) |   if (this->remove_) | ||||||
|     return false; |     return false; | ||||||
|  |   if (!this->helper_->can_write_without_blocking()) | ||||||
|  |     return false; | ||||||
|  |  | ||||||
|   std::vector<uint8_t> header; |   APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size()); | ||||||
|   header.push_back(0x00); |   if (err == APIError::WOULD_BLOCK) | ||||||
|   ProtoVarInt(buffer.get_buffer()->size()).encode(header); |     return false; | ||||||
|   ProtoVarInt(message_type).encode(header); |   if (err != APIError::OK) { | ||||||
|  |     on_fatal_error(); | ||||||
|   size_t needed_space = buffer.get_buffer()->size() + header.size(); |     if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { | ||||||
|  |       ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str()); | ||||||
|   if (needed_space > this->client_->space()) { |     } else { | ||||||
|     delay(0); |       ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); | ||||||
|     if (needed_space > this->client_->space()) { |  | ||||||
|       // SubscribeLogsResponse |  | ||||||
|       if (message_type != 29) { |  | ||||||
|         ESP_LOGV(TAG, "Cannot send message because of TCP buffer space"); |  | ||||||
|     } |     } | ||||||
|       delay(0); |  | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|   } |   this->last_traffic_ = millis(); | ||||||
|  |   return true; | ||||||
|   this->client_->add(reinterpret_cast<char *>(header.data()), header.size(), |  | ||||||
|                      ASYNC_WRITE_FLAG_COPY | ASYNC_WRITE_FLAG_MORE); |  | ||||||
|   this->client_->add(reinterpret_cast<char *>(buffer.get_buffer()->data()), buffer.get_buffer()->size(), |  | ||||||
|                      ASYNC_WRITE_FLAG_COPY); |  | ||||||
|   bool ret = this->client_->send(); |  | ||||||
|   return ret; |  | ||||||
| } | } | ||||||
| void APIConnection::on_unauthenticated_access() { | void APIConnection::on_unauthenticated_access() { | ||||||
|   ESP_LOGD(TAG, "'%s' tried to access without authentication.", this->client_info_.c_str()); |  | ||||||
|   this->on_fatal_error(); |   this->on_fatal_error(); | ||||||
|  |   ESP_LOGD(TAG, "%s: tried to access without authentication.", this->client_info_.c_str()); | ||||||
| } | } | ||||||
| void APIConnection::on_no_setup_connection() { | void APIConnection::on_no_setup_connection() { | ||||||
|   ESP_LOGD(TAG, "'%s' tried to access without full connection.", this->client_info_.c_str()); |  | ||||||
|   this->on_fatal_error(); |   this->on_fatal_error(); | ||||||
|  |   ESP_LOGD(TAG, "%s: tried to access without full connection.", this->client_info_.c_str()); | ||||||
| } | } | ||||||
| void APIConnection::on_fatal_error() { | void APIConnection::on_fatal_error() { | ||||||
|   ESP_LOGV(TAG, "Error: Disconnecting %s", this->client_info_.c_str()); |   this->helper_->close(); | ||||||
|   this->client_->close(); |  | ||||||
|   this->remove_ = true; |   this->remove_ = true; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,16 +5,17 @@ | |||||||
| #include "api_pb2.h" | #include "api_pb2.h" | ||||||
| #include "api_pb2_service.h" | #include "api_pb2_service.h" | ||||||
| #include "api_server.h" | #include "api_server.h" | ||||||
|  | #include "api_frame_helper.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace api { | namespace api { | ||||||
|  |  | ||||||
| class APIConnection : public APIServerConnection { | class APIConnection : public APIServerConnection { | ||||||
|  public: |  public: | ||||||
|   APIConnection(AsyncClient *client, APIServer *parent); |   APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent); | ||||||
|   virtual ~APIConnection(); |   virtual ~APIConnection() = default; | ||||||
|  |  | ||||||
|   void disconnect_client(); |   void start(); | ||||||
|   void loop(); |   void loop(); | ||||||
|  |  | ||||||
|   bool send_list_info_done() { |   bool send_list_info_done() { | ||||||
| @@ -86,10 +87,7 @@ class APIConnection : public APIServerConnection { | |||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   void on_disconnect_response(const DisconnectResponse &value) override { |   void on_disconnect_response(const DisconnectResponse &value) override; | ||||||
|     // we initiated disconnect_client |  | ||||||
|     this->next_close_ = true; |  | ||||||
|   } |  | ||||||
|   void on_ping_response(const PingResponse &value) override { |   void on_ping_response(const PingResponse &value) override { | ||||||
|     // we initiated ping |     // we initiated ping | ||||||
|     this->sent_ping_ = false; |     this->sent_ping_ = false; | ||||||
| @@ -100,12 +98,7 @@ class APIConnection : public APIServerConnection { | |||||||
| #endif | #endif | ||||||
|   HelloResponse hello(const HelloRequest &msg) override; |   HelloResponse hello(const HelloRequest &msg) override; | ||||||
|   ConnectResponse connect(const ConnectRequest &msg) override; |   ConnectResponse connect(const ConnectRequest &msg) override; | ||||||
|   DisconnectResponse disconnect(const DisconnectRequest &msg) override { |   DisconnectResponse disconnect(const DisconnectRequest &msg) override; | ||||||
|     // remote initiated disconnect_client |  | ||||||
|     this->next_close_ = true; |  | ||||||
|     DisconnectResponse resp; |  | ||||||
|     return resp; |  | ||||||
|   } |  | ||||||
|   PingResponse ping(const PingRequest &msg) override { return {}; } |   PingResponse ping(const PingRequest &msg) override { return {}; } | ||||||
|   DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override; |   DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override; | ||||||
|   void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } |   void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } | ||||||
| @@ -135,19 +128,16 @@ class APIConnection : public APIServerConnection { | |||||||
|   void on_unauthenticated_access() override; |   void on_unauthenticated_access() override; | ||||||
|   void on_no_setup_connection() override; |   void on_no_setup_connection() override; | ||||||
|   ProtoWriteBuffer create_buffer() override { |   ProtoWriteBuffer create_buffer() override { | ||||||
|     this->send_buffer_.clear(); |     // FIXME: ensure no recursive writes can happen | ||||||
|     return {&this->send_buffer_}; |     this->proto_write_buffer_.clear(); | ||||||
|  |     return {&this->proto_write_buffer_}; | ||||||
|   } |   } | ||||||
|   bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; |   bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   friend APIServer; |   friend APIServer; | ||||||
|  |  | ||||||
|   void on_error_(int8_t error); |   bool send_(const void *buf, size_t len, bool force); | ||||||
|   void on_disconnect_(); |  | ||||||
|   void on_timeout_(uint32_t time); |  | ||||||
|   void on_data_(uint8_t *buf, size_t len); |  | ||||||
|   void parse_recv_buffer_(); |  | ||||||
|  |  | ||||||
|   enum class ConnectionState { |   enum class ConnectionState { | ||||||
|     WAITING_FOR_HELLO, |     WAITING_FOR_HELLO, | ||||||
| @@ -157,8 +147,10 @@ class APIConnection : public APIServerConnection { | |||||||
|  |  | ||||||
|   bool remove_{false}; |   bool remove_{false}; | ||||||
|  |  | ||||||
|   std::vector<uint8_t> send_buffer_; |   // Buffer used to encode proto messages | ||||||
|   std::vector<uint8_t> recv_buffer_; |   // Re-use to prevent allocations | ||||||
|  |   std::vector<uint8_t> proto_write_buffer_; | ||||||
|  |   std::unique_ptr<APIFrameHelper> helper_; | ||||||
|  |  | ||||||
|   std::string client_info_; |   std::string client_info_; | ||||||
| #ifdef USE_ESP32_CAMERA | #ifdef USE_ESP32_CAMERA | ||||||
| @@ -170,12 +162,11 @@ class APIConnection : public APIServerConnection { | |||||||
|   uint32_t last_traffic_; |   uint32_t last_traffic_; | ||||||
|   bool sent_ping_{false}; |   bool sent_ping_{false}; | ||||||
|   bool service_call_subscription_{false}; |   bool service_call_subscription_{false}; | ||||||
|   bool current_nodelay_{false}; |   bool next_close_ = false; | ||||||
|   bool next_close_{false}; |  | ||||||
|   AsyncClient *client_; |  | ||||||
|   APIServer *parent_; |   APIServer *parent_; | ||||||
|   InitialStateIterator initial_state_iterator_; |   InitialStateIterator initial_state_iterator_; | ||||||
|   ListEntitiesIterator list_entities_iterator_; |   ListEntitiesIterator list_entities_iterator_; | ||||||
|  |   int state_subs_at_ = -1; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace api | }  // namespace api | ||||||
|   | |||||||
							
								
								
									
										961
									
								
								esphome/components/api/api_frame_helper.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										961
									
								
								esphome/components/api/api_frame_helper.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,961 @@ | |||||||
|  | #include "api_frame_helper.h" | ||||||
|  |  | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  | #include "proto.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace api { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "api.socket"; | ||||||
|  |  | ||||||
|  | /// Is the given return value (from read/write syscalls) a wouldblock error? | ||||||
|  | bool is_would_block(ssize_t ret) { | ||||||
|  |   if (ret == -1) { | ||||||
|  |     return errno == EWOULDBLOCK || errno == EAGAIN; | ||||||
|  |   } | ||||||
|  |   return ret == 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const char *api_error_to_str(APIError err) { | ||||||
|  |   // not using switch to ensure compiler doesn't try to build a big table out of it | ||||||
|  |   if (err == APIError::OK) { | ||||||
|  |     return "OK"; | ||||||
|  |   } else if (err == APIError::WOULD_BLOCK) { | ||||||
|  |     return "WOULD_BLOCK"; | ||||||
|  |   } else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) { | ||||||
|  |     return "BAD_HANDSHAKE_PACKET_LEN"; | ||||||
|  |   } else if (err == APIError::BAD_INDICATOR) { | ||||||
|  |     return "BAD_INDICATOR"; | ||||||
|  |   } else if (err == APIError::BAD_DATA_PACKET) { | ||||||
|  |     return "BAD_DATA_PACKET"; | ||||||
|  |   } else if (err == APIError::TCP_NODELAY_FAILED) { | ||||||
|  |     return "TCP_NODELAY_FAILED"; | ||||||
|  |   } else if (err == APIError::TCP_NONBLOCKING_FAILED) { | ||||||
|  |     return "TCP_NONBLOCKING_FAILED"; | ||||||
|  |   } else if (err == APIError::CLOSE_FAILED) { | ||||||
|  |     return "CLOSE_FAILED"; | ||||||
|  |   } else if (err == APIError::SHUTDOWN_FAILED) { | ||||||
|  |     return "SHUTDOWN_FAILED"; | ||||||
|  |   } else if (err == APIError::BAD_STATE) { | ||||||
|  |     return "BAD_STATE"; | ||||||
|  |   } else if (err == APIError::BAD_ARG) { | ||||||
|  |     return "BAD_ARG"; | ||||||
|  |   } else if (err == APIError::SOCKET_READ_FAILED) { | ||||||
|  |     return "SOCKET_READ_FAILED"; | ||||||
|  |   } else if (err == APIError::SOCKET_WRITE_FAILED) { | ||||||
|  |     return "SOCKET_WRITE_FAILED"; | ||||||
|  |   } else if (err == APIError::HANDSHAKESTATE_READ_FAILED) { | ||||||
|  |     return "HANDSHAKESTATE_READ_FAILED"; | ||||||
|  |   } else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) { | ||||||
|  |     return "HANDSHAKESTATE_WRITE_FAILED"; | ||||||
|  |   } else if (err == APIError::HANDSHAKESTATE_BAD_STATE) { | ||||||
|  |     return "HANDSHAKESTATE_BAD_STATE"; | ||||||
|  |   } else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) { | ||||||
|  |     return "CIPHERSTATE_DECRYPT_FAILED"; | ||||||
|  |   } else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) { | ||||||
|  |     return "CIPHERSTATE_ENCRYPT_FAILED"; | ||||||
|  |   } else if (err == APIError::OUT_OF_MEMORY) { | ||||||
|  |     return "OUT_OF_MEMORY"; | ||||||
|  |   } else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) { | ||||||
|  |     return "HANDSHAKESTATE_SETUP_FAILED"; | ||||||
|  |   } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) { | ||||||
|  |     return "HANDSHAKESTATE_SPLIT_FAILED"; | ||||||
|  |   } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) { | ||||||
|  |     return "BAD_HANDSHAKE_ERROR_BYTE"; | ||||||
|  |   } | ||||||
|  |   return "UNKNOWN"; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__) | ||||||
|  | // uncomment to log raw packets | ||||||
|  | //#define HELPER_LOG_PACKETS | ||||||
|  |  | ||||||
|  | #ifdef USE_API_NOISE | ||||||
|  | static const char *const PROLOGUE_INIT = "NoiseAPIInit"; | ||||||
|  |  | ||||||
|  | /// Convert a noise error code to a readable error | ||||||
|  | std::string noise_err_to_str(int err) { | ||||||
|  |   if (err == NOISE_ERROR_NO_MEMORY) | ||||||
|  |     return "NO_MEMORY"; | ||||||
|  |   if (err == NOISE_ERROR_UNKNOWN_ID) | ||||||
|  |     return "UNKNOWN_ID"; | ||||||
|  |   if (err == NOISE_ERROR_UNKNOWN_NAME) | ||||||
|  |     return "UNKNOWN_NAME"; | ||||||
|  |   if (err == NOISE_ERROR_MAC_FAILURE) | ||||||
|  |     return "MAC_FAILURE"; | ||||||
|  |   if (err == NOISE_ERROR_NOT_APPLICABLE) | ||||||
|  |     return "NOT_APPLICABLE"; | ||||||
|  |   if (err == NOISE_ERROR_SYSTEM) | ||||||
|  |     return "SYSTEM"; | ||||||
|  |   if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) | ||||||
|  |     return "REMOTE_KEY_REQUIRED"; | ||||||
|  |   if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED) | ||||||
|  |     return "LOCAL_KEY_REQUIRED"; | ||||||
|  |   if (err == NOISE_ERROR_PSK_REQUIRED) | ||||||
|  |     return "PSK_REQUIRED"; | ||||||
|  |   if (err == NOISE_ERROR_INVALID_LENGTH) | ||||||
|  |     return "INVALID_LENGTH"; | ||||||
|  |   if (err == NOISE_ERROR_INVALID_PARAM) | ||||||
|  |     return "INVALID_PARAM"; | ||||||
|  |   if (err == NOISE_ERROR_INVALID_STATE) | ||||||
|  |     return "INVALID_STATE"; | ||||||
|  |   if (err == NOISE_ERROR_INVALID_NONCE) | ||||||
|  |     return "INVALID_NONCE"; | ||||||
|  |   if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) | ||||||
|  |     return "INVALID_PRIVATE_KEY"; | ||||||
|  |   if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) | ||||||
|  |     return "INVALID_PUBLIC_KEY"; | ||||||
|  |   if (err == NOISE_ERROR_INVALID_FORMAT) | ||||||
|  |     return "INVALID_FORMAT"; | ||||||
|  |   if (err == NOISE_ERROR_INVALID_SIGNATURE) | ||||||
|  |     return "INVALID_SIGNATURE"; | ||||||
|  |   return to_string(err); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Initialize the frame helper, returns OK if successful. | ||||||
|  | APIError APINoiseFrameHelper::init() { | ||||||
|  |   if (state_ != State::INITIALIZE || socket_ == nullptr) { | ||||||
|  |     HELPER_LOG("Bad state for init %d", (int) state_); | ||||||
|  |     return APIError::BAD_STATE; | ||||||
|  |   } | ||||||
|  |   int err = socket_->setblocking(false); | ||||||
|  |   if (err != 0) { | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("Setting nonblocking failed with errno %d", errno); | ||||||
|  |     return APIError::TCP_NONBLOCKING_FAILED; | ||||||
|  |   } | ||||||
|  |   int enable = 1; | ||||||
|  |   err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); | ||||||
|  |   if (err != 0) { | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("Setting nodelay failed with errno %d", errno); | ||||||
|  |     return APIError::TCP_NODELAY_FAILED; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // init prologue | ||||||
|  |   prologue_.insert(prologue_.end(), PROLOGUE_INIT, PROLOGUE_INIT + strlen(PROLOGUE_INIT)); | ||||||
|  |  | ||||||
|  |   state_ = State::CLIENT_HELLO; | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  | /// Run through handshake messages (if in that phase) | ||||||
|  | APIError APINoiseFrameHelper::loop() { | ||||||
|  |   APIError err = state_action_(); | ||||||
|  |   if (err == APIError::WOULD_BLOCK) | ||||||
|  |     return APIError::OK; | ||||||
|  |   if (err != APIError::OK) | ||||||
|  |     return err; | ||||||
|  |   if (!tx_buf_.empty()) { | ||||||
|  |     err = try_send_tx_buf_(); | ||||||
|  |     if (err != APIError::OK) { | ||||||
|  |       return err; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter | ||||||
|  |  * | ||||||
|  |  * @param frame: The struct to hold the frame information in. | ||||||
|  |  *   msg_start: points to the start of the payload - this pointer is only valid until the next | ||||||
|  |  *     try_receive_raw_ call | ||||||
|  |  * | ||||||
|  |  * @return 0 if a full packet is in rx_buf_ | ||||||
|  |  * @return -1 if error, check errno. | ||||||
|  |  * | ||||||
|  |  * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later. | ||||||
|  |  * errno ENOMEM: Not enough memory for reading packet. | ||||||
|  |  * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. | ||||||
|  |  * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. | ||||||
|  |  */ | ||||||
|  | APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { | ||||||
|  |   int err; | ||||||
|  |   APIError aerr; | ||||||
|  |  | ||||||
|  |   if (frame == nullptr) { | ||||||
|  |     HELPER_LOG("Bad argument for try_read_frame_"); | ||||||
|  |     return APIError::BAD_ARG; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // read header | ||||||
|  |   if (rx_header_buf_len_ < 3) { | ||||||
|  |     // no header information yet | ||||||
|  |     size_t to_read = 3 - rx_header_buf_len_; | ||||||
|  |     ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); | ||||||
|  |     if (is_would_block(received)) { | ||||||
|  |       return APIError::WOULD_BLOCK; | ||||||
|  |     } else if (received == -1) { | ||||||
|  |       state_ = State::FAILED; | ||||||
|  |       HELPER_LOG("Socket read failed with errno %d", errno); | ||||||
|  |       return APIError::SOCKET_READ_FAILED; | ||||||
|  |     } | ||||||
|  |     rx_header_buf_len_ += received; | ||||||
|  |     if (received != to_read) { | ||||||
|  |       // not a full read | ||||||
|  |       return APIError::WOULD_BLOCK; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // header reading done | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // read body | ||||||
|  |   uint8_t indicator = rx_header_buf_[0]; | ||||||
|  |   if (indicator != 0x01) { | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("Bad indicator byte %u", indicator); | ||||||
|  |     return APIError::BAD_INDICATOR; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; | ||||||
|  |  | ||||||
|  |   if (state_ != State::DATA && msg_size > 128) { | ||||||
|  |     // for handshake message only permit up to 128 bytes | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("Bad packet len for handshake: %d", msg_size); | ||||||
|  |     return APIError::BAD_HANDSHAKE_PACKET_LEN; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // reserve space for body | ||||||
|  |   if (rx_buf_.size() != msg_size) { | ||||||
|  |     rx_buf_.resize(msg_size); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (rx_buf_len_ < msg_size) { | ||||||
|  |     // more data to read | ||||||
|  |     size_t to_read = msg_size - rx_buf_len_; | ||||||
|  |     ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); | ||||||
|  |     if (is_would_block(received)) { | ||||||
|  |       return APIError::WOULD_BLOCK; | ||||||
|  |     } else if (received == -1) { | ||||||
|  |       state_ = State::FAILED; | ||||||
|  |       HELPER_LOG("Socket read failed with errno %d", errno); | ||||||
|  |       return APIError::SOCKET_READ_FAILED; | ||||||
|  |     } | ||||||
|  |     rx_buf_len_ += received; | ||||||
|  |     if (received != to_read) { | ||||||
|  |       // not all read | ||||||
|  |       return APIError::WOULD_BLOCK; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // uncomment for even more debugging | ||||||
|  | #ifdef HELPER_LOG_PACKETS | ||||||
|  |   ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); | ||||||
|  | #endif | ||||||
|  |   frame->msg = std::move(rx_buf_); | ||||||
|  |   // consume msg | ||||||
|  |   rx_buf_ = {}; | ||||||
|  |   rx_buf_len_ = 0; | ||||||
|  |   rx_header_buf_len_ = 0; | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** To be called from read/write methods. | ||||||
|  |  * | ||||||
|  |  * This method runs through the internal handshake methods, if in that state. | ||||||
|  |  * | ||||||
|  |  * If the handshake is still active when this method returns and a read/write can't take place at | ||||||
|  |  * the moment, returns WOULD_BLOCK. | ||||||
|  |  * If an error occured, returns that error. Only returns OK if the transport is ready for data | ||||||
|  |  * traffic. | ||||||
|  |  */ | ||||||
|  | APIError APINoiseFrameHelper::state_action_() { | ||||||
|  |   int err; | ||||||
|  |   APIError aerr; | ||||||
|  |   if (state_ == State::INITIALIZE) { | ||||||
|  |     HELPER_LOG("Bad state for method: %d", (int) state_); | ||||||
|  |     return APIError::BAD_STATE; | ||||||
|  |   } | ||||||
|  |   if (state_ == State::CLIENT_HELLO) { | ||||||
|  |     // waiting for client hello | ||||||
|  |     ParsedFrame frame; | ||||||
|  |     aerr = try_read_frame_(&frame); | ||||||
|  |     if (aerr == APIError::BAD_INDICATOR) { | ||||||
|  |       send_explicit_handshake_reject_("Bad indicator byte"); | ||||||
|  |       return aerr; | ||||||
|  |     } | ||||||
|  |     if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { | ||||||
|  |       send_explicit_handshake_reject_("Bad handshake packet len"); | ||||||
|  |       return aerr; | ||||||
|  |     } | ||||||
|  |     if (aerr != APIError::OK) | ||||||
|  |       return aerr; | ||||||
|  |     // ignore contents, may be used in future for flags | ||||||
|  |     prologue_.push_back((uint8_t)(frame.msg.size() >> 8)); | ||||||
|  |     prologue_.push_back((uint8_t) frame.msg.size()); | ||||||
|  |     prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end()); | ||||||
|  |  | ||||||
|  |     state_ = State::SERVER_HELLO; | ||||||
|  |   } | ||||||
|  |   if (state_ == State::SERVER_HELLO) { | ||||||
|  |     // send server hello | ||||||
|  |     uint8_t msg[1]; | ||||||
|  |     msg[0] = 0x01;  // chosen proto | ||||||
|  |     aerr = write_frame_(msg, 1); | ||||||
|  |     if (aerr != APIError::OK) | ||||||
|  |       return aerr; | ||||||
|  |  | ||||||
|  |     // start handshake | ||||||
|  |     aerr = init_handshake_(); | ||||||
|  |     if (aerr != APIError::OK) | ||||||
|  |       return aerr; | ||||||
|  |  | ||||||
|  |     state_ = State::HANDSHAKE; | ||||||
|  |   } | ||||||
|  |   if (state_ == State::HANDSHAKE) { | ||||||
|  |     int action = noise_handshakestate_get_action(handshake_); | ||||||
|  |     if (action == NOISE_ACTION_READ_MESSAGE) { | ||||||
|  |       // waiting for handshake msg | ||||||
|  |       ParsedFrame frame; | ||||||
|  |       aerr = try_read_frame_(&frame); | ||||||
|  |       if (aerr == APIError::BAD_INDICATOR) { | ||||||
|  |         send_explicit_handshake_reject_("Bad indicator byte"); | ||||||
|  |         return aerr; | ||||||
|  |       } | ||||||
|  |       if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { | ||||||
|  |         send_explicit_handshake_reject_("Bad handshake packet len"); | ||||||
|  |         return aerr; | ||||||
|  |       } | ||||||
|  |       if (aerr != APIError::OK) | ||||||
|  |         return aerr; | ||||||
|  |  | ||||||
|  |       if (frame.msg.empty()) { | ||||||
|  |         send_explicit_handshake_reject_("Empty handshake message"); | ||||||
|  |         return APIError::BAD_HANDSHAKE_ERROR_BYTE; | ||||||
|  |       } else if (frame.msg[0] != 0x00) { | ||||||
|  |         HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]); | ||||||
|  |         send_explicit_handshake_reject_("Bad handshake error byte"); | ||||||
|  |         return APIError::BAD_HANDSHAKE_ERROR_BYTE; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       NoiseBuffer mbuf; | ||||||
|  |       noise_buffer_init(mbuf); | ||||||
|  |       noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1); | ||||||
|  |       err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); | ||||||
|  |       if (err != 0) { | ||||||
|  |         state_ = State::FAILED; | ||||||
|  |         HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str()); | ||||||
|  |         if (err == NOISE_ERROR_MAC_FAILURE) { | ||||||
|  |           send_explicit_handshake_reject_("Handshake MAC failure"); | ||||||
|  |         } else { | ||||||
|  |           send_explicit_handshake_reject_("Handshake error"); | ||||||
|  |         } | ||||||
|  |         return APIError::HANDSHAKESTATE_READ_FAILED; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       aerr = check_handshake_finished_(); | ||||||
|  |       if (aerr != APIError::OK) | ||||||
|  |         return aerr; | ||||||
|  |     } else if (action == NOISE_ACTION_WRITE_MESSAGE) { | ||||||
|  |       uint8_t buffer[65]; | ||||||
|  |       NoiseBuffer mbuf; | ||||||
|  |       noise_buffer_init(mbuf); | ||||||
|  |       noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); | ||||||
|  |  | ||||||
|  |       err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); | ||||||
|  |       if (err != 0) { | ||||||
|  |         state_ = State::FAILED; | ||||||
|  |         HELPER_LOG("noise_handshakestate_write_message failed: %s", noise_err_to_str(err).c_str()); | ||||||
|  |         return APIError::HANDSHAKESTATE_WRITE_FAILED; | ||||||
|  |       } | ||||||
|  |       buffer[0] = 0x00;  // success | ||||||
|  |  | ||||||
|  |       aerr = write_frame_(buffer, mbuf.size + 1); | ||||||
|  |       if (aerr != APIError::OK) | ||||||
|  |         return aerr; | ||||||
|  |       aerr = check_handshake_finished_(); | ||||||
|  |       if (aerr != APIError::OK) | ||||||
|  |         return aerr; | ||||||
|  |     } else { | ||||||
|  |       // bad state for action | ||||||
|  |       state_ = State::FAILED; | ||||||
|  |       HELPER_LOG("Bad action for handshake: %d", action); | ||||||
|  |       return APIError::HANDSHAKESTATE_BAD_STATE; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (state_ == State::CLOSED || state_ == State::FAILED) { | ||||||
|  |     return APIError::BAD_STATE; | ||||||
|  |   } | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  | void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { | ||||||
|  |   std::vector<uint8_t> data; | ||||||
|  |   data.resize(reason.length() + 1); | ||||||
|  |   data[0] = 0x01;  // failure | ||||||
|  |   for (size_t i = 0; i < reason.length(); i++) { | ||||||
|  |     data[i + 1] = (uint8_t) reason[i]; | ||||||
|  |   } | ||||||
|  |   // temporarily remove failed state | ||||||
|  |   auto orig_state = state_; | ||||||
|  |   state_ = State::EXPLICIT_REJECT; | ||||||
|  |   write_frame_(data.data(), data.size()); | ||||||
|  |   state_ = orig_state; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { | ||||||
|  |   int err; | ||||||
|  |   APIError aerr; | ||||||
|  |   aerr = state_action_(); | ||||||
|  |   if (aerr != APIError::OK) { | ||||||
|  |     return aerr; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (state_ != State::DATA) { | ||||||
|  |     return APIError::WOULD_BLOCK; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ParsedFrame frame; | ||||||
|  |   aerr = try_read_frame_(&frame); | ||||||
|  |   if (aerr != APIError::OK) | ||||||
|  |     return aerr; | ||||||
|  |  | ||||||
|  |   NoiseBuffer mbuf; | ||||||
|  |   noise_buffer_init(mbuf); | ||||||
|  |   noise_buffer_set_inout(mbuf, frame.msg.data(), frame.msg.size(), frame.msg.size()); | ||||||
|  |   err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); | ||||||
|  |   if (err != 0) { | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("noise_cipherstate_decrypt failed: %s", noise_err_to_str(err).c_str()); | ||||||
|  |     return APIError::CIPHERSTATE_DECRYPT_FAILED; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   size_t msg_size = mbuf.size; | ||||||
|  |   uint8_t *msg_data = frame.msg.data(); | ||||||
|  |   if (msg_size < 4) { | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("Bad data packet: size %d too short", msg_size); | ||||||
|  |     return APIError::BAD_DATA_PACKET; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // uint16_t type; | ||||||
|  |   // uint16_t data_len; | ||||||
|  |   // uint8_t *data; | ||||||
|  |   // uint8_t *padding;  zero or more bytes to fill up the rest of the packet | ||||||
|  |   uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; | ||||||
|  |   uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; | ||||||
|  |   if (data_len > msg_size - 4) { | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size); | ||||||
|  |     return APIError::BAD_DATA_PACKET; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   buffer->container = std::move(frame.msg); | ||||||
|  |   buffer->data_offset = 4; | ||||||
|  |   buffer->data_len = data_len; | ||||||
|  |   buffer->type = type; | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  | bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } | ||||||
|  | APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { | ||||||
|  |   int err; | ||||||
|  |   APIError aerr; | ||||||
|  |   aerr = state_action_(); | ||||||
|  |   if (aerr != APIError::OK) { | ||||||
|  |     return aerr; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (state_ != State::DATA) { | ||||||
|  |     return APIError::WOULD_BLOCK; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   size_t padding = 0; | ||||||
|  |   size_t msg_len = 4 + payload_len + padding; | ||||||
|  |   size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_); | ||||||
|  |   auto tmpbuf = std::unique_ptr<uint8_t[]>{new (std::nothrow) uint8_t[frame_len]}; | ||||||
|  |   if (tmpbuf == nullptr) { | ||||||
|  |     HELPER_LOG("Could not allocate for writing packet"); | ||||||
|  |     return APIError::OUT_OF_MEMORY; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   tmpbuf[0] = 0x01;  // indicator | ||||||
|  |   // tmpbuf[1], tmpbuf[2] to be set later | ||||||
|  |   const uint8_t msg_offset = 3; | ||||||
|  |   const uint8_t payload_offset = msg_offset + 4; | ||||||
|  |   tmpbuf[msg_offset + 0] = (uint8_t)(type >> 8);  // type | ||||||
|  |   tmpbuf[msg_offset + 1] = (uint8_t) type; | ||||||
|  |   tmpbuf[msg_offset + 2] = (uint8_t)(payload_len >> 8);  // data_len | ||||||
|  |   tmpbuf[msg_offset + 3] = (uint8_t) payload_len; | ||||||
|  |   // copy data | ||||||
|  |   std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]); | ||||||
|  |   // fill padding with zeros | ||||||
|  |   std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0); | ||||||
|  |  | ||||||
|  |   NoiseBuffer mbuf; | ||||||
|  |   noise_buffer_init(mbuf); | ||||||
|  |   noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset); | ||||||
|  |   err = noise_cipherstate_encrypt(send_cipher_, &mbuf); | ||||||
|  |   if (err != 0) { | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("noise_cipherstate_encrypt failed: %s", noise_err_to_str(err).c_str()); | ||||||
|  |     return APIError::CIPHERSTATE_ENCRYPT_FAILED; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   size_t total_len = 3 + mbuf.size; | ||||||
|  |   tmpbuf[1] = (uint8_t)(mbuf.size >> 8); | ||||||
|  |   tmpbuf[2] = (uint8_t) mbuf.size; | ||||||
|  |   // write raw to not have two packets sent if NAGLE disabled | ||||||
|  |   aerr = write_raw_(&tmpbuf[0], total_len); | ||||||
|  |   if (aerr != APIError::OK) { | ||||||
|  |     return aerr; | ||||||
|  |   } | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  | APIError APINoiseFrameHelper::try_send_tx_buf_() { | ||||||
|  |   // try send from tx_buf | ||||||
|  |   while (state_ != State::CLOSED && !tx_buf_.empty()) { | ||||||
|  |     ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size()); | ||||||
|  |     if (sent == -1) { | ||||||
|  |       if (errno == EWOULDBLOCK || errno == EAGAIN) | ||||||
|  |         break; | ||||||
|  |       state_ = State::FAILED; | ||||||
|  |       HELPER_LOG("Socket write failed with errno %d", errno); | ||||||
|  |       return APIError::SOCKET_WRITE_FAILED; | ||||||
|  |     } else if (sent == 0) { | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     // TODO: inefficient if multiple packets in txbuf | ||||||
|  |     // replace with deque of buffers | ||||||
|  |     tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  | /** Write the data to the socket, or buffer it a write would block | ||||||
|  |  * | ||||||
|  |  * @param data The data to write | ||||||
|  |  * @param len The length of data | ||||||
|  |  */ | ||||||
|  | APIError APINoiseFrameHelper::write_raw_(const uint8_t *data, size_t len) { | ||||||
|  |   if (len == 0) | ||||||
|  |     return APIError::OK; | ||||||
|  |   int err; | ||||||
|  |   APIError aerr; | ||||||
|  |  | ||||||
|  |   // uncomment for even more debugging | ||||||
|  | #ifdef HELPER_LOG_PACKETS | ||||||
|  |   ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str()); | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |   if (!tx_buf_.empty()) { | ||||||
|  |     // try to empty tx_buf_ first | ||||||
|  |     aerr = try_send_tx_buf_(); | ||||||
|  |     if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK) | ||||||
|  |       return aerr; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!tx_buf_.empty()) { | ||||||
|  |     // tx buf not empty, can't write now because then stream would be inconsistent | ||||||
|  |     tx_buf_.insert(tx_buf_.end(), data, data + len); | ||||||
|  |     return APIError::OK; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ssize_t sent = socket_->write(data, len); | ||||||
|  |   if (is_would_block(sent)) { | ||||||
|  |     // operation would block, add buffer to tx_buf | ||||||
|  |     tx_buf_.insert(tx_buf_.end(), data, data + len); | ||||||
|  |     return APIError::OK; | ||||||
|  |   } else if (sent == -1) { | ||||||
|  |     // an error occured | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("Socket write failed with errno %d", errno); | ||||||
|  |     return APIError::SOCKET_WRITE_FAILED; | ||||||
|  |   } else if (sent != len) { | ||||||
|  |     // partially sent, add end to tx_buf | ||||||
|  |     tx_buf_.insert(tx_buf_.end(), data + sent, data + len); | ||||||
|  |     return APIError::OK; | ||||||
|  |   } | ||||||
|  |   // fully sent | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  | APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) { | ||||||
|  |   APIError aerr; | ||||||
|  |  | ||||||
|  |   uint8_t header[3]; | ||||||
|  |   header[0] = 0x01;  // indicator | ||||||
|  |   header[1] = (uint8_t)(len >> 8); | ||||||
|  |   header[2] = (uint8_t) len; | ||||||
|  |  | ||||||
|  |   aerr = write_raw_(header, 3); | ||||||
|  |   if (aerr != APIError::OK) | ||||||
|  |     return aerr; | ||||||
|  |   aerr = write_raw_(data, len); | ||||||
|  |   return aerr; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Initiate the data structures for the handshake. | ||||||
|  |  * | ||||||
|  |  * @return 0 on success, -1 on error (check errno) | ||||||
|  |  */ | ||||||
|  | APIError APINoiseFrameHelper::init_handshake_() { | ||||||
|  |   int err; | ||||||
|  |   memset(&nid_, 0, sizeof(nid_)); | ||||||
|  |   // const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; | ||||||
|  |   // err = noise_protocol_name_to_id(&nid_, proto, strlen(proto)); | ||||||
|  |   nid_.pattern_id = NOISE_PATTERN_NN; | ||||||
|  |   nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY; | ||||||
|  |   nid_.dh_id = NOISE_DH_CURVE25519; | ||||||
|  |   nid_.prefix_id = NOISE_PREFIX_STANDARD; | ||||||
|  |   nid_.hybrid_id = NOISE_DH_NONE; | ||||||
|  |   nid_.hash_id = NOISE_HASH_SHA256; | ||||||
|  |   nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; | ||||||
|  |  | ||||||
|  |   err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER); | ||||||
|  |   if (err != 0) { | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("noise_handshakestate_new_by_id failed: %s", noise_err_to_str(err).c_str()); | ||||||
|  |     return APIError::HANDSHAKESTATE_SETUP_FAILED; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const auto &psk = ctx_->get_psk(); | ||||||
|  |   err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); | ||||||
|  |   if (err != 0) { | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("noise_handshakestate_set_pre_shared_key failed: %s", noise_err_to_str(err).c_str()); | ||||||
|  |     return APIError::HANDSHAKESTATE_SETUP_FAILED; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); | ||||||
|  |   if (err != 0) { | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("noise_handshakestate_set_prologue failed: %s", noise_err_to_str(err).c_str()); | ||||||
|  |     return APIError::HANDSHAKESTATE_SETUP_FAILED; | ||||||
|  |   } | ||||||
|  |   // set_prologue copies it into handshakestate, so we can get rid of it now | ||||||
|  |   prologue_ = {}; | ||||||
|  |  | ||||||
|  |   err = noise_handshakestate_start(handshake_); | ||||||
|  |   if (err != 0) { | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("noise_handshakestate_start failed: %s", noise_err_to_str(err).c_str()); | ||||||
|  |     return APIError::HANDSHAKESTATE_SETUP_FAILED; | ||||||
|  |   } | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | APIError APINoiseFrameHelper::check_handshake_finished_() { | ||||||
|  |   assert(state_ == State::HANDSHAKE); | ||||||
|  |  | ||||||
|  |   int action = noise_handshakestate_get_action(handshake_); | ||||||
|  |   if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE) | ||||||
|  |     return APIError::OK; | ||||||
|  |   if (action != NOISE_ACTION_SPLIT) { | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("Bad action for handshake: %d", action); | ||||||
|  |     return APIError::HANDSHAKESTATE_BAD_STATE; | ||||||
|  |   } | ||||||
|  |   int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); | ||||||
|  |   if (err != 0) { | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("noise_handshakestate_split failed: %s", noise_err_to_str(err).c_str()); | ||||||
|  |     return APIError::HANDSHAKESTATE_SPLIT_FAILED; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   HELPER_LOG("Handshake complete!"); | ||||||
|  |   noise_handshakestate_free(handshake_); | ||||||
|  |   handshake_ = nullptr; | ||||||
|  |   state_ = State::DATA; | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | APINoiseFrameHelper::~APINoiseFrameHelper() { | ||||||
|  |   if (handshake_ != nullptr) { | ||||||
|  |     noise_handshakestate_free(handshake_); | ||||||
|  |     handshake_ = nullptr; | ||||||
|  |   } | ||||||
|  |   if (send_cipher_ != nullptr) { | ||||||
|  |     noise_cipherstate_free(send_cipher_); | ||||||
|  |     send_cipher_ = nullptr; | ||||||
|  |   } | ||||||
|  |   if (recv_cipher_ != nullptr) { | ||||||
|  |     noise_cipherstate_free(recv_cipher_); | ||||||
|  |     recv_cipher_ = nullptr; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | APIError APINoiseFrameHelper::close() { | ||||||
|  |   state_ = State::CLOSED; | ||||||
|  |   int err = socket_->close(); | ||||||
|  |   if (err == -1) | ||||||
|  |     return APIError::CLOSE_FAILED; | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  | APIError APINoiseFrameHelper::shutdown(int how) { | ||||||
|  |   int err = socket_->shutdown(how); | ||||||
|  |   if (err == -1) | ||||||
|  |     return APIError::SHUTDOWN_FAILED; | ||||||
|  |   if (how == SHUT_RDWR) { | ||||||
|  |     state_ = State::CLOSED; | ||||||
|  |   } | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  | extern "C" { | ||||||
|  | // declare how noise generates random bytes (here with a good HWRNG based on the RF system) | ||||||
|  | void noise_rand_bytes(void *output, size_t len) { esphome::fill_random(reinterpret_cast<uint8_t *>(output), len); } | ||||||
|  | } | ||||||
|  | #endif  // USE_API_NOISE | ||||||
|  |  | ||||||
|  | #ifdef USE_API_PLAINTEXT | ||||||
|  |  | ||||||
|  | /// Initialize the frame helper, returns OK if successful. | ||||||
|  | APIError APIPlaintextFrameHelper::init() { | ||||||
|  |   if (state_ != State::INITIALIZE || socket_ == nullptr) { | ||||||
|  |     HELPER_LOG("Bad state for init %d", (int) state_); | ||||||
|  |     return APIError::BAD_STATE; | ||||||
|  |   } | ||||||
|  |   int err = socket_->setblocking(false); | ||||||
|  |   if (err != 0) { | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("Setting nonblocking failed with errno %d", errno); | ||||||
|  |     return APIError::TCP_NONBLOCKING_FAILED; | ||||||
|  |   } | ||||||
|  |   int enable = 1; | ||||||
|  |   err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); | ||||||
|  |   if (err != 0) { | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("Setting nodelay failed with errno %d", errno); | ||||||
|  |     return APIError::TCP_NODELAY_FAILED; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   state_ = State::DATA; | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  | /// Not used for plaintext | ||||||
|  | APIError APIPlaintextFrameHelper::loop() { | ||||||
|  |   if (state_ != State::DATA) { | ||||||
|  |     return APIError::BAD_STATE; | ||||||
|  |   } | ||||||
|  |   // try send pending TX data | ||||||
|  |   if (!tx_buf_.empty()) { | ||||||
|  |     APIError err = try_send_tx_buf_(); | ||||||
|  |     if (err != APIError::OK) { | ||||||
|  |       return err; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter | ||||||
|  |  * | ||||||
|  |  * @param frame: The struct to hold the frame information in. | ||||||
|  |  *   msg: store the parsed frame in that struct | ||||||
|  |  * | ||||||
|  |  * @return See APIError | ||||||
|  |  * | ||||||
|  |  * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. | ||||||
|  |  */ | ||||||
|  | APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { | ||||||
|  |   int err; | ||||||
|  |   APIError aerr; | ||||||
|  |  | ||||||
|  |   if (frame == nullptr) { | ||||||
|  |     HELPER_LOG("Bad argument for try_read_frame_"); | ||||||
|  |     return APIError::BAD_ARG; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // read header | ||||||
|  |   while (!rx_header_parsed_) { | ||||||
|  |     uint8_t data; | ||||||
|  |     ssize_t received = socket_->read(&data, 1); | ||||||
|  |     if (is_would_block(received)) { | ||||||
|  |       return APIError::WOULD_BLOCK; | ||||||
|  |     } else if (received == -1) { | ||||||
|  |       state_ = State::FAILED; | ||||||
|  |       HELPER_LOG("Socket read failed with errno %d", errno); | ||||||
|  |       return APIError::SOCKET_READ_FAILED; | ||||||
|  |     } | ||||||
|  |     rx_header_buf_.push_back(data); | ||||||
|  |  | ||||||
|  |     // try parse header | ||||||
|  |     if (rx_header_buf_[0] != 0x00) { | ||||||
|  |       state_ = State::FAILED; | ||||||
|  |       HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); | ||||||
|  |       return APIError::BAD_INDICATOR; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     size_t i = 1; | ||||||
|  |     uint32_t consumed = 0; | ||||||
|  |     auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); | ||||||
|  |     if (!msg_size_varint.has_value()) { | ||||||
|  |       // not enough data there yet | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     i += consumed; | ||||||
|  |     rx_header_parsed_len_ = msg_size_varint->as_uint32(); | ||||||
|  |  | ||||||
|  |     auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); | ||||||
|  |     if (!msg_type_varint.has_value()) { | ||||||
|  |       // not enough data there yet | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |     rx_header_parsed_type_ = msg_type_varint->as_uint32(); | ||||||
|  |     rx_header_parsed_ = true; | ||||||
|  |   } | ||||||
|  |   // header reading done | ||||||
|  |  | ||||||
|  |   // reserve space for body | ||||||
|  |   if (rx_buf_.size() != rx_header_parsed_len_) { | ||||||
|  |     rx_buf_.resize(rx_header_parsed_len_); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (rx_buf_len_ < rx_header_parsed_len_) { | ||||||
|  |     // more data to read | ||||||
|  |     size_t to_read = rx_header_parsed_len_ - rx_buf_len_; | ||||||
|  |     ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); | ||||||
|  |     if (is_would_block(received)) { | ||||||
|  |       return APIError::WOULD_BLOCK; | ||||||
|  |     } else if (received == -1) { | ||||||
|  |       state_ = State::FAILED; | ||||||
|  |       HELPER_LOG("Socket read failed with errno %d", errno); | ||||||
|  |       return APIError::SOCKET_READ_FAILED; | ||||||
|  |     } | ||||||
|  |     rx_buf_len_ += received; | ||||||
|  |     if (received != to_read) { | ||||||
|  |       // not all read | ||||||
|  |       return APIError::WOULD_BLOCK; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // uncomment for even more debugging | ||||||
|  | #ifdef HELPER_LOG_PACKETS | ||||||
|  |   ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); | ||||||
|  | #endif | ||||||
|  |   frame->msg = std::move(rx_buf_); | ||||||
|  |   // consume msg | ||||||
|  |   rx_buf_ = {}; | ||||||
|  |   rx_buf_len_ = 0; | ||||||
|  |   rx_header_buf_.clear(); | ||||||
|  |   rx_header_parsed_ = false; | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { | ||||||
|  |   int err; | ||||||
|  |   APIError aerr; | ||||||
|  |  | ||||||
|  |   if (state_ != State::DATA) { | ||||||
|  |     return APIError::WOULD_BLOCK; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ParsedFrame frame; | ||||||
|  |   aerr = try_read_frame_(&frame); | ||||||
|  |   if (aerr != APIError::OK) | ||||||
|  |     return aerr; | ||||||
|  |  | ||||||
|  |   buffer->container = std::move(frame.msg); | ||||||
|  |   buffer->data_offset = 0; | ||||||
|  |   buffer->data_len = rx_header_parsed_len_; | ||||||
|  |   buffer->type = rx_header_parsed_type_; | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  | bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } | ||||||
|  | APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { | ||||||
|  |   int err; | ||||||
|  |   APIError aerr; | ||||||
|  |  | ||||||
|  |   if (state_ != State::DATA) { | ||||||
|  |     return APIError::BAD_STATE; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   std::vector<uint8_t> header; | ||||||
|  |   header.push_back(0x00); | ||||||
|  |   ProtoVarInt(payload_len).encode(header); | ||||||
|  |   ProtoVarInt(type).encode(header); | ||||||
|  |  | ||||||
|  |   aerr = write_raw_(&header[0], header.size()); | ||||||
|  |   if (aerr != APIError::OK) { | ||||||
|  |     return aerr; | ||||||
|  |   } | ||||||
|  |   aerr = write_raw_(payload, payload_len); | ||||||
|  |   if (aerr != APIError::OK) { | ||||||
|  |     return aerr; | ||||||
|  |   } | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  | APIError APIPlaintextFrameHelper::try_send_tx_buf_() { | ||||||
|  |   // try send from tx_buf | ||||||
|  |   while (state_ != State::CLOSED && !tx_buf_.empty()) { | ||||||
|  |     ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size()); | ||||||
|  |     if (is_would_block(sent)) { | ||||||
|  |       break; | ||||||
|  |     } else if (sent == -1) { | ||||||
|  |       state_ = State::FAILED; | ||||||
|  |       HELPER_LOG("Socket write failed with errno %d", errno); | ||||||
|  |       return APIError::SOCKET_WRITE_FAILED; | ||||||
|  |     } | ||||||
|  |     // TODO: inefficient if multiple packets in txbuf | ||||||
|  |     // replace with deque of buffers | ||||||
|  |     tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  | /** Write the data to the socket, or buffer it a write would block | ||||||
|  |  * | ||||||
|  |  * @param data The data to write | ||||||
|  |  * @param len The length of data | ||||||
|  |  */ | ||||||
|  | APIError APIPlaintextFrameHelper::write_raw_(const uint8_t *data, size_t len) { | ||||||
|  |   if (len == 0) | ||||||
|  |     return APIError::OK; | ||||||
|  |   int err; | ||||||
|  |   APIError aerr; | ||||||
|  |  | ||||||
|  |   // uncomment for even more debugging | ||||||
|  | #ifdef HELPER_LOG_PACKETS | ||||||
|  |   ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str()); | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |   if (!tx_buf_.empty()) { | ||||||
|  |     // try to empty tx_buf_ first | ||||||
|  |     aerr = try_send_tx_buf_(); | ||||||
|  |     if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK) | ||||||
|  |       return aerr; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!tx_buf_.empty()) { | ||||||
|  |     // tx buf not empty, can't write now because then stream would be inconsistent | ||||||
|  |     tx_buf_.insert(tx_buf_.end(), data, data + len); | ||||||
|  |     return APIError::OK; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ssize_t sent = socket_->write(data, len); | ||||||
|  |   if (is_would_block(sent)) { | ||||||
|  |     // operation would block, add buffer to tx_buf | ||||||
|  |     tx_buf_.insert(tx_buf_.end(), data, data + len); | ||||||
|  |     return APIError::OK; | ||||||
|  |   } else if (sent == -1) { | ||||||
|  |     // an error occured | ||||||
|  |     state_ = State::FAILED; | ||||||
|  |     HELPER_LOG("Socket write failed with errno %d", errno); | ||||||
|  |     return APIError::SOCKET_WRITE_FAILED; | ||||||
|  |   } else if (sent != len) { | ||||||
|  |     // partially sent, add end to tx_buf | ||||||
|  |     tx_buf_.insert(tx_buf_.end(), data + sent, data + len); | ||||||
|  |     return APIError::OK; | ||||||
|  |   } | ||||||
|  |   // fully sent | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | APIError APIPlaintextFrameHelper::close() { | ||||||
|  |   state_ = State::CLOSED; | ||||||
|  |   int err = socket_->close(); | ||||||
|  |   if (err == -1) | ||||||
|  |     return APIError::CLOSE_FAILED; | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  | APIError APIPlaintextFrameHelper::shutdown(int how) { | ||||||
|  |   int err = socket_->shutdown(how); | ||||||
|  |   if (err == -1) | ||||||
|  |     return APIError::SHUTDOWN_FAILED; | ||||||
|  |   if (how == SHUT_RDWR) { | ||||||
|  |     state_ = State::CLOSED; | ||||||
|  |   } | ||||||
|  |   return APIError::OK; | ||||||
|  | } | ||||||
|  | #endif  // USE_API_PLAINTEXT | ||||||
|  |  | ||||||
|  | }  // namespace api | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										182
									
								
								esphome/components/api/api_frame_helper.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								esphome/components/api/api_frame_helper.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | |||||||
|  | #pragma once | ||||||
|  | #include <cstdint> | ||||||
|  | #include <vector> | ||||||
|  | #include <deque> | ||||||
|  |  | ||||||
|  | #include "esphome/core/defines.h" | ||||||
|  |  | ||||||
|  | #ifdef USE_API_NOISE | ||||||
|  | #include "noise/protocol.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #include "esphome/components/socket/socket.h" | ||||||
|  | #include "api_noise_context.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace api { | ||||||
|  |  | ||||||
|  | struct ReadPacketBuffer { | ||||||
|  |   std::vector<uint8_t> container; | ||||||
|  |   uint16_t type; | ||||||
|  |   size_t data_offset; | ||||||
|  |   size_t data_len; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | struct PacketBuffer { | ||||||
|  |   const std::vector<uint8_t> container; | ||||||
|  |   uint16_t type; | ||||||
|  |   uint8_t data_offset; | ||||||
|  |   uint8_t data_len; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum class APIError : int { | ||||||
|  |   OK = 0, | ||||||
|  |   WOULD_BLOCK = 1001, | ||||||
|  |   BAD_HANDSHAKE_PACKET_LEN = 1002, | ||||||
|  |   BAD_INDICATOR = 1003, | ||||||
|  |   BAD_DATA_PACKET = 1004, | ||||||
|  |   TCP_NODELAY_FAILED = 1005, | ||||||
|  |   TCP_NONBLOCKING_FAILED = 1006, | ||||||
|  |   CLOSE_FAILED = 1007, | ||||||
|  |   SHUTDOWN_FAILED = 1008, | ||||||
|  |   BAD_STATE = 1009, | ||||||
|  |   BAD_ARG = 1010, | ||||||
|  |   SOCKET_READ_FAILED = 1011, | ||||||
|  |   SOCKET_WRITE_FAILED = 1012, | ||||||
|  |   HANDSHAKESTATE_READ_FAILED = 1013, | ||||||
|  |   HANDSHAKESTATE_WRITE_FAILED = 1014, | ||||||
|  |   HANDSHAKESTATE_BAD_STATE = 1015, | ||||||
|  |   CIPHERSTATE_DECRYPT_FAILED = 1016, | ||||||
|  |   CIPHERSTATE_ENCRYPT_FAILED = 1017, | ||||||
|  |   OUT_OF_MEMORY = 1018, | ||||||
|  |   HANDSHAKESTATE_SETUP_FAILED = 1019, | ||||||
|  |   HANDSHAKESTATE_SPLIT_FAILED = 1020, | ||||||
|  |   BAD_HANDSHAKE_ERROR_BYTE = 1021, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const char *api_error_to_str(APIError err); | ||||||
|  |  | ||||||
|  | class APIFrameHelper { | ||||||
|  |  public: | ||||||
|  |   virtual APIError init() = 0; | ||||||
|  |   virtual APIError loop() = 0; | ||||||
|  |   virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; | ||||||
|  |   virtual bool can_write_without_blocking() = 0; | ||||||
|  |   virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0; | ||||||
|  |   virtual std::string getpeername() = 0; | ||||||
|  |   virtual APIError close() = 0; | ||||||
|  |   virtual APIError shutdown(int how) = 0; | ||||||
|  |   // Give this helper a name for logging | ||||||
|  |   virtual void set_log_info(std::string info) = 0; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | #ifdef USE_API_NOISE | ||||||
|  | class APINoiseFrameHelper : public APIFrameHelper { | ||||||
|  |  public: | ||||||
|  |   APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx) | ||||||
|  |       : socket_(std::move(socket)), ctx_(ctx) {} | ||||||
|  |   ~APINoiseFrameHelper(); | ||||||
|  |   APIError init() override; | ||||||
|  |   APIError loop() override; | ||||||
|  |   APIError read_packet(ReadPacketBuffer *buffer) override; | ||||||
|  |   bool can_write_without_blocking() override; | ||||||
|  |   APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; | ||||||
|  |   std::string getpeername() override { return socket_->getpeername(); } | ||||||
|  |   APIError close() override; | ||||||
|  |   APIError shutdown(int how) override; | ||||||
|  |   // Give this helper a name for logging | ||||||
|  |   void set_log_info(std::string info) override { info_ = std::move(info); } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   struct ParsedFrame { | ||||||
|  |     std::vector<uint8_t> msg; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   APIError state_action_(); | ||||||
|  |   APIError try_read_frame_(ParsedFrame *frame); | ||||||
|  |   APIError try_send_tx_buf_(); | ||||||
|  |   APIError write_frame_(const uint8_t *data, size_t len); | ||||||
|  |   APIError write_raw_(const uint8_t *data, size_t len); | ||||||
|  |   APIError init_handshake_(); | ||||||
|  |   APIError check_handshake_finished_(); | ||||||
|  |   void send_explicit_handshake_reject_(const std::string &reason); | ||||||
|  |  | ||||||
|  |   std::unique_ptr<socket::Socket> socket_; | ||||||
|  |  | ||||||
|  |   std::string info_; | ||||||
|  |   uint8_t rx_header_buf_[3]; | ||||||
|  |   size_t rx_header_buf_len_ = 0; | ||||||
|  |   std::vector<uint8_t> rx_buf_; | ||||||
|  |   size_t rx_buf_len_ = 0; | ||||||
|  |  | ||||||
|  |   std::vector<uint8_t> tx_buf_; | ||||||
|  |   std::vector<uint8_t> prologue_; | ||||||
|  |  | ||||||
|  |   std::shared_ptr<APINoiseContext> ctx_; | ||||||
|  |   NoiseHandshakeState *handshake_ = nullptr; | ||||||
|  |   NoiseCipherState *send_cipher_ = nullptr; | ||||||
|  |   NoiseCipherState *recv_cipher_ = nullptr; | ||||||
|  |   NoiseProtocolId nid_; | ||||||
|  |  | ||||||
|  |   enum class State { | ||||||
|  |     INITIALIZE = 1, | ||||||
|  |     CLIENT_HELLO = 2, | ||||||
|  |     SERVER_HELLO = 3, | ||||||
|  |     HANDSHAKE = 4, | ||||||
|  |     DATA = 5, | ||||||
|  |     CLOSED = 6, | ||||||
|  |     FAILED = 7, | ||||||
|  |     EXPLICIT_REJECT = 8, | ||||||
|  |   } state_ = State::INITIALIZE; | ||||||
|  | }; | ||||||
|  | #endif  // USE_API_NOISE | ||||||
|  |  | ||||||
|  | #ifdef USE_API_PLAINTEXT | ||||||
|  | class APIPlaintextFrameHelper : public APIFrameHelper { | ||||||
|  |  public: | ||||||
|  |   APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {} | ||||||
|  |   ~APIPlaintextFrameHelper() = default; | ||||||
|  |   APIError init() override; | ||||||
|  |   APIError loop() override; | ||||||
|  |   APIError read_packet(ReadPacketBuffer *buffer) override; | ||||||
|  |   bool can_write_without_blocking() override; | ||||||
|  |   APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; | ||||||
|  |   std::string getpeername() override { return socket_->getpeername(); } | ||||||
|  |   APIError close() override; | ||||||
|  |   APIError shutdown(int how) override; | ||||||
|  |   // Give this helper a name for logging | ||||||
|  |   void set_log_info(std::string info) override { info_ = std::move(info); } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   struct ParsedFrame { | ||||||
|  |     std::vector<uint8_t> msg; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   APIError try_read_frame_(ParsedFrame *frame); | ||||||
|  |   APIError try_send_tx_buf_(); | ||||||
|  |   APIError write_raw_(const uint8_t *data, size_t len); | ||||||
|  |  | ||||||
|  |   std::unique_ptr<socket::Socket> socket_; | ||||||
|  |  | ||||||
|  |   std::string info_; | ||||||
|  |   std::vector<uint8_t> rx_header_buf_; | ||||||
|  |   bool rx_header_parsed_ = false; | ||||||
|  |   uint32_t rx_header_parsed_type_ = 0; | ||||||
|  |   uint32_t rx_header_parsed_len_ = 0; | ||||||
|  |  | ||||||
|  |   std::vector<uint8_t> rx_buf_; | ||||||
|  |   size_t rx_buf_len_ = 0; | ||||||
|  |  | ||||||
|  |   std::vector<uint8_t> tx_buf_; | ||||||
|  |  | ||||||
|  |   enum class State { | ||||||
|  |     INITIALIZE = 1, | ||||||
|  |     DATA = 2, | ||||||
|  |     CLOSED = 3, | ||||||
|  |     FAILED = 4, | ||||||
|  |   } state_ = State::INITIALIZE; | ||||||
|  | }; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | }  // namespace api | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										23
									
								
								esphome/components/api/api_noise_context.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/api/api_noise_context.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | #pragma once | ||||||
|  | #include <cstdint> | ||||||
|  | #include <array> | ||||||
|  | #include "esphome/core/defines.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace api { | ||||||
|  |  | ||||||
|  | #ifdef USE_API_NOISE | ||||||
|  | using psk_t = std::array<uint8_t, 32>; | ||||||
|  |  | ||||||
|  | class APINoiseContext { | ||||||
|  |  public: | ||||||
|  |   void set_psk(psk_t psk) { psk_ = std::move(psk); } | ||||||
|  |   const psk_t &get_psk() const { return psk_; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   psk_t psk_; | ||||||
|  | }; | ||||||
|  | #endif  // USE_API_NOISE | ||||||
|  |  | ||||||
|  | }  // namespace api | ||||||
|  | }  // namespace esphome | ||||||
| @@ -1817,7 +1817,7 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va | |||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|     case 11: { |     case 11: { | ||||||
|       this->last_reset_type = value.as_enum<enums::SensorLastResetType>(); |       this->legacy_last_reset_type = value.as_enum<enums::SensorLastResetType>(); | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|     case 12: { |     case 12: { | ||||||
| @@ -1879,7 +1879,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { | |||||||
|   buffer.encode_bool(8, this->force_update); |   buffer.encode_bool(8, this->force_update); | ||||||
|   buffer.encode_string(9, this->device_class); |   buffer.encode_string(9, this->device_class); | ||||||
|   buffer.encode_enum<enums::SensorStateClass>(10, this->state_class); |   buffer.encode_enum<enums::SensorStateClass>(10, this->state_class); | ||||||
|   buffer.encode_enum<enums::SensorLastResetType>(11, this->last_reset_type); |   buffer.encode_enum<enums::SensorLastResetType>(11, this->legacy_last_reset_type); | ||||||
|   buffer.encode_bool(12, this->disabled_by_default); |   buffer.encode_bool(12, this->disabled_by_default); | ||||||
| } | } | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
| @@ -1928,8 +1928,8 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { | |||||||
|   out.append(proto_enum_to_string<enums::SensorStateClass>(this->state_class)); |   out.append(proto_enum_to_string<enums::SensorStateClass>(this->state_class)); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
|   out.append("  last_reset_type: "); |   out.append("  legacy_last_reset_type: "); | ||||||
|   out.append(proto_enum_to_string<enums::SensorLastResetType>(this->last_reset_type)); |   out.append(proto_enum_to_string<enums::SensorLastResetType>(this->legacy_last_reset_type)); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
|   out.append("  disabled_by_default: "); |   out.append("  disabled_by_default: "); | ||||||
|   | |||||||
| @@ -510,7 +510,7 @@ class ListEntitiesSensorResponse : public ProtoMessage { | |||||||
|   bool force_update{false}; |   bool force_update{false}; | ||||||
|   std::string device_class{}; |   std::string device_class{}; | ||||||
|   enums::SensorStateClass state_class{}; |   enums::SensorStateClass state_class{}; | ||||||
|   enums::SensorLastResetType last_reset_type{}; |   enums::SensorLastResetType legacy_last_reset_type{}; | ||||||
|   bool disabled_by_default{false}; |   bool disabled_by_default{false}; | ||||||
|   void encode(ProtoWriteBuffer buffer) const override; |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   | |||||||
| @@ -1,10 +1,11 @@ | |||||||
| #include "api_server.h" | #include "api_server.h" | ||||||
| #include "api_connection.h" | #include "api_connection.h" | ||||||
| #include "esphome/core/log.h" |  | ||||||
| #include "esphome/core/application.h" | #include "esphome/core/application.h" | ||||||
| #include "esphome/core/util.h" |  | ||||||
| #include "esphome/core/defines.h" | #include "esphome/core/defines.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/core/util.h" | ||||||
| #include "esphome/core/version.h" | #include "esphome/core/version.h" | ||||||
|  | #include <cerrno> | ||||||
|  |  | ||||||
| #ifdef USE_LOGGER | #ifdef USE_LOGGER | ||||||
| #include "esphome/components/logger/logger.h" | #include "esphome/components/logger/logger.h" | ||||||
| @@ -21,20 +22,45 @@ static const char *const TAG = "api"; | |||||||
| void APIServer::setup() { | void APIServer::setup() { | ||||||
|   ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server..."); |   ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server..."); | ||||||
|   this->setup_controller(); |   this->setup_controller(); | ||||||
|   this->server_ = AsyncServer(this->port_); |   socket_ = socket::socket(AF_INET, SOCK_STREAM, 0); | ||||||
|   this->server_.setNoDelay(false); |   if (socket_ == nullptr) { | ||||||
|   this->server_.begin(); |     ESP_LOGW(TAG, "Could not create socket."); | ||||||
|   this->server_.onClient( |     this->mark_failed(); | ||||||
|       [](void *s, AsyncClient *client) { |  | ||||||
|         if (client == nullptr) |  | ||||||
|     return; |     return; | ||||||
|  |   } | ||||||
|  |   int enable = 1; | ||||||
|  |   int err = socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); | ||||||
|  |   if (err != 0) { | ||||||
|  |     ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); | ||||||
|  |     // we can still continue | ||||||
|  |   } | ||||||
|  |   err = socket_->setblocking(false); | ||||||
|  |   if (err != 0) { | ||||||
|  |     ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   struct sockaddr_in server; | ||||||
|  |   memset(&server, 0, sizeof(server)); | ||||||
|  |   server.sin_family = AF_INET; | ||||||
|  |   server.sin_addr.s_addr = ESPHOME_INADDR_ANY; | ||||||
|  |   server.sin_port = htons(this->port_); | ||||||
|  |  | ||||||
|  |   err = socket_->bind((struct sockaddr *) &server, sizeof(server)); | ||||||
|  |   if (err != 0) { | ||||||
|  |     ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   err = socket_->listen(4); | ||||||
|  |   if (err != 0) { | ||||||
|  |     ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|         // can't print here because in lwIP thread |  | ||||||
|         // ESP_LOGD(TAG, "New client connected from %s", client->remoteIP().toString().c_str()); |  | ||||||
|         auto *a_this = (APIServer *) s; |  | ||||||
|         a_this->clients_.push_back(new APIConnection(client, a_this)); |  | ||||||
|       }, |  | ||||||
|       this); |  | ||||||
| #ifdef USE_LOGGER | #ifdef USE_LOGGER | ||||||
|   if (logger::global_logger != nullptr) { |   if (logger::global_logger != nullptr) { | ||||||
|     logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) { |     logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) { | ||||||
| @@ -59,12 +85,26 @@ void APIServer::setup() { | |||||||
| #endif | #endif | ||||||
| } | } | ||||||
| void APIServer::loop() { | void APIServer::loop() { | ||||||
|  |   // Accept new clients | ||||||
|  |   while (true) { | ||||||
|  |     struct sockaddr_storage source_addr; | ||||||
|  |     socklen_t addr_len = sizeof(source_addr); | ||||||
|  |     auto sock = socket_->accept((struct sockaddr *) &source_addr, &addr_len); | ||||||
|  |     if (!sock) | ||||||
|  |       break; | ||||||
|  |     ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str()); | ||||||
|  |  | ||||||
|  |     auto *conn = new APIConnection(std::move(sock), this); | ||||||
|  |     clients_.push_back(conn); | ||||||
|  |     conn->start(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // Partition clients into remove and active |   // Partition clients into remove and active | ||||||
|   auto new_end = |   auto new_end = | ||||||
|       std::partition(this->clients_.begin(), this->clients_.end(), [](APIConnection *conn) { return !conn->remove_; }); |       std::partition(this->clients_.begin(), this->clients_.end(), [](APIConnection *conn) { return !conn->remove_; }); | ||||||
|   // print disconnection messages |   // print disconnection messages | ||||||
|   for (auto it = new_end; it != this->clients_.end(); ++it) { |   for (auto it = new_end; it != this->clients_.end(); ++it) { | ||||||
|     ESP_LOGD(TAG, "Disconnecting %s", (*it)->client_info_.c_str()); |     ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str()); | ||||||
|   } |   } | ||||||
|   // only then delete the pointers, otherwise log routine |   // only then delete the pointers, otherwise log routine | ||||||
|   // would access freed memory |   // would access freed memory | ||||||
|   | |||||||
| @@ -4,19 +4,14 @@ | |||||||
| #include "esphome/core/controller.h" | #include "esphome/core/controller.h" | ||||||
| #include "esphome/core/defines.h" | #include "esphome/core/defines.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/components/socket/socket.h" | ||||||
| #include "api_pb2.h" | #include "api_pb2.h" | ||||||
| #include "api_pb2_service.h" | #include "api_pb2_service.h" | ||||||
| #include "util.h" | #include "util.h" | ||||||
| #include "list_entities.h" | #include "list_entities.h" | ||||||
| #include "subscribe_state.h" | #include "subscribe_state.h" | ||||||
| #include "user_services.h" | #include "user_services.h" | ||||||
|  | #include "api_noise_context.h" | ||||||
| #ifdef ARDUINO_ARCH_ESP32 |  | ||||||
| #include <AsyncTCP.h> |  | ||||||
| #endif |  | ||||||
| #ifdef ARDUINO_ARCH_ESP8266 |  | ||||||
| #include <ESPAsyncTCP.h> |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace api { | namespace api { | ||||||
| @@ -35,6 +30,12 @@ class APIServer : public Component, public Controller { | |||||||
|   void set_port(uint16_t port); |   void set_port(uint16_t port); | ||||||
|   void set_password(const std::string &password); |   void set_password(const std::string &password); | ||||||
|   void set_reboot_timeout(uint32_t reboot_timeout); |   void set_reboot_timeout(uint32_t reboot_timeout); | ||||||
|  |  | ||||||
|  | #ifdef USE_API_NOISE | ||||||
|  |   void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(std::move(psk)); } | ||||||
|  |   std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; } | ||||||
|  | #endif  // USE_API_NOISE | ||||||
|  |  | ||||||
|   void handle_disconnect(APIConnection *conn); |   void handle_disconnect(APIConnection *conn); | ||||||
| #ifdef USE_BINARY_SENSOR | #ifdef USE_BINARY_SENSOR | ||||||
|   void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override; |   void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override; | ||||||
| @@ -86,7 +87,7 @@ class APIServer : public Component, public Controller { | |||||||
|   const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; } |   const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   AsyncServer server_{0}; |   std::unique_ptr<socket::Socket> socket_ = nullptr; | ||||||
|   uint16_t port_{6053}; |   uint16_t port_{6053}; | ||||||
|   uint32_t reboot_timeout_{300000}; |   uint32_t reboot_timeout_{300000}; | ||||||
|   uint32_t last_connected_{0}; |   uint32_t last_connected_{0}; | ||||||
| @@ -94,6 +95,10 @@ class APIServer : public Component, public Controller { | |||||||
|   std::string password_; |   std::string password_; | ||||||
|   std::vector<HomeAssistantStateSubscription> state_subs_; |   std::vector<HomeAssistantStateSubscription> state_subs_; | ||||||
|   std::vector<UserServiceDescriptor *> user_services_; |   std::vector<UserServiceDescriptor *> user_services_; | ||||||
|  |  | ||||||
|  | #ifdef USE_API_NOISE | ||||||
|  |   std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>(); | ||||||
|  | #endif  // USE_API_NOISE | ||||||
| }; | }; | ||||||
|  |  | ||||||
| extern APIServer *global_api_server;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | extern APIServer *global_api_server;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
|   | |||||||
							
								
								
									
										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 = 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)) | ||||||
| @@ -19,8 +19,8 @@ from esphome.const import ( | |||||||
|     DEVICE_CLASS_VOLTAGE, |     DEVICE_CLASS_VOLTAGE, | ||||||
|     ICON_LIGHTBULB, |     ICON_LIGHTBULB, | ||||||
|     ICON_CURRENT_AC, |     ICON_CURRENT_AC, | ||||||
|     LAST_RESET_TYPE_AUTO, |  | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|  |     STATE_CLASS_TOTAL_INCREASING, | ||||||
|     UNIT_HERTZ, |     UNIT_HERTZ, | ||||||
|     UNIT_VOLT, |     UNIT_VOLT, | ||||||
|     UNIT_AMPERE, |     UNIT_AMPERE, | ||||||
| @@ -94,15 +94,13 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( | |||||||
|             unit_of_measurement=UNIT_WATT_HOURS, |             unit_of_measurement=UNIT_WATT_HOURS, | ||||||
|             accuracy_decimals=2, |             accuracy_decimals=2, | ||||||
|             device_class=DEVICE_CLASS_ENERGY, |             device_class=DEVICE_CLASS_ENERGY, | ||||||
|             state_class=STATE_CLASS_MEASUREMENT, |             state_class=STATE_CLASS_TOTAL_INCREASING, | ||||||
|             last_reset_type=LAST_RESET_TYPE_AUTO, |  | ||||||
|         ), |         ), | ||||||
|         cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema( |         cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema( | ||||||
|             unit_of_measurement=UNIT_WATT_HOURS, |             unit_of_measurement=UNIT_WATT_HOURS, | ||||||
|             accuracy_decimals=2, |             accuracy_decimals=2, | ||||||
|             device_class=DEVICE_CLASS_ENERGY, |             device_class=DEVICE_CLASS_ENERGY, | ||||||
|             state_class=STATE_CLASS_MEASUREMENT, |             state_class=STATE_CLASS_TOTAL_INCREASING, | ||||||
|             last_reset_type=LAST_RESET_TYPE_AUTO, |  | ||||||
|         ), |         ), | ||||||
|         cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, |         cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, | ||||||
|         cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, |         cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, | ||||||
|   | |||||||
| @@ -55,7 +55,10 @@ void BinaryFan::loop() { | |||||||
|     ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); |     ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| float BinaryFan::get_setup_priority() const { return setup_priority::DATA; } |  | ||||||
|  | // We need a higher priority than the FanState component to make sure that the traits are set | ||||||
|  | // when that component sets itself up. | ||||||
|  | float BinaryFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; } | ||||||
|  |  | ||||||
| }  // namespace binary | }  // namespace binary | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -48,6 +48,7 @@ from esphome.const import ( | |||||||
|     DEVICE_CLASS_SAFETY, |     DEVICE_CLASS_SAFETY, | ||||||
|     DEVICE_CLASS_SMOKE, |     DEVICE_CLASS_SMOKE, | ||||||
|     DEVICE_CLASS_SOUND, |     DEVICE_CLASS_SOUND, | ||||||
|  |     DEVICE_CLASS_UPDATE, | ||||||
|     DEVICE_CLASS_VIBRATION, |     DEVICE_CLASS_VIBRATION, | ||||||
|     DEVICE_CLASS_WINDOW, |     DEVICE_CLASS_WINDOW, | ||||||
| ) | ) | ||||||
| @@ -79,6 +80,7 @@ DEVICE_CLASSES = [ | |||||||
|     DEVICE_CLASS_SAFETY, |     DEVICE_CLASS_SAFETY, | ||||||
|     DEVICE_CLASS_SMOKE, |     DEVICE_CLASS_SMOKE, | ||||||
|     DEVICE_CLASS_SOUND, |     DEVICE_CLASS_SOUND, | ||||||
|  |     DEVICE_CLASS_UPDATE, | ||||||
|     DEVICE_CLASS_VIBRATION, |     DEVICE_CLASS_VIBRATION, | ||||||
|     DEVICE_CLASS_WINDOW, |     DEVICE_CLASS_WINDOW, | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -1,7 +1,14 @@ | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.components import binary_sensor, esp32_ble_tracker | from esphome.components import binary_sensor, esp32_ble_tracker | ||||||
| from esphome.const import CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_ID | from esphome.const import ( | ||||||
|  |     CONF_MAC_ADDRESS, | ||||||
|  |     CONF_SERVICE_UUID, | ||||||
|  |     CONF_IBEACON_MAJOR, | ||||||
|  |     CONF_IBEACON_MINOR, | ||||||
|  |     CONF_IBEACON_UUID, | ||||||
|  |     CONF_ID, | ||||||
|  | ) | ||||||
|  |  | ||||||
| DEPENDENCIES = ["esp32_ble_tracker"] | DEPENDENCIES = ["esp32_ble_tracker"] | ||||||
|  |  | ||||||
| @@ -13,17 +20,30 @@ BLEPresenceDevice = ble_presence_ns.class_( | |||||||
|     esp32_ble_tracker.ESPBTDeviceListener, |     esp32_ble_tracker.ESPBTDeviceListener, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _validate(config): | ||||||
|  |     if CONF_IBEACON_MAJOR in config and CONF_IBEACON_UUID not in config: | ||||||
|  |         raise cv.Invalid("iBeacon major identifier requires iBeacon UUID") | ||||||
|  |     if CONF_IBEACON_MINOR in config and CONF_IBEACON_UUID not in config: | ||||||
|  |         raise cv.Invalid("iBeacon minor identifier requires iBeacon UUID") | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = cv.All( | ||||||
|     binary_sensor.BINARY_SENSOR_SCHEMA.extend( |     binary_sensor.BINARY_SENSOR_SCHEMA.extend( | ||||||
|         { |         { | ||||||
|             cv.GenerateID(): cv.declare_id(BLEPresenceDevice), |             cv.GenerateID(): cv.declare_id(BLEPresenceDevice), | ||||||
|             cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, |             cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, | ||||||
|             cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, |             cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, | ||||||
|  |             cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t, | ||||||
|  |             cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t, | ||||||
|  |             cv.Optional(CONF_IBEACON_UUID): cv.uuid, | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) |     .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) | ||||||
|     .extend(cv.COMPONENT_SCHEMA), |     .extend(cv.COMPONENT_SCHEMA), | ||||||
|     cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID), |     cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_IBEACON_UUID), | ||||||
|  |     _validate, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -50,5 +70,15 @@ async def to_code(config): | |||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|         elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): |         elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): | ||||||
|             uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID]) |             uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) | ||||||
|             cg.add(var.set_service_uuid128(uuid128)) |             cg.add(var.set_service_uuid128(uuid128)) | ||||||
|  |  | ||||||
|  |     if CONF_IBEACON_UUID in config: | ||||||
|  |         ibeacon_uuid = esp32_ble_tracker.as_hex_array(str(config[CONF_IBEACON_UUID])) | ||||||
|  |         cg.add(var.set_ibeacon_uuid(ibeacon_uuid)) | ||||||
|  |  | ||||||
|  |         if CONF_IBEACON_MAJOR in config: | ||||||
|  |             cg.add(var.set_ibeacon_major(config[CONF_IBEACON_MAJOR])) | ||||||
|  |  | ||||||
|  |         if CONF_IBEACON_MINOR in config: | ||||||
|  |             cg.add(var.set_ibeacon_minor(config[CONF_IBEACON_MINOR])) | ||||||
|   | |||||||
| @@ -14,34 +14,48 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, | |||||||
|                           public Component { |                           public Component { | ||||||
|  public: |  public: | ||||||
|   void set_address(uint64_t address) { |   void set_address(uint64_t address) { | ||||||
|     this->by_address_ = true; |     this->match_by_ = MATCH_BY_MAC_ADDRESS; | ||||||
|     this->address_ = address; |     this->address_ = address; | ||||||
|   } |   } | ||||||
|   void set_service_uuid16(uint16_t uuid) { |   void set_service_uuid16(uint16_t uuid) { | ||||||
|     this->by_address_ = false; |     this->match_by_ = MATCH_BY_SERVICE_UUID; | ||||||
|     this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid); |     this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid); | ||||||
|   } |   } | ||||||
|   void set_service_uuid32(uint32_t uuid) { |   void set_service_uuid32(uint32_t uuid) { | ||||||
|     this->by_address_ = false; |     this->match_by_ = MATCH_BY_SERVICE_UUID; | ||||||
|     this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint32(uuid); |     this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint32(uuid); | ||||||
|   } |   } | ||||||
|   void set_service_uuid128(uint8_t *uuid) { |   void set_service_uuid128(uint8_t *uuid) { | ||||||
|     this->by_address_ = false; |     this->match_by_ = MATCH_BY_SERVICE_UUID; | ||||||
|     this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid); |     this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid); | ||||||
|   } |   } | ||||||
|  |   void set_ibeacon_uuid(uint8_t *uuid) { | ||||||
|  |     this->match_by_ = MATCH_BY_IBEACON_UUID; | ||||||
|  |     this->ibeacon_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid); | ||||||
|  |   } | ||||||
|  |   void set_ibeacon_major(uint16_t major) { | ||||||
|  |     this->check_ibeacon_major_ = true; | ||||||
|  |     this->ibeacon_major_ = major; | ||||||
|  |   } | ||||||
|  |   void set_ibeacon_minor(uint16_t minor) { | ||||||
|  |     this->check_ibeacon_minor_ = true; | ||||||
|  |     this->ibeacon_minor_ = minor; | ||||||
|  |   } | ||||||
|   void on_scan_end() override { |   void on_scan_end() override { | ||||||
|     if (!this->found_) |     if (!this->found_) | ||||||
|       this->publish_state(false); |       this->publish_state(false); | ||||||
|     this->found_ = false; |     this->found_ = false; | ||||||
|   } |   } | ||||||
|   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { |   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { | ||||||
|     if (this->by_address_) { |     switch (this->match_by_) { | ||||||
|  |       case MATCH_BY_MAC_ADDRESS: | ||||||
|         if (device.address_uint64() == this->address_) { |         if (device.address_uint64() == this->address_) { | ||||||
|           this->publish_state(true); |           this->publish_state(true); | ||||||
|           this->found_ = true; |           this->found_ = true; | ||||||
|           return true; |           return true; | ||||||
|         } |         } | ||||||
|     } else { |         break; | ||||||
|  |       case MATCH_BY_SERVICE_UUID: | ||||||
|         for (auto uuid : device.get_service_uuids()) { |         for (auto uuid : device.get_service_uuids()) { | ||||||
|           if (this->uuid_ == uuid) { |           if (this->uuid_ == uuid) { | ||||||
|             this->publish_state(device.get_rssi()); |             this->publish_state(device.get_rssi()); | ||||||
| @@ -49,6 +63,29 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, | |||||||
|             return true; |             return true; | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|  |         break; | ||||||
|  |       case MATCH_BY_IBEACON_UUID: | ||||||
|  |         if (!device.get_ibeacon().has_value()) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         auto ibeacon = device.get_ibeacon().value(); | ||||||
|  |  | ||||||
|  |         if (this->ibeacon_uuid_ != ibeacon.get_uuid()) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (this->check_ibeacon_major_ && this->ibeacon_major_ != ibeacon.get_major()) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (this->check_ibeacon_minor_ && this->ibeacon_minor_ != ibeacon.get_minor()) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this->publish_state(device.get_rssi()); | ||||||
|  |         this->found_ = true; | ||||||
|  |         return true; | ||||||
|     } |     } | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
| @@ -56,10 +93,20 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, | |||||||
|   float get_setup_priority() const override { return setup_priority::DATA; } |   float get_setup_priority() const override { return setup_priority::DATA; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|  |   enum MATCH_TYPE { MATCH_BY_MAC_ADDRESS, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; | ||||||
|  |   MATCH_TYPE match_by_; | ||||||
|  |  | ||||||
|   bool found_{false}; |   bool found_{false}; | ||||||
|   bool by_address_{false}; |  | ||||||
|   uint64_t address_; |   uint64_t address_; | ||||||
|  |  | ||||||
|   esp32_ble_tracker::ESPBTUUID uuid_; |   esp32_ble_tracker::ESPBTUUID uuid_; | ||||||
|  |  | ||||||
|  |   esp32_ble_tracker::ESPBTUUID ibeacon_uuid_; | ||||||
|  |   uint16_t ibeacon_major_; | ||||||
|  |   bool check_ibeacon_major_; | ||||||
|  |   uint16_t ibeacon_minor_; | ||||||
|  |   bool check_ibeacon_minor_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace ble_presence | }  // namespace ble_presence | ||||||
|   | |||||||
| @@ -60,5 +60,5 @@ async def to_code(config): | |||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|         elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): |         elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): | ||||||
|             uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID]) |             uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) | ||||||
|             cg.add(var.set_service_uuid128(uuid128)) |             cg.add(var.set_service_uuid128(uuid128)) | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ static const char *const TAG = "bme680_bsec.sensor"; | |||||||
|  |  | ||||||
| static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; | static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; | ||||||
|  |  | ||||||
| BME680BSECComponent *BME680BSECComponent::instance; | BME680BSECComponent *BME680BSECComponent::instance;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
|  |  | ||||||
| void BME680BSECComponent::setup() { | void BME680BSECComponent::setup() { | ||||||
|   ESP_LOGCONFIG(TAG, "Setting up BME680 via BSEC..."); |   ESP_LOGCONFIG(TAG, "Setting up BME680 via BSEC..."); | ||||||
| @@ -359,7 +359,7 @@ void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float va | |||||||
|   sensor->publish_state(value); |   sensor->publish_state(value); | ||||||
| } | } | ||||||
|  |  | ||||||
| void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value) { | void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) { | ||||||
|   if (!sensor || (sensor->has_state() && sensor->state == value)) { |   if (!sensor || (sensor->has_state() && sensor->state == value)) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -70,7 +70,7 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { | |||||||
|   int64_t get_time_ns_(); |   int64_t get_time_ns_(); | ||||||
|  |  | ||||||
|   void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false); |   void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false); | ||||||
|   void publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value); |   void publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value); | ||||||
|  |  | ||||||
|   void load_state_(); |   void load_state_(); | ||||||
|   void save_state_(uint8_t accuracy); |   void save_state_(uint8_t accuracy); | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ static const char *const TAG = "ccs811"; | |||||||
|     return; \ |     return; \ | ||||||
|   } |   } | ||||||
|  |  | ||||||
| #define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICAITON_FAILED) | #define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICATION_FAILED) | ||||||
|  |  | ||||||
| void CCS811Component::setup() { | void CCS811Component::setup() { | ||||||
|   // page 9 programming guide - hwid is always 0x81 |   // page 9 programming guide - hwid is always 0x81 | ||||||
| @@ -38,12 +38,14 @@ void CCS811Component::setup() { | |||||||
|   // set MEAS_MODE (page 5) |   // set MEAS_MODE (page 5) | ||||||
|   uint8_t meas_mode = 0; |   uint8_t meas_mode = 0; | ||||||
|   uint32_t interval = this->get_update_interval(); |   uint32_t interval = this->get_update_interval(); | ||||||
|   if (interval <= 1000) |   if (interval >= 60 * 1000) | ||||||
|     meas_mode = 1 << 4; |     meas_mode = 3 << 4;  // sensor takes a reading every 60 seconds | ||||||
|   else if (interval <= 10000) |   else if (interval >= 10 * 1000) | ||||||
|     meas_mode = 2 << 4; |     meas_mode = 2 << 4;  // sensor takes a reading every 10 seconds | ||||||
|  |   else if (interval >= 1 * 1000) | ||||||
|  |     meas_mode = 1 << 4;  // sensor takes a reading every second | ||||||
|   else |   else | ||||||
|     meas_mode = 3 << 4; |     meas_mode = 4 << 4;  // sensor takes a reading every 250ms | ||||||
|  |  | ||||||
|   CHECKED_IO(this->write_byte(0x01, meas_mode)) |   CHECKED_IO(this->write_byte(0x01, meas_mode)) | ||||||
|  |  | ||||||
| @@ -51,6 +53,36 @@ void CCS811Component::setup() { | |||||||
|     // baseline available, write to sensor |     // baseline available, write to sensor | ||||||
|     this->write_bytes(0x11, decode_uint16(*this->baseline_)); |     this->write_bytes(0x11, decode_uint16(*this->baseline_)); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   auto hardware_version_data = this->read_bytes<1>(0x21); | ||||||
|  |   auto bootloader_version_data = this->read_bytes<2>(0x23); | ||||||
|  |   auto application_version_data = this->read_bytes<2>(0x24); | ||||||
|  |  | ||||||
|  |   uint8_t hardware_version = 0; | ||||||
|  |   uint16_t bootloader_version = 0; | ||||||
|  |   uint16_t application_version = 0; | ||||||
|  |  | ||||||
|  |   if (hardware_version_data.has_value()) { | ||||||
|  |     hardware_version = (*hardware_version_data)[0]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (bootloader_version_data.has_value()) { | ||||||
|  |     bootloader_version = encode_uint16((*bootloader_version_data)[0], (*bootloader_version_data)[1]); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (application_version_data.has_value()) { | ||||||
|  |     application_version = encode_uint16((*application_version_data)[0], (*application_version_data)[1]); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGD(TAG, "hardware_version=0x%x bootloader_version=0x%x application_version=0x%x\n", hardware_version, | ||||||
|  |            bootloader_version, application_version); | ||||||
|  |   if (this->version_ != nullptr) { | ||||||
|  |     char version[20];  // "15.15.15 (0xffff)" is 17 chars, plus NUL, plus wiggle room | ||||||
|  |     sprintf(version, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15), (application_version >> 8 & 15), | ||||||
|  |             (application_version >> 4 & 15), application_version); | ||||||
|  |     ESP_LOGD(TAG, "publishing version state: %s", version); | ||||||
|  |     this->version_->publish_state(version); | ||||||
|  |   } | ||||||
| } | } | ||||||
| void CCS811Component::update() { | void CCS811Component::update() { | ||||||
|   if (!this->status_has_data_()) |   if (!this->status_has_data_()) | ||||||
| @@ -117,6 +149,7 @@ void CCS811Component::dump_config() { | |||||||
|   LOG_UPDATE_INTERVAL(this) |   LOG_UPDATE_INTERVAL(this) | ||||||
|   LOG_SENSOR("  ", "CO2 Sensor", this->co2_) |   LOG_SENSOR("  ", "CO2 Sensor", this->co2_) | ||||||
|   LOG_SENSOR("  ", "TVOC Sensor", this->tvoc_) |   LOG_SENSOR("  ", "TVOC Sensor", this->tvoc_) | ||||||
|  |   LOG_TEXT_SENSOR("  ", "Firmware Version Sensor", this->version_) | ||||||
|   if (this->baseline_) { |   if (this->baseline_) { | ||||||
|     ESP_LOGCONFIG(TAG, "  Baseline: %04X", *this->baseline_); |     ESP_LOGCONFIG(TAG, "  Baseline: %04X", *this->baseline_); | ||||||
|   } else { |   } else { | ||||||
| @@ -124,7 +157,7 @@ void CCS811Component::dump_config() { | |||||||
|   } |   } | ||||||
|   if (this->is_failed()) { |   if (this->is_failed()) { | ||||||
|     switch (this->error_code_) { |     switch (this->error_code_) { | ||||||
|       case COMMUNICAITON_FAILED: |       case COMMUNICATION_FAILED: | ||||||
|         ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); |         ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); | ||||||
|         break; |         break; | ||||||
|       case INVALID_ID: |       case INVALID_ID: | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ | |||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/preferences.h" | #include "esphome/core/preferences.h" | ||||||
| #include "esphome/components/sensor/sensor.h" | #include "esphome/components/sensor/sensor.h" | ||||||
|  | #include "esphome/components/text_sensor/text_sensor.h" | ||||||
| #include "esphome/components/i2c/i2c.h" | #include "esphome/components/i2c/i2c.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| @@ -12,6 +13,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { | |||||||
|  public: |  public: | ||||||
|   void set_co2(sensor::Sensor *co2) { co2_ = co2; } |   void set_co2(sensor::Sensor *co2) { co2_ = co2; } | ||||||
|   void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; } |   void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; } | ||||||
|  |   void set_version(text_sensor::TextSensor *version) { version_ = version; } | ||||||
|   void set_baseline(uint16_t baseline) { baseline_ = baseline; } |   void set_baseline(uint16_t baseline) { baseline_ = baseline; } | ||||||
|   void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } |   void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } | ||||||
|   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } |   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } | ||||||
| @@ -34,7 +36,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { | |||||||
|  |  | ||||||
|   enum ErrorCode { |   enum ErrorCode { | ||||||
|     UNKNOWN, |     UNKNOWN, | ||||||
|     COMMUNICAITON_FAILED, |     COMMUNICATION_FAILED, | ||||||
|     INVALID_ID, |     INVALID_ID, | ||||||
|     SENSOR_REPORTED_ERROR, |     SENSOR_REPORTED_ERROR, | ||||||
|     APP_INVALID, |     APP_INVALID, | ||||||
| @@ -43,6 +45,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { | |||||||
|  |  | ||||||
|   sensor::Sensor *co2_{nullptr}; |   sensor::Sensor *co2_{nullptr}; | ||||||
|   sensor::Sensor *tvoc_{nullptr}; |   sensor::Sensor *tvoc_{nullptr}; | ||||||
|  |   text_sensor::TextSensor *version_{nullptr}; | ||||||
|   optional<uint16_t> baseline_{}; |   optional<uint16_t> baseline_{}; | ||||||
|   /// Input sensor for humidity reading. |   /// Input sensor for humidity reading. | ||||||
|   sensor::Sensor *humidity_{nullptr}; |   sensor::Sensor *humidity_{nullptr}; | ||||||
|   | |||||||
| @@ -1,9 +1,13 @@ | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.components import i2c, sensor | from esphome.components import i2c, sensor, text_sensor | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|  |     CONF_ICON, | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     ICON_RADIATOR, |     ICON_RADIATOR, | ||||||
|  |     ICON_RESTART, | ||||||
|  |     DEVICE_CLASS_CARBON_DIOXIDE, | ||||||
|  |     DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|     UNIT_PARTS_PER_MILLION, |     UNIT_PARTS_PER_MILLION, | ||||||
|     UNIT_PARTS_PER_BILLION, |     UNIT_PARTS_PER_BILLION, | ||||||
| @@ -12,9 +16,12 @@ from esphome.const import ( | |||||||
|     CONF_TEMPERATURE, |     CONF_TEMPERATURE, | ||||||
|     CONF_TVOC, |     CONF_TVOC, | ||||||
|     CONF_HUMIDITY, |     CONF_HUMIDITY, | ||||||
|  |     CONF_VERSION, | ||||||
|     ICON_MOLECULE_CO2, |     ICON_MOLECULE_CO2, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | AUTO_LOAD = ["text_sensor"] | ||||||
|  | CODEOWNERS = ["@habbie"] | ||||||
| DEPENDENCIES = ["i2c"] | DEPENDENCIES = ["i2c"] | ||||||
|  |  | ||||||
| ccs811_ns = cg.esphome_ns.namespace("ccs811") | ccs811_ns = cg.esphome_ns.namespace("ccs811") | ||||||
| @@ -30,14 +37,22 @@ CONFIG_SCHEMA = ( | |||||||
|                 unit_of_measurement=UNIT_PARTS_PER_MILLION, |                 unit_of_measurement=UNIT_PARTS_PER_MILLION, | ||||||
|                 icon=ICON_MOLECULE_CO2, |                 icon=ICON_MOLECULE_CO2, | ||||||
|                 accuracy_decimals=0, |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_CARBON_DIOXIDE, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|             ), |             ), | ||||||
|             cv.Required(CONF_TVOC): sensor.sensor_schema( |             cv.Required(CONF_TVOC): sensor.sensor_schema( | ||||||
|                 unit_of_measurement=UNIT_PARTS_PER_BILLION, |                 unit_of_measurement=UNIT_PARTS_PER_BILLION, | ||||||
|                 icon=ICON_RADIATOR, |                 icon=ICON_RADIATOR, | ||||||
|                 accuracy_decimals=0, |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|             ), |             ), | ||||||
|  |             cv.Optional(CONF_VERSION): text_sensor.TEXT_SENSOR_SCHEMA.extend( | ||||||
|  |                 { | ||||||
|  |                     cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), | ||||||
|  |                     cv.Optional(CONF_ICON, default=ICON_RESTART): cv.icon, | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|             cv.Optional(CONF_BASELINE): cv.hex_uint16_t, |             cv.Optional(CONF_BASELINE): cv.hex_uint16_t, | ||||||
|             cv.Optional(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), |             cv.Optional(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), | ||||||
|             cv.Optional(CONF_HUMIDITY): cv.use_id(sensor.Sensor), |             cv.Optional(CONF_HUMIDITY): cv.use_id(sensor.Sensor), | ||||||
| @@ -58,6 +73,11 @@ async def to_code(config): | |||||||
|     sens = await sensor.new_sensor(config[CONF_TVOC]) |     sens = await sensor.new_sensor(config[CONF_TVOC]) | ||||||
|     cg.add(var.set_tvoc(sens)) |     cg.add(var.set_tvoc(sens)) | ||||||
|  |  | ||||||
|  |     if CONF_VERSION in config: | ||||||
|  |         sens = cg.new_Pvariable(config[CONF_VERSION][CONF_ID]) | ||||||
|  |         await text_sensor.register_text_sensor(sens, config[CONF_VERSION]) | ||||||
|  |         cg.add(var.set_version(sens)) | ||||||
|  |  | ||||||
|     if CONF_BASELINE in config: |     if CONF_BASELINE in config: | ||||||
|         cg.add(var.set_baseline(config[CONF_BASELINE])) |         cg.add(var.set_baseline(config[CONF_BASELINE])) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -63,6 +63,7 @@ validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True) | |||||||
|  |  | ||||||
| ClimatePreset = climate_ns.enum("ClimatePreset") | ClimatePreset = climate_ns.enum("ClimatePreset") | ||||||
| CLIMATE_PRESETS = { | CLIMATE_PRESETS = { | ||||||
|  |     "NONE": ClimatePreset.CLIMATE_PRESET_NONE, | ||||||
|     "ECO": ClimatePreset.CLIMATE_PRESET_ECO, |     "ECO": ClimatePreset.CLIMATE_PRESET_ECO, | ||||||
|     "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY, |     "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY, | ||||||
|     "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, |     "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, | ||||||
|   | |||||||
| @@ -494,5 +494,74 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { | |||||||
|   climate->publish_state(); |   climate->publish_state(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | template<typename T1, typename T2> bool set_alternative(optional<T1> &dst, optional<T2> &alt, const T1 &src) { | ||||||
|  |   bool is_changed = alt.has_value(); | ||||||
|  |   alt.reset(); | ||||||
|  |   if (is_changed || dst != src) { | ||||||
|  |     dst = src; | ||||||
|  |     is_changed = true; | ||||||
|  |   } | ||||||
|  |   return is_changed; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool Climate::set_fan_mode_(ClimateFanMode mode) { | ||||||
|  |   return set_alternative(this->fan_mode, this->custom_fan_mode, mode); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool Climate::set_custom_fan_mode_(const std::string &mode) { | ||||||
|  |   return set_alternative(this->custom_fan_mode, this->fan_mode, mode); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } | ||||||
|  |  | ||||||
|  | bool Climate::set_custom_preset_(const std::string &preset) { | ||||||
|  |   return set_alternative(this->custom_preset, this->preset, preset); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Climate::dump_traits_(const char *tag) { | ||||||
|  |   auto traits = this->get_traits(); | ||||||
|  |   ESP_LOGCONFIG(tag, "ClimateTraits:"); | ||||||
|  |   ESP_LOGCONFIG(tag, "  [x] Visual settings:"); | ||||||
|  |   ESP_LOGCONFIG(tag, "      - Min: %.1f", traits.get_visual_min_temperature()); | ||||||
|  |   ESP_LOGCONFIG(tag, "      - Max: %.1f", traits.get_visual_max_temperature()); | ||||||
|  |   ESP_LOGCONFIG(tag, "      - Step: %.1f", traits.get_visual_temperature_step()); | ||||||
|  |   if (traits.get_supports_current_temperature()) | ||||||
|  |     ESP_LOGCONFIG(tag, "  [x] Supports current temperature"); | ||||||
|  |   if (traits.get_supports_two_point_target_temperature()) | ||||||
|  |     ESP_LOGCONFIG(tag, "  [x] Supports two-point target temperature"); | ||||||
|  |   if (traits.get_supports_action()) | ||||||
|  |     ESP_LOGCONFIG(tag, "  [x] Supports action"); | ||||||
|  |   if (!traits.get_supported_modes().empty()) { | ||||||
|  |     ESP_LOGCONFIG(tag, "  [x] Supported modes:"); | ||||||
|  |     for (ClimateMode m : traits.get_supported_modes()) | ||||||
|  |       ESP_LOGCONFIG(tag, "      - %s", climate_mode_to_string(m)); | ||||||
|  |   } | ||||||
|  |   if (!traits.get_supported_fan_modes().empty()) { | ||||||
|  |     ESP_LOGCONFIG(tag, "  [x] Supported fan modes:"); | ||||||
|  |     for (ClimateFanMode m : traits.get_supported_fan_modes()) | ||||||
|  |       ESP_LOGCONFIG(tag, "      - %s", climate_fan_mode_to_string(m)); | ||||||
|  |   } | ||||||
|  |   if (!traits.get_supported_custom_fan_modes().empty()) { | ||||||
|  |     ESP_LOGCONFIG(tag, "  [x] Supported custom fan modes:"); | ||||||
|  |     for (const std::string &s : traits.get_supported_custom_fan_modes()) | ||||||
|  |       ESP_LOGCONFIG(tag, "      - %s", s.c_str()); | ||||||
|  |   } | ||||||
|  |   if (!traits.get_supported_presets().empty()) { | ||||||
|  |     ESP_LOGCONFIG(tag, "  [x] Supported presets:"); | ||||||
|  |     for (ClimatePreset p : traits.get_supported_presets()) | ||||||
|  |       ESP_LOGCONFIG(tag, "      - %s", climate_preset_to_string(p)); | ||||||
|  |   } | ||||||
|  |   if (!traits.get_supported_custom_presets().empty()) { | ||||||
|  |     ESP_LOGCONFIG(tag, "  [x] Supported custom presets:"); | ||||||
|  |     for (const std::string &s : traits.get_supported_custom_presets()) | ||||||
|  |       ESP_LOGCONFIG(tag, "      - %s", s.c_str()); | ||||||
|  |   } | ||||||
|  |   if (!traits.get_supported_swing_modes().empty()) { | ||||||
|  |     ESP_LOGCONFIG(tag, "  [x] Supported swing modes:"); | ||||||
|  |     for (ClimateSwingMode m : traits.get_supported_swing_modes()) | ||||||
|  |       ESP_LOGCONFIG(tag, "      - %s", climate_swing_mode_to_string(m)); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| }  // namespace climate | }  // namespace climate | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -245,6 +245,18 @@ class Climate : public Nameable { | |||||||
|  protected: |  protected: | ||||||
|   friend ClimateCall; |   friend ClimateCall; | ||||||
|  |  | ||||||
|  |   /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed. | ||||||
|  |   bool set_fan_mode_(ClimateFanMode mode); | ||||||
|  |  | ||||||
|  |   /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. | ||||||
|  |   bool set_custom_fan_mode_(const std::string &mode); | ||||||
|  |  | ||||||
|  |   /// Set preset. Reset custom preset. Return true if preset has been changed. | ||||||
|  |   bool set_preset_(ClimatePreset preset); | ||||||
|  |  | ||||||
|  |   /// Set custom preset. Reset primary preset. Return true if preset has been changed. | ||||||
|  |   bool set_custom_preset_(const std::string &preset); | ||||||
|  |  | ||||||
|   /** Get the default traits of this climate device. |   /** Get the default traits of this climate device. | ||||||
|    * |    * | ||||||
|    * Traits are static data that encode the capabilities and static data for a climate device such as supported |    * Traits are static data that encode the capabilities and static data for a climate device such as supported | ||||||
| @@ -270,6 +282,7 @@ class Climate : public Nameable { | |||||||
|   void save_state_(); |   void save_state_(); | ||||||
|  |  | ||||||
|   uint32_t hash_base() override; |   uint32_t hash_base() override; | ||||||
|  |   void dump_traits_(const char *tag); | ||||||
|  |  | ||||||
|   CallbackManager<void()> state_callback_{}; |   CallbackManager<void()> state_callback_{}; | ||||||
|   ESPPreferenceObject rtc_; |   ESPPreferenceObject rtc_; | ||||||
|   | |||||||
| @@ -72,6 +72,7 @@ class ClimateTraits { | |||||||
|  |  | ||||||
|   void set_supported_fan_modes(std::set<ClimateFanMode> modes) { supported_fan_modes_ = std::move(modes); } |   void set_supported_fan_modes(std::set<ClimateFanMode> modes) { supported_fan_modes_ = std::move(modes); } | ||||||
|   void add_supported_fan_mode(ClimateFanMode mode) { supported_fan_modes_.insert(mode); } |   void add_supported_fan_mode(ClimateFanMode mode) { supported_fan_modes_.insert(mode); } | ||||||
|  |   void add_supported_custom_fan_mode(const std::string &mode) { supported_custom_fan_modes_.insert(mode); } | ||||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") |   ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") | ||||||
|   void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); } |   void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); } | ||||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") |   ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") | ||||||
| @@ -104,6 +105,7 @@ class ClimateTraits { | |||||||
|  |  | ||||||
|   void set_supported_presets(std::set<ClimatePreset> presets) { supported_presets_ = std::move(presets); } |   void set_supported_presets(std::set<ClimatePreset> presets) { supported_presets_ = std::move(presets); } | ||||||
|   void add_supported_preset(ClimatePreset preset) { supported_presets_.insert(preset); } |   void add_supported_preset(ClimatePreset preset) { supported_presets_.insert(preset); } | ||||||
|  |   void add_supported_custom_preset(const std::string &preset) { supported_custom_presets_.insert(preset); } | ||||||
|   bool supports_preset(ClimatePreset preset) const { return supported_presets_.count(preset); } |   bool supports_preset(ClimatePreset preset) const { return supported_presets_.count(preset); } | ||||||
|   bool get_supports_presets() const { return !supported_presets_.empty(); } |   bool get_supports_presets() const { return !supported_presets_.empty(); } | ||||||
|   const std::set<climate::ClimatePreset> &get_supported_presets() const { return supported_presets_; } |   const std::set<climate::ClimatePreset> &get_supported_presets() const { return supported_presets_; } | ||||||
|   | |||||||
| @@ -125,16 +125,19 @@ class Cover : public Nameable { | |||||||
|    * |    * | ||||||
|    * This is a legacy method and may be removed later, please use `.make_call()` instead. |    * This is a legacy method and may be removed later, please use `.make_call()` instead. | ||||||
|    */ |    */ | ||||||
|  |   ESPDEPRECATED("open() is deprecated, use make_call().set_command_open() instead.", "2021.9") | ||||||
|   void open(); |   void open(); | ||||||
|   /** Close the cover. |   /** Close the cover. | ||||||
|    * |    * | ||||||
|    * This is a legacy method and may be removed later, please use `.make_call()` instead. |    * This is a legacy method and may be removed later, please use `.make_call()` instead. | ||||||
|    */ |    */ | ||||||
|  |   ESPDEPRECATED("close() is deprecated, use make_call().set_command_close() instead.", "2021.9") | ||||||
|   void close(); |   void close(); | ||||||
|   /** Stop the cover. |   /** Stop the cover. | ||||||
|    * |    * | ||||||
|    * This is a legacy method and may be removed later, please use `.make_call()` instead. |    * This is a legacy method and may be removed later, please use `.make_call()` instead. | ||||||
|    */ |    */ | ||||||
|  |   ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop() instead.", "2021.9") | ||||||
|   void stop(); |   void stop(); | ||||||
|  |  | ||||||
|   void add_on_state_callback(std::function<void()> &&f); |   void add_on_state_callback(std::function<void()> &&f); | ||||||
|   | |||||||
| @@ -19,7 +19,6 @@ from esphome.const import ( | |||||||
|     CONF_ICON, |     CONF_ICON, | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     CONF_INVERTED, |     CONF_INVERTED, | ||||||
|     CONF_LAST_RESET_TYPE, |  | ||||||
|     CONF_MAX_VALUE, |     CONF_MAX_VALUE, | ||||||
|     CONF_MIN_VALUE, |     CONF_MIN_VALUE, | ||||||
|     CONF_NAME, |     CONF_NAME, | ||||||
| @@ -40,8 +39,8 @@ from esphome.const import ( | |||||||
|     ICON_BLUR, |     ICON_BLUR, | ||||||
|     ICON_EMPTY, |     ICON_EMPTY, | ||||||
|     ICON_THERMOMETER, |     ICON_THERMOMETER, | ||||||
|     LAST_RESET_TYPE_AUTO, |  | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|  |     STATE_CLASS_TOTAL_INCREASING, | ||||||
|     UNIT_CELSIUS, |     UNIT_CELSIUS, | ||||||
|     UNIT_EMPTY, |     UNIT_EMPTY, | ||||||
|     UNIT_PERCENT, |     UNIT_PERCENT, | ||||||
| @@ -336,8 +335,7 @@ CONFIG_SCHEMA = cv.Schema( | |||||||
|                     CONF_UNIT_OF_MEASUREMENT: UNIT_WATT_HOURS, |                     CONF_UNIT_OF_MEASUREMENT: UNIT_WATT_HOURS, | ||||||
|                     CONF_ACCURACY_DECIMALS: 0, |                     CONF_ACCURACY_DECIMALS: 0, | ||||||
|                     CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, |                     CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, | ||||||
|                     CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, |                     CONF_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, | ||||||
|                     CONF_LAST_RESET_TYPE: LAST_RESET_TYPE_AUTO, |  | ||||||
|                 }, |                 }, | ||||||
|             ], |             ], | ||||||
|         ): [ |         ): [ | ||||||
|   | |||||||
| @@ -11,8 +11,8 @@ class DemoSensor : public sensor::Sensor, public PollingComponent { | |||||||
|  public: |  public: | ||||||
|   void update() override { |   void update() override { | ||||||
|     float val = random_float(); |     float val = random_float(); | ||||||
|     bool is_auto = this->last_reset_type == sensor::LAST_RESET_TYPE_AUTO; |     bool increasing = this->state_class == sensor::STATE_CLASS_TOTAL_INCREASING; | ||||||
|     if (is_auto) { |     if (increasing) { | ||||||
|       float base = isnan(this->state) ? 0.0f : this->state; |       float base = isnan(this->state) ? 0.0f : this->state; | ||||||
|       this->publish_state(base + val * 10); |       this->publish_state(base + val * 10); | ||||||
|     } else { |     } else { | ||||||
|   | |||||||
| @@ -9,13 +9,17 @@ from esphome.const import ( | |||||||
|     DEVICE_CLASS_POWER, |     DEVICE_CLASS_POWER, | ||||||
|     DEVICE_CLASS_VOLTAGE, |     DEVICE_CLASS_VOLTAGE, | ||||||
|     ICON_EMPTY, |     ICON_EMPTY, | ||||||
|     LAST_RESET_TYPE_NEVER, |  | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|     STATE_CLASS_NONE, |     STATE_CLASS_NONE, | ||||||
|  |     STATE_CLASS_TOTAL_INCREASING, | ||||||
|     UNIT_AMPERE, |     UNIT_AMPERE, | ||||||
|  |     UNIT_CUBIC_METER, | ||||||
|     UNIT_EMPTY, |     UNIT_EMPTY, | ||||||
|  |     UNIT_KILOWATT, | ||||||
|  |     UNIT_KILOWATT_HOURS, | ||||||
|  |     UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, | ||||||
|  |     UNIT_KILOVOLT_AMPS_REACTIVE, | ||||||
|     UNIT_VOLT, |     UNIT_VOLT, | ||||||
|     UNIT_WATT, |  | ||||||
| ) | ) | ||||||
| from . import Dsmr, CONF_DSMR_ID | from . import Dsmr, CONF_DSMR_ID | ||||||
|  |  | ||||||
| @@ -26,70 +30,80 @@ CONFIG_SCHEMA = cv.Schema( | |||||||
|     { |     { | ||||||
|         cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr), |         cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr), | ||||||
|         cv.Optional("energy_delivered_lux"): sensor.sensor_schema( |         cv.Optional("energy_delivered_lux"): sensor.sensor_schema( | ||||||
|             "kWh", |             UNIT_KILOWATT_HOURS, | ||||||
|             ICON_EMPTY, |             ICON_EMPTY, | ||||||
|             3, |             3, | ||||||
|             DEVICE_CLASS_ENERGY, |             DEVICE_CLASS_ENERGY, | ||||||
|             STATE_CLASS_MEASUREMENT, |             STATE_CLASS_TOTAL_INCREASING, | ||||||
|             LAST_RESET_TYPE_NEVER, |  | ||||||
|         ), |         ), | ||||||
|         cv.Optional("energy_delivered_tariff1"): sensor.sensor_schema( |         cv.Optional("energy_delivered_tariff1"): sensor.sensor_schema( | ||||||
|             "kWh", |             UNIT_KILOWATT_HOURS, | ||||||
|             ICON_EMPTY, |             ICON_EMPTY, | ||||||
|             3, |             3, | ||||||
|             DEVICE_CLASS_ENERGY, |             DEVICE_CLASS_ENERGY, | ||||||
|             STATE_CLASS_MEASUREMENT, |             STATE_CLASS_TOTAL_INCREASING, | ||||||
|             LAST_RESET_TYPE_NEVER, |  | ||||||
|         ), |         ), | ||||||
|         cv.Optional("energy_delivered_tariff2"): sensor.sensor_schema( |         cv.Optional("energy_delivered_tariff2"): sensor.sensor_schema( | ||||||
|             "kWh", |             UNIT_KILOWATT_HOURS, | ||||||
|             ICON_EMPTY, |             ICON_EMPTY, | ||||||
|             3, |             3, | ||||||
|             DEVICE_CLASS_ENERGY, |             DEVICE_CLASS_ENERGY, | ||||||
|             STATE_CLASS_MEASUREMENT, |             STATE_CLASS_TOTAL_INCREASING, | ||||||
|             LAST_RESET_TYPE_NEVER, |  | ||||||
|         ), |         ), | ||||||
|         cv.Optional("energy_returned_lux"): sensor.sensor_schema( |         cv.Optional("energy_returned_lux"): sensor.sensor_schema( | ||||||
|             "kWh", |             UNIT_KILOWATT_HOURS, | ||||||
|             ICON_EMPTY, |             ICON_EMPTY, | ||||||
|             3, |             3, | ||||||
|             DEVICE_CLASS_ENERGY, |             DEVICE_CLASS_ENERGY, | ||||||
|             STATE_CLASS_MEASUREMENT, |             STATE_CLASS_TOTAL_INCREASING, | ||||||
|             LAST_RESET_TYPE_NEVER, |  | ||||||
|         ), |         ), | ||||||
|         cv.Optional("energy_returned_tariff1"): sensor.sensor_schema( |         cv.Optional("energy_returned_tariff1"): sensor.sensor_schema( | ||||||
|             "kWh", |             UNIT_KILOWATT_HOURS, | ||||||
|             ICON_EMPTY, |             ICON_EMPTY, | ||||||
|             3, |             3, | ||||||
|             DEVICE_CLASS_ENERGY, |             DEVICE_CLASS_ENERGY, | ||||||
|             STATE_CLASS_MEASUREMENT, |             STATE_CLASS_TOTAL_INCREASING, | ||||||
|             LAST_RESET_TYPE_NEVER, |  | ||||||
|         ), |         ), | ||||||
|         cv.Optional("energy_returned_tariff2"): sensor.sensor_schema( |         cv.Optional("energy_returned_tariff2"): sensor.sensor_schema( | ||||||
|             "kWh", |             UNIT_KILOWATT_HOURS, | ||||||
|             ICON_EMPTY, |             ICON_EMPTY, | ||||||
|             3, |             3, | ||||||
|             DEVICE_CLASS_ENERGY, |             DEVICE_CLASS_ENERGY, | ||||||
|             STATE_CLASS_MEASUREMENT, |             STATE_CLASS_TOTAL_INCREASING, | ||||||
|             LAST_RESET_TYPE_NEVER, |  | ||||||
|         ), |         ), | ||||||
|         cv.Optional("total_imported_energy"): sensor.sensor_schema( |         cv.Optional("total_imported_energy"): sensor.sensor_schema( | ||||||
|             "kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE |             UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, | ||||||
|  |             ICON_EMPTY, | ||||||
|  |             3, | ||||||
|  |             DEVICE_CLASS_ENERGY, | ||||||
|  |             STATE_CLASS_NONE, | ||||||
|         ), |         ), | ||||||
|         cv.Optional("total_exported_energy"): sensor.sensor_schema( |         cv.Optional("total_exported_energy"): sensor.sensor_schema( | ||||||
|             "kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE |             UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, | ||||||
|  |             ICON_EMPTY, | ||||||
|  |             3, | ||||||
|  |             DEVICE_CLASS_ENERGY, | ||||||
|  |             STATE_CLASS_NONE, | ||||||
|         ), |         ), | ||||||
|         cv.Optional("power_delivered"): sensor.sensor_schema( |         cv.Optional("power_delivered"): sensor.sensor_schema( | ||||||
|             UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT |             UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT | ||||||
|         ), |         ), | ||||||
|         cv.Optional("power_returned"): sensor.sensor_schema( |         cv.Optional("power_returned"): sensor.sensor_schema( | ||||||
|             UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT |             UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT | ||||||
|         ), |         ), | ||||||
|         cv.Optional("reactive_power_delivered"): sensor.sensor_schema( |         cv.Optional("reactive_power_delivered"): sensor.sensor_schema( | ||||||
|             "kvar", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE |             UNIT_KILOVOLT_AMPS_REACTIVE, | ||||||
|  |             ICON_EMPTY, | ||||||
|  |             3, | ||||||
|  |             DEVICE_CLASS_POWER, | ||||||
|  |             STATE_CLASS_MEASUREMENT, | ||||||
|         ), |         ), | ||||||
|         cv.Optional("reactive_power_returned"): sensor.sensor_schema( |         cv.Optional("reactive_power_returned"): sensor.sensor_schema( | ||||||
|             "kvar", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_MEASUREMENT |             UNIT_KILOVOLT_AMPS_REACTIVE, | ||||||
|  |             ICON_EMPTY, | ||||||
|  |             3, | ||||||
|  |             DEVICE_CLASS_POWER, | ||||||
|  |             STATE_CLASS_MEASUREMENT, | ||||||
|         ), |         ), | ||||||
|         cv.Optional("electricity_threshold"): sensor.sensor_schema( |         cv.Optional("electricity_threshold"): sensor.sensor_schema( | ||||||
|             UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE |             UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE | ||||||
| @@ -107,13 +121,13 @@ CONFIG_SCHEMA = cv.Schema( | |||||||
|             UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE |             UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE | ||||||
|         ), |         ), | ||||||
|         cv.Optional("electricity_sags_l2"): sensor.sensor_schema( |         cv.Optional("electricity_sags_l2"): sensor.sensor_schema( | ||||||
|             UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT |             UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE | ||||||
|         ), |         ), | ||||||
|         cv.Optional("electricity_sags_l3"): sensor.sensor_schema( |         cv.Optional("electricity_sags_l3"): sensor.sensor_schema( | ||||||
|             UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE |             UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE | ||||||
|         ), |         ), | ||||||
|         cv.Optional("electricity_swells_l1"): sensor.sensor_schema( |         cv.Optional("electricity_swells_l1"): sensor.sensor_schema( | ||||||
|             UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT |             UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE | ||||||
|         ), |         ), | ||||||
|         cv.Optional("electricity_swells_l2"): sensor.sensor_schema( |         cv.Optional("electricity_swells_l2"): sensor.sensor_schema( | ||||||
|             UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE |             UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE | ||||||
| @@ -131,40 +145,64 @@ CONFIG_SCHEMA = cv.Schema( | |||||||
|             UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT |             UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT | ||||||
|         ), |         ), | ||||||
|         cv.Optional("power_delivered_l1"): sensor.sensor_schema( |         cv.Optional("power_delivered_l1"): sensor.sensor_schema( | ||||||
|             UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT |             UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT | ||||||
|         ), |         ), | ||||||
|         cv.Optional("power_delivered_l2"): sensor.sensor_schema( |         cv.Optional("power_delivered_l2"): sensor.sensor_schema( | ||||||
|             UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT |             UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT | ||||||
|         ), |         ), | ||||||
|         cv.Optional("power_delivered_l3"): sensor.sensor_schema( |         cv.Optional("power_delivered_l3"): sensor.sensor_schema( | ||||||
|             UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT |             UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT | ||||||
|         ), |         ), | ||||||
|         cv.Optional("power_returned_l1"): sensor.sensor_schema( |         cv.Optional("power_returned_l1"): sensor.sensor_schema( | ||||||
|             UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT |             UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT | ||||||
|         ), |         ), | ||||||
|         cv.Optional("power_returned_l2"): sensor.sensor_schema( |         cv.Optional("power_returned_l2"): sensor.sensor_schema( | ||||||
|             UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT |             UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT | ||||||
|         ), |         ), | ||||||
|         cv.Optional("power_returned_l3"): sensor.sensor_schema( |         cv.Optional("power_returned_l3"): sensor.sensor_schema( | ||||||
|             UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT |             UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT | ||||||
|         ), |         ), | ||||||
|         cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema( |         cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema( | ||||||
|             UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT |             UNIT_KILOVOLT_AMPS_REACTIVE, | ||||||
|  |             ICON_EMPTY, | ||||||
|  |             3, | ||||||
|  |             DEVICE_CLASS_POWER, | ||||||
|  |             STATE_CLASS_MEASUREMENT, | ||||||
|         ), |         ), | ||||||
|         cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema( |         cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema( | ||||||
|             UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT |             UNIT_KILOVOLT_AMPS_REACTIVE, | ||||||
|  |             ICON_EMPTY, | ||||||
|  |             3, | ||||||
|  |             DEVICE_CLASS_POWER, | ||||||
|  |             STATE_CLASS_MEASUREMENT, | ||||||
|         ), |         ), | ||||||
|         cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema( |         cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema( | ||||||
|             UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT |             UNIT_KILOVOLT_AMPS_REACTIVE, | ||||||
|  |             ICON_EMPTY, | ||||||
|  |             3, | ||||||
|  |             DEVICE_CLASS_POWER, | ||||||
|  |             STATE_CLASS_MEASUREMENT, | ||||||
|         ), |         ), | ||||||
|         cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema( |         cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema( | ||||||
|             UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT |             UNIT_KILOVOLT_AMPS_REACTIVE, | ||||||
|  |             ICON_EMPTY, | ||||||
|  |             3, | ||||||
|  |             DEVICE_CLASS_POWER, | ||||||
|  |             STATE_CLASS_MEASUREMENT, | ||||||
|         ), |         ), | ||||||
|         cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema( |         cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema( | ||||||
|             UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT |             UNIT_KILOVOLT_AMPS_REACTIVE, | ||||||
|  |             ICON_EMPTY, | ||||||
|  |             3, | ||||||
|  |             DEVICE_CLASS_POWER, | ||||||
|  |             STATE_CLASS_MEASUREMENT, | ||||||
|         ), |         ), | ||||||
|         cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema( |         cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema( | ||||||
|             UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT |             UNIT_KILOVOLT_AMPS_REACTIVE, | ||||||
|  |             ICON_EMPTY, | ||||||
|  |             3, | ||||||
|  |             DEVICE_CLASS_POWER, | ||||||
|  |             STATE_CLASS_MEASUREMENT, | ||||||
|         ), |         ), | ||||||
|         cv.Optional("voltage_l1"): sensor.sensor_schema( |         cv.Optional("voltage_l1"): sensor.sensor_schema( | ||||||
|             UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE |             UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE | ||||||
| @@ -176,20 +214,18 @@ CONFIG_SCHEMA = cv.Schema( | |||||||
|             UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE |             UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE | ||||||
|         ), |         ), | ||||||
|         cv.Optional("gas_delivered"): sensor.sensor_schema( |         cv.Optional("gas_delivered"): sensor.sensor_schema( | ||||||
|             "m³", |             UNIT_CUBIC_METER, | ||||||
|             ICON_EMPTY, |             ICON_EMPTY, | ||||||
|             3, |             3, | ||||||
|             DEVICE_CLASS_GAS, |             DEVICE_CLASS_GAS, | ||||||
|             STATE_CLASS_MEASUREMENT, |             STATE_CLASS_TOTAL_INCREASING, | ||||||
|             LAST_RESET_TYPE_NEVER, |  | ||||||
|         ), |         ), | ||||||
|         cv.Optional("gas_delivered_be"): sensor.sensor_schema( |         cv.Optional("gas_delivered_be"): sensor.sensor_schema( | ||||||
|             "m³", |             UNIT_CUBIC_METER, | ||||||
|             ICON_EMPTY, |             ICON_EMPTY, | ||||||
|             3, |             3, | ||||||
|             DEVICE_CLASS_GAS, |             DEVICE_CLASS_GAS, | ||||||
|             STATE_CLASS_MEASUREMENT, |             STATE_CLASS_TOTAL_INCREASING, | ||||||
|             LAST_RESET_TYPE_NEVER, |  | ||||||
|         ), |         ), | ||||||
|     } |     } | ||||||
| ).extend(cv.COMPONENT_SCHEMA) | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|   | |||||||
| @@ -84,6 +84,7 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet | |||||||
|       break; |       break; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   it->schedule_show(); | ||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -82,11 +82,9 @@ bool BLEServer::create_device_characteristics_() { | |||||||
|         this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); |         this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); | ||||||
|     model->set_value(this->model_.value()); |     model->set_value(this->model_.value()); | ||||||
|   } else { |   } else { | ||||||
| #ifdef ARDUINO_BOARD |  | ||||||
|     BLECharacteristic *model = |     BLECharacteristic *model = | ||||||
|         this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); |         this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); | ||||||
|     model->set_value(ARDUINO_BOARD); |     model->set_value(ESPHOME_BOARD); | ||||||
| #endif |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   BLECharacteristic *version = |   BLECharacteristic *version = | ||||||
|   | |||||||
| @@ -108,6 +108,16 @@ def as_hex(value): | |||||||
|  |  | ||||||
|  |  | ||||||
| def as_hex_array(value): | def as_hex_array(value): | ||||||
|  |     value = value.replace("-", "") | ||||||
|  |     cpp_array = [ | ||||||
|  |         f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)] | ||||||
|  |     ] | ||||||
|  |     return cg.RawExpression( | ||||||
|  |         "(uint8_t*)(const uint8_t[16]){{{}}}".format(",".join(cpp_array)) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def as_reversed_hex_array(value): | ||||||
|     value = value.replace("-", "") |     value = value.replace("-", "") | ||||||
|     cpp_array = [ |     cpp_array = [ | ||||||
|         f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)] |         f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)] | ||||||
| @@ -193,7 +203,7 @@ async def to_code(config): | |||||||
|         elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid32_format): |         elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid32_format): | ||||||
|             cg.add(trigger.set_service_uuid32(as_hex(conf[CONF_SERVICE_UUID]))) |             cg.add(trigger.set_service_uuid32(as_hex(conf[CONF_SERVICE_UUID]))) | ||||||
|         elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid128_format): |         elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid128_format): | ||||||
|             uuid128 = as_hex_array(conf[CONF_SERVICE_UUID]) |             uuid128 = as_reversed_hex_array(conf[CONF_SERVICE_UUID]) | ||||||
|             cg.add(trigger.set_service_uuid128(uuid128)) |             cg.add(trigger.set_service_uuid128(uuid128)) | ||||||
|         if CONF_MAC_ADDRESS in conf: |         if CONF_MAC_ADDRESS in conf: | ||||||
|             cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) |             cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) | ||||||
| @@ -205,7 +215,7 @@ async def to_code(config): | |||||||
|         elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid32_format): |         elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid32_format): | ||||||
|             cg.add(trigger.set_manufacturer_uuid32(as_hex(conf[CONF_MANUFACTURER_ID]))) |             cg.add(trigger.set_manufacturer_uuid32(as_hex(conf[CONF_MANUFACTURER_ID]))) | ||||||
|         elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid128_format): |         elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid128_format): | ||||||
|             uuid128 = as_hex_array(conf[CONF_MANUFACTURER_ID]) |             uuid128 = as_reversed_hex_array(conf[CONF_MANUFACTURER_ID]) | ||||||
|             cg.add(trigger.set_manufacturer_uuid128(uuid128)) |             cg.add(trigger.set_manufacturer_uuid128(uuid128)) | ||||||
|         if CONF_MAC_ADDRESS in conf: |         if CONF_MAC_ADDRESS in conf: | ||||||
|             cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) |             cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) | ||||||
|   | |||||||
| @@ -434,6 +434,14 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e | |||||||
|   } |   } | ||||||
|   for (auto &data : this->manufacturer_datas_) { |   for (auto &data : this->manufacturer_datas_) { | ||||||
|     ESP_LOGVV(TAG, "  Manufacturer data: %s", hexencode(data.data).c_str()); |     ESP_LOGVV(TAG, "  Manufacturer data: %s", hexencode(data.data).c_str()); | ||||||
|  |     if (this->get_ibeacon().has_value()) { | ||||||
|  |       auto ibeacon = this->get_ibeacon().value(); | ||||||
|  |       ESP_LOGVV(TAG, "    iBeacon data:"); | ||||||
|  |       ESP_LOGVV(TAG, "      UUID: %s", ibeacon.get_uuid().to_string().c_str()); | ||||||
|  |       ESP_LOGVV(TAG, "      Major: %u", ibeacon.get_major()); | ||||||
|  |       ESP_LOGVV(TAG, "      Minor: %u", ibeacon.get_minor()); | ||||||
|  |       ESP_LOGVV(TAG, "      TXPower: %d", ibeacon.get_signal_power()); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|   for (auto &data : this->service_datas_) { |   for (auto &data : this->service_datas_) { | ||||||
|     ESP_LOGVV(TAG, "  Service data:"); |     ESP_LOGVV(TAG, "  Service data:"); | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
| #include "esp8266_pwm.h" | #include "esp8266_pwm.h" | ||||||
|  | #include "esphome/core/macros.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
| #ifdef ARDUINO_ESP8266_RELEASE_2_3_0 | #if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0) | ||||||
| #error ESP8266 PWM requires at least arduino_core_version 2.4.0 | #error ESP8266 PWM requires at least arduino_version 2.4.0 | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #include <core_esp8266_waveform.h> | #include <core_esp8266_waveform.h> | ||||||
|   | |||||||
| @@ -1,13 +1,12 @@ | |||||||
| import re | import re | ||||||
| import logging | import logging | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| import subprocess |  | ||||||
| import hashlib |  | ||||||
| import datetime |  | ||||||
|  |  | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_COMPONENTS, |     CONF_COMPONENTS, | ||||||
|  |     CONF_REF, | ||||||
|  |     CONF_REFRESH, | ||||||
|     CONF_SOURCE, |     CONF_SOURCE, | ||||||
|     CONF_URL, |     CONF_URL, | ||||||
|     CONF_TYPE, |     CONF_TYPE, | ||||||
| @@ -15,7 +14,7 @@ from esphome.const import ( | |||||||
|     CONF_PATH, |     CONF_PATH, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE | from esphome.core import CORE | ||||||
| from esphome import loader | from esphome import git, loader | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -23,19 +22,11 @@ DOMAIN = CONF_EXTERNAL_COMPONENTS | |||||||
|  |  | ||||||
| TYPE_GIT = "git" | TYPE_GIT = "git" | ||||||
| TYPE_LOCAL = "local" | TYPE_LOCAL = "local" | ||||||
| CONF_REFRESH = "refresh" |  | ||||||
| CONF_REF = "ref" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_git_ref(value): |  | ||||||
|     if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None: |  | ||||||
|         raise cv.Invalid("Not a valid git ref") |  | ||||||
|     return value |  | ||||||
|  |  | ||||||
|  |  | ||||||
| GIT_SCHEMA = { | GIT_SCHEMA = { | ||||||
|     cv.Required(CONF_URL): cv.url, |     cv.Required(CONF_URL): cv.url, | ||||||
|     cv.Optional(CONF_REF): validate_git_ref, |     cv.Optional(CONF_REF): cv.git_ref, | ||||||
| } | } | ||||||
| LOCAL_SCHEMA = { | LOCAL_SCHEMA = { | ||||||
|     cv.Required(CONF_PATH): cv.directory, |     cv.Required(CONF_PATH): cv.directory, | ||||||
| @@ -68,14 +59,6 @@ def validate_source_shorthand(value): | |||||||
|     return SOURCE_SCHEMA(conf) |     return SOURCE_SCHEMA(conf) | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_refresh(value: str): |  | ||||||
|     if value.lower() == "always": |  | ||||||
|         return validate_refresh("0s") |  | ||||||
|     if value.lower() == "never": |  | ||||||
|         return validate_refresh("1000y") |  | ||||||
|     return cv.positive_time_period_seconds(value) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| SOURCE_SCHEMA = cv.Any( | SOURCE_SCHEMA = cv.Any( | ||||||
|     validate_source_shorthand, |     validate_source_shorthand, | ||||||
|     cv.typed_schema( |     cv.typed_schema( | ||||||
| @@ -90,7 +73,7 @@ SOURCE_SCHEMA = cv.Any( | |||||||
| CONFIG_SCHEMA = cv.ensure_list( | CONFIG_SCHEMA = cv.ensure_list( | ||||||
|     { |     { | ||||||
|         cv.Required(CONF_SOURCE): SOURCE_SCHEMA, |         cv.Required(CONF_SOURCE): SOURCE_SCHEMA, | ||||||
|         cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, validate_refresh), |         cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, cv.source_refresh), | ||||||
|         cv.Optional(CONF_COMPONENTS, default="all"): cv.Any( |         cv.Optional(CONF_COMPONENTS, default="all"): cv.Any( | ||||||
|             "all", cv.ensure_list(cv.string) |             "all", cv.ensure_list(cv.string) | ||||||
|         ), |         ), | ||||||
| @@ -102,65 +85,13 @@ async def to_code(config): | |||||||
|     pass |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| def _compute_destination_path(key: str) -> Path: |  | ||||||
|     base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN |  | ||||||
|     h = hashlib.new("sha256") |  | ||||||
|     h.update(key.encode()) |  | ||||||
|     return base_dir / h.hexdigest()[:8] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def _run_git_command(cmd, cwd=None): |  | ||||||
|     try: |  | ||||||
|         ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False) |  | ||||||
|     except FileNotFoundError as err: |  | ||||||
|         raise cv.Invalid( |  | ||||||
|             "git is not installed but required for external_components.\n" |  | ||||||
|             "Please see https://git-scm.com/book/en/v2/Getting-Started-Installing-Git for installing git" |  | ||||||
|         ) from err |  | ||||||
|  |  | ||||||
|     if ret.returncode != 0 and ret.stderr: |  | ||||||
|         err_str = ret.stderr.decode("utf-8") |  | ||||||
|         lines = [x.strip() for x in err_str.splitlines()] |  | ||||||
|         if lines[-1].startswith("fatal:"): |  | ||||||
|             raise cv.Invalid(lines[-1][len("fatal: ") :]) |  | ||||||
|         raise cv.Invalid(err_str) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def _process_git_config(config: dict, refresh) -> str: | def _process_git_config(config: dict, refresh) -> str: | ||||||
|     key = f"{config[CONF_URL]}@{config.get(CONF_REF)}" |     repo_dir = git.clone_or_update( | ||||||
|     repo_dir = _compute_destination_path(key) |         url=config[CONF_URL], | ||||||
|     if not repo_dir.is_dir(): |         ref=config.get(CONF_REF), | ||||||
|         _LOGGER.info("Cloning %s", key) |         refresh=refresh, | ||||||
|         _LOGGER.debug("Location: %s", repo_dir) |         domain=DOMAIN, | ||||||
|         cmd = ["git", "clone", "--depth=1"] |  | ||||||
|         if CONF_REF in config: |  | ||||||
|             cmd += ["--branch", config[CONF_REF]] |  | ||||||
|         cmd += ["--", config[CONF_URL], str(repo_dir)] |  | ||||||
|         _run_git_command(cmd) |  | ||||||
|  |  | ||||||
|     else: |  | ||||||
|         # Check refresh needed |  | ||||||
|         file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD") |  | ||||||
|         # On first clone, FETCH_HEAD does not exists |  | ||||||
|         if not file_timestamp.exists(): |  | ||||||
|             file_timestamp = Path(repo_dir / ".git" / "HEAD") |  | ||||||
|         age = datetime.datetime.now() - datetime.datetime.fromtimestamp( |  | ||||||
|             file_timestamp.stat().st_mtime |  | ||||||
|     ) |     ) | ||||||
|         if age.total_seconds() > refresh.total_seconds: |  | ||||||
|             _LOGGER.info("Updating %s", key) |  | ||||||
|             _LOGGER.debug("Location: %s", repo_dir) |  | ||||||
|             # Stash local changes (if any) |  | ||||||
|             _run_git_command( |  | ||||||
|                 ["git", "stash", "push", "--include-untracked"], str(repo_dir) |  | ||||||
|             ) |  | ||||||
|             # Fetch remote ref |  | ||||||
|             cmd = ["git", "fetch", "--", "origin"] |  | ||||||
|             if CONF_REF in config: |  | ||||||
|                 cmd.append(config[CONF_REF]) |  | ||||||
|             _run_git_command(cmd, str(repo_dir)) |  | ||||||
|             # Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch) |  | ||||||
|             _run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir)) |  | ||||||
|  |  | ||||||
|     if (repo_dir / "esphome" / "components").is_dir(): |     if (repo_dir / "esphome" / "components").is_dir(): | ||||||
|         components_dir = repo_dir / "esphome" / "components" |         components_dir = repo_dir / "esphome" / "components" | ||||||
|   | |||||||
| @@ -15,9 +15,11 @@ from esphome.const import ( | |||||||
|     CONF_SPEED_COMMAND_TOPIC, |     CONF_SPEED_COMMAND_TOPIC, | ||||||
|     CONF_SPEED_STATE_TOPIC, |     CONF_SPEED_STATE_TOPIC, | ||||||
|     CONF_NAME, |     CONF_NAME, | ||||||
|  |     CONF_ON_SPEED_SET, | ||||||
|     CONF_ON_TURN_OFF, |     CONF_ON_TURN_OFF, | ||||||
|     CONF_ON_TURN_ON, |     CONF_ON_TURN_ON, | ||||||
|     CONF_TRIGGER_ID, |     CONF_TRIGGER_ID, | ||||||
|  |     CONF_DIRECTION, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import CORE, coroutine_with_priority | ||||||
|  |  | ||||||
| @@ -27,6 +29,12 @@ fan_ns = cg.esphome_ns.namespace("fan") | |||||||
| FanState = fan_ns.class_("FanState", cg.Nameable, cg.Component) | FanState = fan_ns.class_("FanState", cg.Nameable, cg.Component) | ||||||
| MakeFan = cg.Application.struct("MakeFan") | MakeFan = cg.Application.struct("MakeFan") | ||||||
|  |  | ||||||
|  | FanDirection = fan_ns.enum("FanDirection") | ||||||
|  | FAN_DIRECTION_ENUM = { | ||||||
|  |     "FORWARD": FanDirection.FAN_DIRECTION_FORWARD, | ||||||
|  |     "REVERSE": FanDirection.FAN_DIRECTION_REVERSE, | ||||||
|  | } | ||||||
|  |  | ||||||
| # Actions | # Actions | ||||||
| TurnOnAction = fan_ns.class_("TurnOnAction", automation.Action) | TurnOnAction = fan_ns.class_("TurnOnAction", automation.Action) | ||||||
| TurnOffAction = fan_ns.class_("TurnOffAction", automation.Action) | TurnOffAction = fan_ns.class_("TurnOffAction", automation.Action) | ||||||
| @@ -34,6 +42,10 @@ ToggleAction = fan_ns.class_("ToggleAction", automation.Action) | |||||||
|  |  | ||||||
| FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template()) | FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template()) | ||||||
| FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template()) | FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template()) | ||||||
|  | FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template()) | ||||||
|  |  | ||||||
|  | FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) | ||||||
|  | FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template()) | ||||||
|  |  | ||||||
| FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( | FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( | ||||||
|     { |     { | ||||||
| @@ -61,6 +73,11 @@ FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( | |||||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOffTrigger), |                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOffTrigger), | ||||||
|             } |             } | ||||||
|         ), |         ), | ||||||
|  |         cv.Optional(CONF_ON_SPEED_SET): automation.validate_automation( | ||||||
|  |             { | ||||||
|  |                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger), | ||||||
|  |             } | ||||||
|  |         ), | ||||||
|     } |     } | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -100,6 +117,9 @@ async def setup_fan_core_(var, config): | |||||||
|     for conf in config.get(CONF_ON_TURN_OFF, []): |     for conf in config.get(CONF_ON_TURN_OFF, []): | ||||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|         await automation.build_automation(trigger, [], conf) |         await automation.build_automation(trigger, [], conf) | ||||||
|  |     for conf in config.get(CONF_ON_SPEED_SET, []): | ||||||
|  |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|  |         await automation.build_automation(trigger, [], conf) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def register_fan(var, config): | async def register_fan(var, config): | ||||||
| @@ -143,6 +163,9 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args): | |||||||
|             cv.Required(CONF_ID): cv.use_id(FanState), |             cv.Required(CONF_ID): cv.use_id(FanState), | ||||||
|             cv.Optional(CONF_OSCILLATING): cv.templatable(cv.boolean), |             cv.Optional(CONF_OSCILLATING): cv.templatable(cv.boolean), | ||||||
|             cv.Optional(CONF_SPEED): cv.templatable(cv.int_range(1)), |             cv.Optional(CONF_SPEED): cv.templatable(cv.int_range(1)), | ||||||
|  |             cv.Optional(CONF_DIRECTION): cv.templatable( | ||||||
|  |                 cv.enum(FAN_DIRECTION_ENUM, upper=True) | ||||||
|  |             ), | ||||||
|         } |         } | ||||||
|     ), |     ), | ||||||
| ) | ) | ||||||
| @@ -155,9 +178,35 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args): | |||||||
|     if CONF_SPEED in config: |     if CONF_SPEED in config: | ||||||
|         template_ = await cg.templatable(config[CONF_SPEED], args, int) |         template_ = await cg.templatable(config[CONF_SPEED], args, int) | ||||||
|         cg.add(var.set_speed(template_)) |         cg.add(var.set_speed(template_)) | ||||||
|  |     if CONF_DIRECTION in config: | ||||||
|  |         template_ = await cg.templatable(config[CONF_DIRECTION], args, FanDirection) | ||||||
|  |         cg.add(var.set_direction(template_)) | ||||||
|     return var |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_condition( | ||||||
|  |     "fan.is_on", | ||||||
|  |     FanIsOnCondition, | ||||||
|  |     automation.maybe_simple_id( | ||||||
|  |         { | ||||||
|  |             cv.Required(CONF_ID): cv.use_id(FanState), | ||||||
|  |         } | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  | @automation.register_condition( | ||||||
|  |     "fan.is_off", | ||||||
|  |     FanIsOffCondition, | ||||||
|  |     automation.maybe_simple_id( | ||||||
|  |         { | ||||||
|  |             cv.Required(CONF_ID): cv.use_id(FanState), | ||||||
|  |         } | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  | async def fan_is_on_off_to_code(config, condition_id, template_arg, args): | ||||||
|  |     paren = await cg.get_variable(config[CONF_ID]) | ||||||
|  |     return cg.new_Pvariable(condition_id, template_arg, paren) | ||||||
|  |  | ||||||
|  |  | ||||||
| @coroutine_with_priority(100.0) | @coroutine_with_priority(100.0) | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     cg.add_define("USE_FAN") |     cg.add_define("USE_FAN") | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ template<typename... Ts> class TurnOnAction : public Action<Ts...> { | |||||||
|  |  | ||||||
|   TEMPLATABLE_VALUE(bool, oscillating) |   TEMPLATABLE_VALUE(bool, oscillating) | ||||||
|   TEMPLATABLE_VALUE(int, speed) |   TEMPLATABLE_VALUE(int, speed) | ||||||
|  |   TEMPLATABLE_VALUE(FanDirection, direction) | ||||||
|  |  | ||||||
|   void play(Ts... x) override { |   void play(Ts... x) override { | ||||||
|     auto call = this->state_->turn_on(); |     auto call = this->state_->turn_on(); | ||||||
| @@ -22,6 +23,9 @@ template<typename... Ts> class TurnOnAction : public Action<Ts...> { | |||||||
|     if (this->speed_.has_value()) { |     if (this->speed_.has_value()) { | ||||||
|       call.set_speed(this->speed_.value(x...)); |       call.set_speed(this->speed_.value(x...)); | ||||||
|     } |     } | ||||||
|  |     if (this->direction_.has_value()) { | ||||||
|  |       call.set_direction(this->direction_.value(x...)); | ||||||
|  |     } | ||||||
|     call.perform(); |     call.perform(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -46,6 +50,23 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> { | |||||||
|   FanState *state_; |   FanState *state_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class FanIsOnCondition : public Condition<Ts...> { | ||||||
|  |  public: | ||||||
|  |   explicit FanIsOnCondition(FanState *state) : state_(state) {} | ||||||
|  |   bool check(Ts... x) override { return this->state_->state; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   FanState *state_; | ||||||
|  | }; | ||||||
|  | template<typename... Ts> class FanIsOffCondition : public Condition<Ts...> { | ||||||
|  |  public: | ||||||
|  |   explicit FanIsOffCondition(FanState *state) : state_(state) {} | ||||||
|  |   bool check(Ts... x) override { return !this->state_->state; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   FanState *state_; | ||||||
|  | }; | ||||||
|  |  | ||||||
| class FanTurnOnTrigger : public Trigger<> { | class FanTurnOnTrigger : public Trigger<> { | ||||||
|  public: |  public: | ||||||
|   FanTurnOnTrigger(FanState *state) { |   FanTurnOnTrigger(FanState *state) { | ||||||
| @@ -82,5 +103,23 @@ class FanTurnOffTrigger : public Trigger<> { | |||||||
|   bool last_on_; |   bool last_on_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | class FanSpeedSetTrigger : public Trigger<> { | ||||||
|  |  public: | ||||||
|  |   FanSpeedSetTrigger(FanState *state) { | ||||||
|  |     state->add_on_state_callback([this, state]() { | ||||||
|  |       auto speed = state->speed; | ||||||
|  |       auto should_trigger = speed != !this->last_speed_; | ||||||
|  |       this->last_speed_ = speed; | ||||||
|  |       if (should_trigger) { | ||||||
|  |         this->trigger(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     this->last_speed_ = state->speed; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   int last_speed_; | ||||||
|  | }; | ||||||
|  |  | ||||||
| }  // namespace fan | }  // namespace fan | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -4,6 +4,9 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace fan { | namespace fan { | ||||||
|  |  | ||||||
|  | // This whole file is deprecated, don't warn about usage of deprecated types in here. | ||||||
|  | #pragma GCC diagnostic ignored "-Wdeprecated-declarations" | ||||||
|  |  | ||||||
| FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) { | 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); | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -39,7 +39,7 @@ void FanState::setup() { | |||||||
|   call.set_direction(recovered.direction); |   call.set_direction(recovered.direction); | ||||||
|   call.perform(); |   call.perform(); | ||||||
| } | } | ||||||
| float FanState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } | float FanState::get_setup_priority() const { return setup_priority::DATA - 1.0f; } | ||||||
| uint32_t FanState::hash_base() { return 418001110UL; } | uint32_t FanState::hash_base() { return 418001110UL; } | ||||||
|  |  | ||||||
| void FanStateCall::perform() const { | void FanStateCall::perform() const { | ||||||
| @@ -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) { | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ namespace esphome { | |||||||
| namespace fan { | namespace fan { | ||||||
|  |  | ||||||
| /// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon | /// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon | ||||||
| enum FanSpeed { | enum ESPDEPRECATED("FanSpeed is deprecated.", "2021.9") FanSpeed { | ||||||
|   FAN_SPEED_LOW = 0,     ///< The fan is running on low speed. |   FAN_SPEED_LOW = 0,     ///< The fan is running on low speed. | ||||||
|   FAN_SPEED_MEDIUM = 1,  ///< The fan is running on medium speed. |   FAN_SPEED_MEDIUM = 1,  ///< The fan is running on medium speed. | ||||||
|   FAN_SPEED_HIGH = 2     ///< The fan is running on high/full speed. |   FAN_SPEED_HIGH = 2     ///< The fan is running on high/full speed. | ||||||
| @@ -45,6 +45,7 @@ class FanStateCall { | |||||||
|     this->speed_ = speed; |     this->speed_ = speed; | ||||||
|     return *this; |     return *this; | ||||||
|   } |   } | ||||||
|  |   ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9") | ||||||
|   FanStateCall &set_speed(const char *legacy_speed); |   FanStateCall &set_speed(const char *legacy_speed); | ||||||
|   FanStateCall &set_direction(FanDirection direction) { |   FanStateCall &set_direction(FanDirection direction) { | ||||||
|     this->direction_ = direction; |     this->direction_ = direction; | ||||||
|   | |||||||
| @@ -20,13 +20,12 @@ void FastLEDLightOutput::dump_config() { | |||||||
|   ESP_LOGCONFIG(TAG, "  Num LEDs: %u", this->num_leds_); |   ESP_LOGCONFIG(TAG, "  Num LEDs: %u", this->num_leds_); | ||||||
|   ESP_LOGCONFIG(TAG, "  Max refresh rate: %u", *this->max_refresh_rate_); |   ESP_LOGCONFIG(TAG, "  Max refresh rate: %u", *this->max_refresh_rate_); | ||||||
| } | } | ||||||
| void FastLEDLightOutput::loop() { | void FastLEDLightOutput::write_state(light::LightState *state) { | ||||||
|   if (!this->should_show_()) |  | ||||||
|     return; |  | ||||||
|  |  | ||||||
|   uint32_t now = micros(); |  | ||||||
|   // protect from refreshing too often |   // protect from refreshing too often | ||||||
|  |   uint32_t now = micros(); | ||||||
|   if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) { |   if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) { | ||||||
|  |     // try again next loop iteration, so that this change won't get lost | ||||||
|  |     this->schedule_show(); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   this->last_refresh_ = now; |   this->last_refresh_ = now; | ||||||
|   | |||||||
| @@ -213,7 +213,7 @@ class FastLEDLightOutput : public light::AddressableLight { | |||||||
|   } |   } | ||||||
|   void setup() override; |   void setup() override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void loop() override; |   void write_state(light::LightState *state) override; | ||||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } |   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||||
|  |  | ||||||
|   void clear_effect_data() override { |   void clear_effect_data() override { | ||||||
|   | |||||||
| @@ -297,12 +297,6 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Validate footer |  | ||||||
|   if (!data.expect_mark(FUJITSU_GENERAL_BIT_MARK)) { |  | ||||||
|     ESP_LOGV(TAG, "Footer fail"); |  | ||||||
|     return false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   for (uint8_t byte = 0; byte < recv_message_length; ++byte) { |   for (uint8_t byte = 0; byte < recv_message_length; ++byte) { | ||||||
|     ESP_LOGVV(TAG, "%02X", recv_message[byte]); |     ESP_LOGVV(TAG, "%02X", recv_message[byte]); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -13,9 +13,8 @@ from esphome.const import ( | |||||||
|     DEVICE_CLASS_POWER, |     DEVICE_CLASS_POWER, | ||||||
|     DEVICE_CLASS_VOLTAGE, |     DEVICE_CLASS_VOLTAGE, | ||||||
|     ICON_CURRENT_AC, |     ICON_CURRENT_AC, | ||||||
|     LAST_RESET_TYPE_AUTO, |  | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|     STATE_CLASS_NONE, |     STATE_CLASS_TOTAL_INCREASING, | ||||||
|     UNIT_AMPERE, |     UNIT_AMPERE, | ||||||
|     UNIT_DEGREES, |     UNIT_DEGREES, | ||||||
|     UNIT_HERTZ, |     UNIT_HERTZ, | ||||||
| @@ -143,25 +142,23 @@ CONFIG_SCHEMA = ( | |||||||
|                 unit_of_measurement=UNIT_KILOWATT_HOURS, |                 unit_of_measurement=UNIT_KILOWATT_HOURS, | ||||||
|                 accuracy_decimals=2, |                 accuracy_decimals=2, | ||||||
|                 device_class=DEVICE_CLASS_ENERGY, |                 device_class=DEVICE_CLASS_ENERGY, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |                 state_class=STATE_CLASS_TOTAL_INCREASING, | ||||||
|                 last_reset_type=LAST_RESET_TYPE_AUTO, |  | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema( |             cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema( | ||||||
|                 unit_of_measurement=UNIT_KILOWATT_HOURS, |                 unit_of_measurement=UNIT_KILOWATT_HOURS, | ||||||
|                 accuracy_decimals=0, |                 accuracy_decimals=0, | ||||||
|                 device_class=DEVICE_CLASS_ENERGY, |                 device_class=DEVICE_CLASS_ENERGY, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |                 state_class=STATE_CLASS_TOTAL_INCREASING, | ||||||
|                 last_reset_type=LAST_RESET_TYPE_AUTO, |  | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_TOTAL_GENERATION_TIME): sensor.sensor_schema( |             cv.Optional(CONF_TOTAL_GENERATION_TIME): sensor.sensor_schema( | ||||||
|                 unit_of_measurement=UNIT_HOURS, |                 unit_of_measurement=UNIT_HOURS, | ||||||
|                 accuracy_decimals=0, |                 accuracy_decimals=0, | ||||||
|                 state_class=STATE_CLASS_NONE, |                 state_class=STATE_CLASS_TOTAL_INCREASING, | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_TODAY_GENERATION_TIME): sensor.sensor_schema( |             cv.Optional(CONF_TODAY_GENERATION_TIME): sensor.sensor_schema( | ||||||
|                 unit_of_measurement=UNIT_MINUTE, |                 unit_of_measurement=UNIT_MINUTE, | ||||||
|                 accuracy_decimals=0, |                 accuracy_decimals=0, | ||||||
|                 state_class=STATE_CLASS_NONE, |                 state_class=STATE_CLASS_TOTAL_INCREASING, | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_INVERTER_MODULE_TEMP): sensor.sensor_schema( |             cv.Optional(CONF_INVERTER_MODULE_TEMP): sensor.sensor_schema( | ||||||
|                 unit_of_measurement=UNIT_DEGREES, |                 unit_of_measurement=UNIT_DEGREES, | ||||||
|   | |||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  |  | ||||||
|  | hbridge_ns = cg.esphome_ns.namespace("hbridge") | ||||||
|   | |||||||
							
								
								
									
										70
									
								
								esphome/components/hbridge/fan/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								esphome/components/hbridge/fan/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome import automation | ||||||
|  | from esphome.automation import maybe_simple_id | ||||||
|  | from esphome.components import fan, output | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_DECAY_MODE, | ||||||
|  |     CONF_SPEED_COUNT, | ||||||
|  |     CONF_PIN_A, | ||||||
|  |     CONF_PIN_B, | ||||||
|  |     CONF_ENABLE_PIN, | ||||||
|  | ) | ||||||
|  | from .. import hbridge_ns | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@WeekendWarrior"] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | HBridgeFan = hbridge_ns.class_("HBridgeFan", fan.FanState) | ||||||
|  |  | ||||||
|  | DecayMode = hbridge_ns.enum("DecayMode") | ||||||
|  | DECAY_MODE_OPTIONS = { | ||||||
|  |     "SLOW": DecayMode.DECAY_MODE_SLOW, | ||||||
|  |     "FAST": DecayMode.DECAY_MODE_FAST, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | # Actions | ||||||
|  | BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan), | ||||||
|  |         cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput), | ||||||
|  |         cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput), | ||||||
|  |         cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum( | ||||||
|  |             DECAY_MODE_OPTIONS, upper=True | ||||||
|  |         ), | ||||||
|  |         cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), | ||||||
|  |         cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), | ||||||
|  |     } | ||||||
|  | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action( | ||||||
|  |     "fan.hbridge.brake", | ||||||
|  |     BrakeAction, | ||||||
|  |     maybe_simple_id({cv.Required(CONF_ID): cv.use_id(HBridgeFan)}), | ||||||
|  | ) | ||||||
|  | async def fan_hbridge_brake_to_code(config, action_id, template_arg, args): | ||||||
|  |     paren = await cg.get_variable(config[CONF_ID]) | ||||||
|  |     return cg.new_Pvariable(action_id, template_arg, paren) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable( | ||||||
|  |         config[CONF_ID], | ||||||
|  |         config[CONF_SPEED_COUNT], | ||||||
|  |         config[CONF_DECAY_MODE], | ||||||
|  |     ) | ||||||
|  |     await fan.register_fan(var, config) | ||||||
|  |     pin_a_ = await cg.get_variable(config[CONF_PIN_A]) | ||||||
|  |     cg.add(var.set_pin_a(pin_a_)) | ||||||
|  |     pin_b_ = await cg.get_variable(config[CONF_PIN_B]) | ||||||
|  |     cg.add(var.set_pin_b(pin_b_)) | ||||||
|  |  | ||||||
|  |     if CONF_ENABLE_PIN in config: | ||||||
|  |         enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN]) | ||||||
|  |         cg.add(var.set_enable_pin(enable_pin)) | ||||||
							
								
								
									
										85
									
								
								esphome/components/hbridge/fan/hbridge_fan.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								esphome/components/hbridge/fan/hbridge_fan.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | #include "hbridge_fan.h" | ||||||
|  | #include "esphome/components/fan/fan_helpers.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace hbridge { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "fan.hbridge"; | ||||||
|  |  | ||||||
|  | void HBridgeFan::set_hbridge_levels_(float a_level, float b_level) { | ||||||
|  |   this->pin_a_->set_level(a_level); | ||||||
|  |   this->pin_b_->set_level(b_level); | ||||||
|  |   ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f", a_level, b_level); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // constant IN1/IN2, PWM on EN => power control, fast current decay | ||||||
|  | // constant IN1/EN, PWM on IN2 => power control, slow current decay | ||||||
|  | void HBridgeFan::set_hbridge_levels_(float a_level, float b_level, float enable) { | ||||||
|  |   this->pin_a_->set_level(a_level); | ||||||
|  |   this->pin_b_->set_level(b_level); | ||||||
|  |   this->enable_->set_level(enable); | ||||||
|  |   ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f, enable: %.2f", a_level, b_level, enable); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fan::FanStateCall HBridgeFan::brake() { | ||||||
|  |   ESP_LOGD(TAG, "Braking"); | ||||||
|  |   (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f) : this->set_hbridge_levels_(1.0f, 1.0f, 1.0f); | ||||||
|  |   return this->make_call().set_state(false); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void HBridgeFan::dump_config() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "Fan '%s':", this->get_name().c_str()); | ||||||
|  |   if (this->get_traits().supports_oscillation()) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "  Oscillation: YES"); | ||||||
|  |   } | ||||||
|  |   if (this->get_traits().supports_direction()) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "  Direction: YES"); | ||||||
|  |   } | ||||||
|  |   if (this->decay_mode_ == DECAY_MODE_SLOW) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "  Decay Mode: Slow"); | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGCONFIG(TAG, "  Decay Mode: Fast"); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | void HBridgeFan::setup() { | ||||||
|  |   auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); | ||||||
|  |   this->set_traits(traits); | ||||||
|  |   this->add_on_state_callback([this]() { this->next_update_ = true; }); | ||||||
|  | } | ||||||
|  | void HBridgeFan::loop() { | ||||||
|  |   if (!this->next_update_) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   this->next_update_ = false; | ||||||
|  |  | ||||||
|  |   float speed = 0.0f; | ||||||
|  |   if (this->state) { | ||||||
|  |     speed = static_cast<float>(this->speed) / static_cast<float>(this->speed_count_); | ||||||
|  |   } | ||||||
|  |   if (speed == 0.0f) {  // off means idle | ||||||
|  |     (this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, speed) | ||||||
|  |                                : this->set_hbridge_levels_(speed, speed, speed); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   if (this->direction == fan::FAN_DIRECTION_FORWARD) { | ||||||
|  |     if (this->decay_mode_ == DECAY_MODE_SLOW) { | ||||||
|  |       (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f - speed, 1.0f) | ||||||
|  |                                  : this->set_hbridge_levels_(1.0f - speed, 1.0f, 1.0f); | ||||||
|  |     } else {  // DECAY_MODE_FAST | ||||||
|  |       (this->enable_ == nullptr) ? this->set_hbridge_levels_(0.0f, speed) | ||||||
|  |                                  : this->set_hbridge_levels_(0.0f, 1.0f, speed); | ||||||
|  |     } | ||||||
|  |   } else {  // fan::FAN_DIRECTION_REVERSE | ||||||
|  |     if (this->decay_mode_ == DECAY_MODE_SLOW) { | ||||||
|  |       (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f - speed) | ||||||
|  |                                  : this->set_hbridge_levels_(1.0f, 1.0f - speed, 1.0f); | ||||||
|  |     } else {  // DECAY_MODE_FAST | ||||||
|  |       (this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, 0.0f) | ||||||
|  |                                  : this->set_hbridge_levels_(1.0f, 0.0f, speed); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace hbridge | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										58
									
								
								esphome/components/hbridge/fan/hbridge_fan.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								esphome/components/hbridge/fan/hbridge_fan.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/automation.h" | ||||||
|  | #include "esphome/components/output/binary_output.h" | ||||||
|  | #include "esphome/components/output/float_output.h" | ||||||
|  | #include "esphome/components/fan/fan_state.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace hbridge { | ||||||
|  |  | ||||||
|  | enum DecayMode { | ||||||
|  |   DECAY_MODE_SLOW = 0, | ||||||
|  |   DECAY_MODE_FAST = 1, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class HBridgeFan : public fan::FanState { | ||||||
|  |  public: | ||||||
|  |   HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {} | ||||||
|  |  | ||||||
|  |   void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } | ||||||
|  |   void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } | ||||||
|  |   void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } | ||||||
|  |  | ||||||
|  |   void setup() override; | ||||||
|  |   void loop() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||||
|  |  | ||||||
|  |   fan::FanStateCall brake(); | ||||||
|  |  | ||||||
|  |   int get_speed_count() { return this->speed_count_; } | ||||||
|  |   // update Hbridge without a triggered FanState change, eg. for acceleration/deceleration ramping | ||||||
|  |   void internal_update() { this->next_update_ = true; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   output::FloatOutput *pin_a_; | ||||||
|  |   output::FloatOutput *pin_b_; | ||||||
|  |   output::FloatOutput *enable_{nullptr}; | ||||||
|  |   output::BinaryOutput *oscillating_{nullptr}; | ||||||
|  |   bool next_update_{true}; | ||||||
|  |   int speed_count_{}; | ||||||
|  |   DecayMode decay_mode_{DECAY_MODE_SLOW}; | ||||||
|  |  | ||||||
|  |   void set_hbridge_levels_(float a_level, float b_level); | ||||||
|  |   void set_hbridge_levels_(float a_level, float b_level, float enable); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class BrakeAction : public Action<Ts...> { | ||||||
|  |  public: | ||||||
|  |   explicit BrakeAction(HBridgeFan *parent) : parent_(parent) {} | ||||||
|  |  | ||||||
|  |   void play(Ts... x) override { this->parent_->brake(); } | ||||||
|  |  | ||||||
|  |   HBridgeFan *parent_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace hbridge | ||||||
|  | }  // namespace esphome | ||||||
| @@ -2,8 +2,10 @@ import esphome.codegen as cg | |||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.components import light, output | from esphome.components import light, output | ||||||
| from esphome.const import CONF_OUTPUT_ID, CONF_PIN_A, CONF_PIN_B | from esphome.const import CONF_OUTPUT_ID, CONF_PIN_A, CONF_PIN_B | ||||||
|  | from .. import hbridge_ns | ||||||
|  | 
 | ||||||
|  | CODEOWNERS = ["@DotNetDann"] | ||||||
| 
 | 
 | ||||||
| hbridge_ns = cg.esphome_ns.namespace("hbridge") |  | ||||||
| HBridgeLightOutput = hbridge_ns.class_( | HBridgeLightOutput = hbridge_ns.class_( | ||||||
|     "HBridgeLightOutput", cg.PollingComponent, light.LightOutput |     "HBridgeLightOutput", cg.PollingComponent, light.LightOutput | ||||||
| ) | ) | ||||||
| @@ -18,8 +18,8 @@ from esphome.const import ( | |||||||
|     DEVICE_CLASS_ENERGY, |     DEVICE_CLASS_ENERGY, | ||||||
|     DEVICE_CLASS_POWER, |     DEVICE_CLASS_POWER, | ||||||
|     DEVICE_CLASS_VOLTAGE, |     DEVICE_CLASS_VOLTAGE, | ||||||
|     LAST_RESET_TYPE_AUTO, |  | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|  |     STATE_CLASS_TOTAL_INCREASING, | ||||||
|     UNIT_VOLT, |     UNIT_VOLT, | ||||||
|     UNIT_AMPERE, |     UNIT_AMPERE, | ||||||
|     UNIT_WATT, |     UNIT_WATT, | ||||||
| @@ -78,8 +78,7 @@ CONFIG_SCHEMA = cv.Schema( | |||||||
|             unit_of_measurement=UNIT_WATT_HOURS, |             unit_of_measurement=UNIT_WATT_HOURS, | ||||||
|             accuracy_decimals=1, |             accuracy_decimals=1, | ||||||
|             device_class=DEVICE_CLASS_ENERGY, |             device_class=DEVICE_CLASS_ENERGY, | ||||||
|             state_class=STATE_CLASS_MEASUREMENT, |             state_class=STATE_CLASS_TOTAL_INCREASING, | ||||||
|             last_reset_type=LAST_RESET_TYPE_AUTO, |  | ||||||
|         ), |         ), | ||||||
|         cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, |         cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, | ||||||
|         cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float, |         cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float, | ||||||
|   | |||||||
| @@ -6,6 +6,10 @@ from esphome.const import ( | |||||||
|     CONF_PM_2_5, |     CONF_PM_2_5, | ||||||
|     CONF_PM_10_0, |     CONF_PM_10_0, | ||||||
|     CONF_PM_1_0, |     CONF_PM_1_0, | ||||||
|  |     DEVICE_CLASS_AQI, | ||||||
|  |     DEVICE_CLASS_PM1, | ||||||
|  |     DEVICE_CLASS_PM10, | ||||||
|  |     DEVICE_CLASS_PM25, | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|     UNIT_MICROGRAMS_PER_CUBIC_METER, |     UNIT_MICROGRAMS_PER_CUBIC_METER, | ||||||
|     ICON_CHEMICAL_WEAPON, |     ICON_CHEMICAL_WEAPON, | ||||||
| @@ -45,24 +49,28 @@ CONFIG_SCHEMA = cv.All( | |||||||
|                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, |                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, | ||||||
|                 icon=ICON_CHEMICAL_WEAPON, |                 icon=ICON_CHEMICAL_WEAPON, | ||||||
|                 accuracy_decimals=0, |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_PM1, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_PM_2_5): sensor.sensor_schema( |             cv.Optional(CONF_PM_2_5): sensor.sensor_schema( | ||||||
|                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, |                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, | ||||||
|                 icon=ICON_CHEMICAL_WEAPON, |                 icon=ICON_CHEMICAL_WEAPON, | ||||||
|                 accuracy_decimals=0, |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_PM25, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_PM_10_0): sensor.sensor_schema( |             cv.Optional(CONF_PM_10_0): sensor.sensor_schema( | ||||||
|                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, |                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, | ||||||
|                 icon=ICON_CHEMICAL_WEAPON, |                 icon=ICON_CHEMICAL_WEAPON, | ||||||
|                 accuracy_decimals=0, |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_PM10, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_AQI): sensor.sensor_schema( |             cv.Optional(CONF_AQI): sensor.sensor_schema( | ||||||
|                 unit_of_measurement=UNIT_INDEX, |                 unit_of_measurement=UNIT_INDEX, | ||||||
|                 icon=ICON_CHEMICAL_WEAPON, |                 icon=ICON_CHEMICAL_WEAPON, | ||||||
|                 accuracy_decimals=0, |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_AQI, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|             ).extend( |             ).extend( | ||||||
|                 { |                 { | ||||||
|   | |||||||
| @@ -42,7 +42,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     .extend(cv.polling_component_schema("1s")) |     .extend(cv.polling_component_schema("1s")) | ||||||
|     .extend(spi.spi_device_schema()), |     .extend(spi.spi_device_schema(False)), | ||||||
|     cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), |     cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), | ||||||
| ) | ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ from esphome.const import ( | |||||||
|     CONF_DEFAULT_TRANSITION_LENGTH, |     CONF_DEFAULT_TRANSITION_LENGTH, | ||||||
|     CONF_DISABLED_BY_DEFAULT, |     CONF_DISABLED_BY_DEFAULT, | ||||||
|     CONF_EFFECTS, |     CONF_EFFECTS, | ||||||
|  |     CONF_FLASH_TRANSITION_LENGTH, | ||||||
|     CONF_GAMMA_CORRECT, |     CONF_GAMMA_CORRECT, | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     CONF_INTERNAL, |     CONF_INTERNAL, | ||||||
| @@ -85,6 +86,9 @@ BRIGHTNESS_ONLY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend( | |||||||
|         cv.Optional( |         cv.Optional( | ||||||
|             CONF_DEFAULT_TRANSITION_LENGTH, default="1s" |             CONF_DEFAULT_TRANSITION_LENGTH, default="1s" | ||||||
|         ): cv.positive_time_period_milliseconds, |         ): cv.positive_time_period_milliseconds, | ||||||
|  |         cv.Optional( | ||||||
|  |             CONF_FLASH_TRANSITION_LENGTH, default="0s" | ||||||
|  |         ): cv.positive_time_period_milliseconds, | ||||||
|         cv.Optional(CONF_EFFECTS): validate_effects(MONOCHROMATIC_EFFECTS), |         cv.Optional(CONF_EFFECTS): validate_effects(MONOCHROMATIC_EFFECTS), | ||||||
|     } |     } | ||||||
| ) | ) | ||||||
| @@ -132,6 +136,10 @@ async def setup_light_core_(light_var, output_var, config): | |||||||
|                 config[CONF_DEFAULT_TRANSITION_LENGTH] |                 config[CONF_DEFAULT_TRANSITION_LENGTH] | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|  |     if CONF_FLASH_TRANSITION_LENGTH in config: | ||||||
|  |         cg.add( | ||||||
|  |             light_var.set_flash_transition_length(config[CONF_FLASH_TRANSITION_LENGTH]) | ||||||
|  |         ) | ||||||
|     if CONF_GAMMA_CORRECT in config: |     if CONF_GAMMA_CORRECT in config: | ||||||
|         cg.add(light_var.set_gamma_correct(config[CONF_GAMMA_CORRECT])) |         cg.add(light_var.set_gamma_correct(config[CONF_GAMMA_CORRECT])) | ||||||
|     effects = await cg.build_registry_list( |     effects = await cg.build_registry_list( | ||||||
|   | |||||||
| @@ -12,14 +12,13 @@ void AddressableLight::call_setup() { | |||||||
| #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE | #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE | ||||||
|   this->set_interval(5000, [this]() { |   this->set_interval(5000, [this]() { | ||||||
|     const char *name = this->state_parent_ == nullptr ? "" : this->state_parent_->get_name().c_str(); |     const char *name = this->state_parent_ == nullptr ? "" : this->state_parent_->get_name().c_str(); | ||||||
|     ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s next_show=%s)", name, YESNO(this->effect_active_), |     ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s)", name, YESNO(this->effect_active_)); | ||||||
|               YESNO(this->next_show_)); |  | ||||||
|     for (int i = 0; i < this->size(); i++) { |     for (int i = 0; i < this->size(); i++) { | ||||||
|       auto color = this->get(i); |       auto color = this->get(i); | ||||||
|       ESP_LOGVV(TAG, "  [%2d] Color: R=%3u G=%3u B=%3u W=%3u", i, color.get_red_raw(), color.get_green_raw(), |       ESP_LOGVV(TAG, "  [%2d] Color: R=%3u G=%3u B=%3u W=%3u", i, color.get_red_raw(), color.get_green_raw(), | ||||||
|                 color.get_blue_raw(), color.get_white_raw()); |                 color.get_blue_raw(), color.get_white_raw()); | ||||||
|     } |     } | ||||||
|     ESP_LOGVV(TAG, ""); |     ESP_LOGVV(TAG, " "); | ||||||
|   }); |   }); | ||||||
| #endif | #endif | ||||||
| } | } | ||||||
| @@ -36,7 +35,7 @@ Color esp_color_from_light_color_values(LightColorValues val) { | |||||||
|   return Color(r, g, b, w); |   return Color(r, g, b, w); | ||||||
| } | } | ||||||
|  |  | ||||||
| void AddressableLight::write_state(LightState *state) { | void AddressableLight::update_state(LightState *state) { | ||||||
|   auto val = state->current_values; |   auto val = state->current_values; | ||||||
|   auto max_brightness = to_uint8_scale(val.get_brightness() * val.get_state()); |   auto max_brightness = to_uint8_scale(val.get_brightness() * val.get_state()); | ||||||
|   this->correction_.set_local_brightness(max_brightness); |   this->correction_.set_local_brightness(max_brightness); | ||||||
|   | |||||||
| @@ -51,9 +51,9 @@ class AddressableLight : public LightOutput, public Component { | |||||||
|       amnt = this->size(); |       amnt = this->size(); | ||||||
|     this->range(amnt, this->size()) = this->range(0, -amnt); |     this->range(amnt, this->size()) = this->range(0, -amnt); | ||||||
|   } |   } | ||||||
|  |   // Indicates whether an effect that directly updates the output buffer is active to prevent overwriting | ||||||
|   bool is_effect_active() const { return this->effect_active_; } |   bool is_effect_active() const { return this->effect_active_; } | ||||||
|   void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; } |   void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; } | ||||||
|   void write_state(LightState *state) override; |  | ||||||
|   std::unique_ptr<LightTransformer> create_default_transition() override; |   std::unique_ptr<LightTransformer> create_default_transition() override; | ||||||
|   void set_correction(float red, float green, float blue, float white = 1.0f) { |   void set_correction(float red, float green, float blue, float white = 1.0f) { | ||||||
|     this->correction_.set_max_brightness( |     this->correction_.set_max_brightness( | ||||||
| @@ -63,7 +63,8 @@ class AddressableLight : public LightOutput, public Component { | |||||||
|     this->correction_.calculate_gamma_table(state->get_gamma_correct()); |     this->correction_.calculate_gamma_table(state->get_gamma_correct()); | ||||||
|     this->state_parent_ = state; |     this->state_parent_ = state; | ||||||
|   } |   } | ||||||
|   void schedule_show() { this->next_show_ = true; } |   void update_state(LightState *state) override; | ||||||
|  |   void schedule_show() { this->state_parent_->next_write_ = true; } | ||||||
|  |  | ||||||
| #ifdef USE_POWER_SUPPLY | #ifdef USE_POWER_SUPPLY | ||||||
|   void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); } |   void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); } | ||||||
| @@ -74,9 +75,7 @@ class AddressableLight : public LightOutput, public Component { | |||||||
|  protected: |  protected: | ||||||
|   friend class AddressableLightTransformer; |   friend class AddressableLightTransformer; | ||||||
|  |  | ||||||
|   bool should_show_() const { return this->effect_active_ || this->next_show_; } |  | ||||||
|   void mark_shown_() { |   void mark_shown_() { | ||||||
|     this->next_show_ = false; |  | ||||||
| #ifdef USE_POWER_SUPPLY | #ifdef USE_POWER_SUPPLY | ||||||
|     for (auto c : *this) { |     for (auto c : *this) { | ||||||
|       if (c.get().is_on()) { |       if (c.get().is_on()) { | ||||||
| @@ -90,7 +89,6 @@ class AddressableLight : public LightOutput, public Component { | |||||||
|   virtual ESPColorView get_view_internal(int32_t index) const = 0; |   virtual ESPColorView get_view_internal(int32_t index) const = 0; | ||||||
|  |  | ||||||
|   bool effect_active_{false}; |   bool effect_active_{false}; | ||||||
|   bool next_show_{true}; |  | ||||||
|   ESPColorCorrection correction_{}; |   ESPColorCorrection correction_{}; | ||||||
| #ifdef USE_POWER_SUPPLY | #ifdef USE_POWER_SUPPLY | ||||||
|   power_supply::PowerSupplyRequester power_; |   power_supply::PowerSupplyRequester power_; | ||||||
|   | |||||||
| @@ -63,6 +63,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { | |||||||
|       this->last_run_ = now; |       this->last_run_ = now; | ||||||
|       this->f_(it, current_color, this->initial_run_); |       this->f_(it, current_color, this->initial_run_); | ||||||
|       this->initial_run_ = false; |       this->initial_run_ = false; | ||||||
|  |       it.schedule_show(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -87,6 +88,7 @@ class AddressableRainbowLightEffect : public AddressableLightEffect { | |||||||
|       var = hsv; |       var = hsv; | ||||||
|       hue += add; |       hue += add; | ||||||
|     } |     } | ||||||
|  |     it.schedule_show(); | ||||||
|   } |   } | ||||||
|   void set_speed(uint32_t speed) { this->speed_ = speed; } |   void set_speed(uint32_t speed) { this->speed_ = speed; } | ||||||
|   void set_width(uint16_t width) { this->width_ = width; } |   void set_width(uint16_t width) { this->width_ = width; } | ||||||
| @@ -134,6 +136,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect { | |||||||
|         new_color.b = c.b; |         new_color.b = c.b; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |     it.schedule_show(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
| @@ -151,14 +154,10 @@ class AddressableScanEffect : public AddressableLightEffect { | |||||||
|   void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; } |   void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; } | ||||||
|   void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; } |   void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; } | ||||||
|   void apply(AddressableLight &it, const Color ¤t_color) override { |   void apply(AddressableLight &it, const Color ¤t_color) override { | ||||||
|     it.all() = Color::BLACK; |  | ||||||
|  |  | ||||||
|     for (auto i = 0; i < this->scan_width_; i++) { |  | ||||||
|       it[this->at_led_ + i] = current_color; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const uint32_t now = millis(); |     const uint32_t now = millis(); | ||||||
|     if (now - this->last_move_ > this->move_interval_) { |     if (now - this->last_move_ < this->move_interval_) | ||||||
|  |       return; | ||||||
|  |  | ||||||
|     if (direction_) { |     if (direction_) { | ||||||
|       this->at_led_++; |       this->at_led_++; | ||||||
|       if (this->at_led_ == it.size() - this->scan_width_) |       if (this->at_led_ == it.size() - this->scan_width_) | ||||||
| @@ -169,7 +168,13 @@ class AddressableScanEffect : public AddressableLightEffect { | |||||||
|         this->direction_ = true; |         this->direction_ = true; | ||||||
|     } |     } | ||||||
|     this->last_move_ = now; |     this->last_move_ = now; | ||||||
|  |  | ||||||
|  |     it.all() = Color::BLACK; | ||||||
|  |     for (auto i = 0; i < this->scan_width_; i++) { | ||||||
|  |       it[this->at_led_ + i] = current_color; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     it.schedule_show(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
| @@ -210,6 +215,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect { | |||||||
|         continue; |         continue; | ||||||
|       addressable[pos].set_effect_data(1); |       addressable[pos].set_effect_data(1); | ||||||
|     } |     } | ||||||
|  |     addressable.schedule_show(); | ||||||
|   } |   } | ||||||
|   void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; } |   void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; } | ||||||
|   void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; } |   void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; } | ||||||
| @@ -257,6 +263,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect { | |||||||
|       const uint8_t color = random_uint32() & 0b111; |       const uint8_t color = random_uint32() & 0b111; | ||||||
|       it[pos].set_effect_data(0b1000 | color); |       it[pos].set_effect_data(0b1000 | color); | ||||||
|     } |     } | ||||||
|  |     it.schedule_show(); | ||||||
|   } |   } | ||||||
|   void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; } |   void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; } | ||||||
|   void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; } |   void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; } | ||||||
| @@ -301,6 +308,7 @@ class AddressableFireworksEffect : public AddressableLightEffect { | |||||||
|         it[pos] = current_color; |         it[pos] = current_color; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |     it.schedule_show(); | ||||||
|   } |   } | ||||||
|   void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } |   void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } | ||||||
|   void set_spark_probability(float spark_probability) { this->spark_probability_ = spark_probability; } |   void set_spark_probability(float spark_probability) { this->spark_probability_ = spark_probability; } | ||||||
| @@ -335,6 +343,7 @@ class AddressableFlickerEffect : public AddressableLightEffect { | |||||||
|       // slowly fade back to "real" value |       // slowly fade back to "real" value | ||||||
|       var = (var.get() * inv_intensity) + (current_color * intensity); |       var = (var.get() * inv_intensity) + (current_color * intensity); | ||||||
|     } |     } | ||||||
|  |     it.schedule_show(); | ||||||
|   } |   } | ||||||
|   void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } |   void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } | ||||||
|   void set_intensity(float intensity) { this->intensity_ = to_uint8_scale(intensity); } |   void set_intensity(float intensity) { this->intensity_ = to_uint8_scale(intensity); } | ||||||
|   | |||||||
| @@ -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); | ||||||
| @@ -196,7 +196,6 @@ class FlickerLightEffect : public LightEffect { | |||||||
|     out.set_warm_white(remote.get_warm_white() * beta + current.get_warm_white() * alpha + |     out.set_warm_white(remote.get_warm_white() * beta + current.get_warm_white() * alpha + | ||||||
|                        (random_cubic_float() * this->intensity_)); |                        (random_cubic_float() * this->intensity_)); | ||||||
|  |  | ||||||
|     auto traits = this->state_->get_traits(); |  | ||||||
|     auto call = this->state_->make_call(); |     auto call = this->state_->make_call(); | ||||||
|     call.set_publish(false); |     call.set_publish(false); | ||||||
|     call.set_save(false); |     call.set_save(false); | ||||||
|   | |||||||
| @@ -52,7 +52,7 @@ enum class ColorMode : uint8_t { | |||||||
|   /// Only on/off control. |   /// Only on/off control. | ||||||
|   ON_OFF = (uint8_t) ColorCapability::ON_OFF, |   ON_OFF = (uint8_t) ColorCapability::ON_OFF, | ||||||
|   /// Dimmable light. |   /// Dimmable light. | ||||||
|   BRIGHTNESS = (uint8_t) ColorCapability::BRIGHTNESS, |   BRIGHTNESS = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS), | ||||||
|   /// White output only (use only if the light also has another color mode such as RGB). |   /// White output only (use only if the light also has another color mode such as RGB). | ||||||
|   WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::WHITE), |   WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::WHITE), | ||||||
|   /// Controllable color temperature output. |   /// Controllable color temperature output. | ||||||
|   | |||||||
| @@ -8,26 +8,23 @@ namespace light { | |||||||
| static const char *const TAG = "light"; | static const char *const TAG = "light"; | ||||||
|  |  | ||||||
| static const char *color_mode_to_human(ColorMode color_mode) { | static const char *color_mode_to_human(ColorMode color_mode) { | ||||||
|   switch (color_mode) { |   if (color_mode == ColorMode::UNKNOWN) | ||||||
|     case ColorMode::UNKNOWN: |  | ||||||
|     return "Unknown"; |     return "Unknown"; | ||||||
|     case ColorMode::WHITE: |   if (color_mode == ColorMode::WHITE) | ||||||
|     return "White"; |     return "White"; | ||||||
|     case ColorMode::COLOR_TEMPERATURE: |   if (color_mode == ColorMode::COLOR_TEMPERATURE) | ||||||
|     return "Color temperature"; |     return "Color temperature"; | ||||||
|     case ColorMode::COLD_WARM_WHITE: |   if (color_mode == ColorMode::COLD_WARM_WHITE) | ||||||
|     return "Cold/warm white"; |     return "Cold/warm white"; | ||||||
|     case ColorMode::RGB: |   if (color_mode == ColorMode::RGB) | ||||||
|     return "RGB"; |     return "RGB"; | ||||||
|     case ColorMode::RGB_WHITE: |   if (color_mode == ColorMode::RGB_WHITE) | ||||||
|     return "RGBW"; |     return "RGBW"; | ||||||
|     case ColorMode::RGB_COLD_WARM_WHITE: |   if (color_mode == ColorMode::RGB_COLD_WARM_WHITE) | ||||||
|     return "RGB + cold/warm white"; |     return "RGB + cold/warm white"; | ||||||
|     case ColorMode::RGB_COLOR_TEMPERATURE: |   if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE) | ||||||
|     return "RGB + color temperature"; |     return "RGB + color temperature"; | ||||||
|     default: |  | ||||||
|   return ""; |   return ""; | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| void LightCall::perform() { | void LightCall::perform() { | ||||||
|   | |||||||
| @@ -19,6 +19,13 @@ class LightOutput { | |||||||
|  |  | ||||||
|   virtual void setup_state(LightState *state) {} |   virtual void setup_state(LightState *state) {} | ||||||
|  |  | ||||||
|  |   /// Called on every update of the current values of the associated LightState, | ||||||
|  |   /// can optionally be used to do processing of this change. | ||||||
|  |   virtual void update_state(LightState *state) {} | ||||||
|  |  | ||||||
|  |   /// Called from loop() every time the light state has changed, and should | ||||||
|  |   /// should write the new state to hardware. Every call to write_state() is | ||||||
|  |   /// preceded by (at least) one call to update_state(). | ||||||
|   virtual void write_state(LightState *state) = 0; |   virtual void write_state(LightState *state) = 0; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -114,9 +114,11 @@ void LightState::loop() { | |||||||
|   // Apply transformer (if any) |   // Apply transformer (if any) | ||||||
|   if (this->transformer_ != nullptr) { |   if (this->transformer_ != nullptr) { | ||||||
|     auto values = this->transformer_->apply(); |     auto values = this->transformer_->apply(); | ||||||
|     this->next_write_ = values.has_value();  // don't write if transformer doesn't want us to |     if (values.has_value()) { | ||||||
|     if (values.has_value()) |  | ||||||
|       this->current_values = *values; |       this->current_values = *values; | ||||||
|  |       this->output_->update_state(this); | ||||||
|  |       this->next_write_ = true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (this->transformer_->is_finished()) { |     if (this->transformer_->is_finished()) { | ||||||
|       this->transformer_->stop(); |       this->transformer_->stop(); | ||||||
| @@ -127,18 +129,15 @@ void LightState::loop() { | |||||||
|  |  | ||||||
|   // Write state to the light |   // Write state to the light | ||||||
|   if (this->next_write_) { |   if (this->next_write_) { | ||||||
|     this->output_->write_state(this); |  | ||||||
|     this->next_write_ = false; |     this->next_write_ = false; | ||||||
|  |     this->output_->write_state(this); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } | float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } | ||||||
| uint32_t LightState::hash_base() { return 1114400283; } | uint32_t LightState::hash_base() { return 1114400283; } | ||||||
|  |  | ||||||
| void LightState::publish_state() { | void LightState::publish_state() { this->remote_values_callback_.call(); } | ||||||
|   this->remote_values_callback_.call(); |  | ||||||
|   this->next_write_ = true; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| LightOutput *LightState::get_output() const { return this->output_; } | LightOutput *LightState::get_output() const { return this->output_; } | ||||||
| std::string LightState::get_effect_name() { | std::string LightState::get_effect_name() { | ||||||
| @@ -158,6 +157,11 @@ void LightState::add_new_target_state_reached_callback(std::function<void()> &&s | |||||||
| void LightState::set_default_transition_length(uint32_t default_transition_length) { | void LightState::set_default_transition_length(uint32_t default_transition_length) { | ||||||
|   this->default_transition_length_ = default_transition_length; |   this->default_transition_length_ = default_transition_length; | ||||||
| } | } | ||||||
|  | uint32_t LightState::get_default_transition_length() const { return this->default_transition_length_; } | ||||||
|  | void LightState::set_flash_transition_length(uint32_t flash_transition_length) { | ||||||
|  |   this->flash_transition_length_ = flash_transition_length; | ||||||
|  | } | ||||||
|  | uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; } | ||||||
| void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } | void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } | ||||||
| void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } | void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } | ||||||
| bool LightState::supports_effects() { return !this->effects_.empty(); } | bool LightState::supports_effects() { return !this->effects_.empty(); } | ||||||
| @@ -235,7 +239,7 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length) { | |||||||
|   // If starting a flash if one is already happening, set end values to end values of current flash |   // If starting a flash if one is already happening, set end values to end values of current flash | ||||||
|   // Hacky but works |   // Hacky but works | ||||||
|   if (this->transformer_ != nullptr) |   if (this->transformer_ != nullptr) | ||||||
|     end_colors = this->transformer_->get_target_values(); |     end_colors = this->transformer_->get_start_values(); | ||||||
|  |  | ||||||
|   this->transformer_ = make_unique<LightFlashTransformer>(*this); |   this->transformer_ = make_unique<LightFlashTransformer>(*this); | ||||||
|   this->transformer_->setup(end_colors, target, length); |   this->transformer_->setup(end_colors, target, length); | ||||||
| @@ -248,6 +252,7 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot | |||||||
|   if (set_remote_values) { |   if (set_remote_values) { | ||||||
|     this->remote_values = target; |     this->remote_values = target; | ||||||
|   } |   } | ||||||
|  |   this->output_->update_state(this); | ||||||
|   this->next_write_ = true; |   this->next_write_ = true; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -99,6 +99,11 @@ class LightState : public Nameable, public Component { | |||||||
|  |  | ||||||
|   /// Set the default transition length, i.e. the transition length when no transition is provided. |   /// Set the default transition length, i.e. the transition length when no transition is provided. | ||||||
|   void set_default_transition_length(uint32_t default_transition_length); |   void set_default_transition_length(uint32_t default_transition_length); | ||||||
|  |   uint32_t get_default_transition_length() const; | ||||||
|  |  | ||||||
|  |   /// Set the flash transition length | ||||||
|  |   void set_flash_transition_length(uint32_t flash_transition_length); | ||||||
|  |   uint32_t get_flash_transition_length() const; | ||||||
|  |  | ||||||
|   /// Set the gamma correction factor |   /// Set the gamma correction factor | ||||||
|   void set_gamma_correct(float gamma_correct); |   void set_gamma_correct(float gamma_correct); | ||||||
| @@ -188,6 +193,8 @@ class LightState : public Nameable, public Component { | |||||||
|  |  | ||||||
|   /// Default transition length for all transitions in ms. |   /// Default transition length for all transitions in ms. | ||||||
|   uint32_t default_transition_length_{}; |   uint32_t default_transition_length_{}; | ||||||
|  |   /// Transition length to use for flash transitions. | ||||||
|  |   uint32_t flash_transition_length_{}; | ||||||
|   /// Gamma correction factor for the light. |   /// Gamma correction factor for the light. | ||||||
|   float gamma_correct_{}; |   float gamma_correct_{}; | ||||||
|   /// Restore mode of the light. |   /// Restore mode of the light. | ||||||
|   | |||||||
| @@ -58,7 +58,43 @@ class LightFlashTransformer : public LightTransformer { | |||||||
|  public: |  public: | ||||||
|   LightFlashTransformer(LightState &state) : state_(state) {} |   LightFlashTransformer(LightState &state) : state_(state) {} | ||||||
|  |  | ||||||
|   optional<LightColorValues> apply() override { return this->get_target_values(); } |   void start() override { | ||||||
|  |     this->transition_length_ = this->state_.get_flash_transition_length(); | ||||||
|  |     if (this->transition_length_ * 2 > this->length_) | ||||||
|  |       this->transition_length_ = this->length_ / 2; | ||||||
|  |  | ||||||
|  |     // do not create transition if length is 0 | ||||||
|  |     if (this->transition_length_ == 0) | ||||||
|  |       return; | ||||||
|  |  | ||||||
|  |     // first transition to original target | ||||||
|  |     this->transformer_ = this->state_.get_output()->create_default_transition(); | ||||||
|  |     this->transformer_->setup(this->state_.current_values, this->target_values_, this->transition_length_); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   optional<LightColorValues> apply() override { | ||||||
|  |     // transition transformer does not handle 0 length as progress returns nan | ||||||
|  |     if (this->transition_length_ == 0) | ||||||
|  |       return this->target_values_; | ||||||
|  |  | ||||||
|  |     if (this->transformer_ != nullptr) { | ||||||
|  |       if (!this->transformer_->is_finished()) { | ||||||
|  |         return this->transformer_->apply(); | ||||||
|  |       } else { | ||||||
|  |         this->transformer_->stop(); | ||||||
|  |         this->transformer_ = nullptr; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (millis() > this->start_time_ + this->length_ - this->transition_length_) { | ||||||
|  |       // second transition back to start value | ||||||
|  |       this->transformer_ = this->state_.get_output()->create_default_transition(); | ||||||
|  |       this->transformer_->setup(this->state_.current_values, this->get_start_values(), this->transition_length_); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // once transition is complete, don't change states until next transition | ||||||
|  |     return optional<LightColorValues>(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // Restore the original values after the flash. |   // Restore the original values after the flash. | ||||||
|   void stop() override { |   void stop() override { | ||||||
| @@ -69,6 +105,8 @@ class LightFlashTransformer : public LightTransformer { | |||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   LightState &state_; |   LightState &state_; | ||||||
|  |   uint32_t transition_length_; | ||||||
|  |   std::unique_ptr<LightTransformer> transformer_{nullptr}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace light | }  // namespace light | ||||||
|   | |||||||
| @@ -43,21 +43,24 @@ void Logger::write_header_(int level, const char *tag, int line) { | |||||||
| } | } | ||||||
|  |  | ||||||
| void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) {  // NOLINT | void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) {  // NOLINT | ||||||
|   if (level > this->level_for(tag)) |   if (level > this->level_for(tag) || recursion_guard_) | ||||||
|     return; |     return; | ||||||
|  |  | ||||||
|  |   recursion_guard_ = true; | ||||||
|   this->reset_buffer_(); |   this->reset_buffer_(); | ||||||
|   this->write_header_(level, tag, line); |   this->write_header_(level, tag, line); | ||||||
|   this->vprintf_to_buffer_(format, args); |   this->vprintf_to_buffer_(format, args); | ||||||
|   this->write_footer_(); |   this->write_footer_(); | ||||||
|   this->log_message_(level, tag); |   this->log_message_(level, tag); | ||||||
|  |   recursion_guard_ = false; | ||||||
| } | } | ||||||
| #ifdef USE_STORE_LOG_STR_IN_FLASH | #ifdef USE_STORE_LOG_STR_IN_FLASH | ||||||
| void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, | void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, | ||||||
|                           va_list args) {  // NOLINT |                           va_list args) {  // NOLINT | ||||||
|   if (level > this->level_for(tag)) |   if (level > this->level_for(tag) || recursion_guard_) | ||||||
|     return; |     return; | ||||||
|  |  | ||||||
|  |   recursion_guard_ = true; | ||||||
|   this->reset_buffer_(); |   this->reset_buffer_(); | ||||||
|   // copy format string |   // copy format string | ||||||
|   const char *format_pgm_p = (PGM_P) format; |   const char *format_pgm_p = (PGM_P) format; | ||||||
| @@ -78,6 +81,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr | |||||||
|   this->vprintf_to_buffer_(this->tx_buffer_, args); |   this->vprintf_to_buffer_(this->tx_buffer_, args); | ||||||
|   this->write_footer_(); |   this->write_footer_(); | ||||||
|   this->log_message_(level, tag, offset); |   this->log_message_(level, tag, offset); | ||||||
|  |   recursion_guard_ = false; | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   | |||||||
| @@ -113,6 +113,8 @@ class Logger : public Component { | |||||||
|   }; |   }; | ||||||
|   std::vector<LogLevelOverride> log_levels_; |   std::vector<LogLevelOverride> log_levels_; | ||||||
|   CallbackManager<void(int, const char *, const char *)> log_callback_{}; |   CallbackManager<void(int, const char *, const char *)> log_callback_{}; | ||||||
|  |   /// Prevents recursive log calls, if true a log message is already being processed. | ||||||
|  |   bool recursion_guard_ = false; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| extern Logger *global_logger;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | extern Logger *global_logger;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
|   | |||||||
| @@ -91,7 +91,7 @@ async def mcp23xxx_pin_to_code(config): | |||||||
|  |  | ||||||
|  |  | ||||||
| # BEGIN Removed pin schemas below to show error in configuration | # BEGIN Removed pin schemas below to show error in configuration | ||||||
| # TODO remove in 1.19.0 | # TODO remove in 2022.5.0 | ||||||
|  |  | ||||||
| for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]: | for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]: | ||||||
|     PIN_SCHEMA = cv.Schema( |     PIN_SCHEMA = cv.Schema( | ||||||
| @@ -110,6 +110,7 @@ for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]: | |||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     # pylint: disable=cell-var-from-loop | ||||||
|     @pins.PIN_SCHEMA_REGISTRY.register(id, (PIN_SCHEMA, PIN_SCHEMA)) |     @pins.PIN_SCHEMA_REGISTRY.register(id, (PIN_SCHEMA, PIN_SCHEMA)) | ||||||
|     def pin_to_code(config): |     def pin_to_code(config): | ||||||
|         pass |         pass | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ from esphome.const import ( | |||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     CONF_TEMPERATURE, |     CONF_TEMPERATURE, | ||||||
|     DEVICE_CLASS_TEMPERATURE, |     DEVICE_CLASS_TEMPERATURE, | ||||||
|  |     DEVICE_CLASS_CARBON_DIOXIDE, | ||||||
|     ICON_MOLECULE_CO2, |     ICON_MOLECULE_CO2, | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|     UNIT_PARTS_PER_MILLION, |     UNIT_PARTS_PER_MILLION, | ||||||
| @@ -34,6 +35,7 @@ CONFIG_SCHEMA = ( | |||||||
|                 unit_of_measurement=UNIT_PARTS_PER_MILLION, |                 unit_of_measurement=UNIT_PARTS_PER_MILLION, | ||||||
|                 icon=ICON_MOLECULE_CO2, |                 icon=ICON_MOLECULE_CO2, | ||||||
|                 accuracy_decimals=0, |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_CARBON_DIOXIDE, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( |             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||||
|   | |||||||
							
								
								
									
										173
									
								
								esphome/components/midea/adapter.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								esphome/components/midea/adapter.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | |||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "adapter.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace midea { | ||||||
|  |  | ||||||
|  | const char *const Constants::TAG = "midea"; | ||||||
|  | const std::string Constants::FREEZE_PROTECTION = "freeze protection"; | ||||||
|  | const std::string Constants::SILENT = "silent"; | ||||||
|  | const std::string Constants::TURBO = "turbo"; | ||||||
|  |  | ||||||
|  | ClimateMode Converters::to_climate_mode(MideaMode mode) { | ||||||
|  |   switch (mode) { | ||||||
|  |     case MideaMode::MODE_AUTO: | ||||||
|  |       return ClimateMode::CLIMATE_MODE_HEAT_COOL; | ||||||
|  |     case MideaMode::MODE_COOL: | ||||||
|  |       return ClimateMode::CLIMATE_MODE_COOL; | ||||||
|  |     case MideaMode::MODE_DRY: | ||||||
|  |       return ClimateMode::CLIMATE_MODE_DRY; | ||||||
|  |     case MideaMode::MODE_FAN_ONLY: | ||||||
|  |       return ClimateMode::CLIMATE_MODE_FAN_ONLY; | ||||||
|  |     case MideaMode::MODE_HEAT: | ||||||
|  |       return ClimateMode::CLIMATE_MODE_HEAT; | ||||||
|  |     default: | ||||||
|  |       return ClimateMode::CLIMATE_MODE_OFF; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | MideaMode Converters::to_midea_mode(ClimateMode mode) { | ||||||
|  |   switch (mode) { | ||||||
|  |     case ClimateMode::CLIMATE_MODE_HEAT_COOL: | ||||||
|  |       return MideaMode::MODE_AUTO; | ||||||
|  |     case ClimateMode::CLIMATE_MODE_COOL: | ||||||
|  |       return MideaMode::MODE_COOL; | ||||||
|  |     case ClimateMode::CLIMATE_MODE_DRY: | ||||||
|  |       return MideaMode::MODE_DRY; | ||||||
|  |     case ClimateMode::CLIMATE_MODE_FAN_ONLY: | ||||||
|  |       return MideaMode::MODE_FAN_ONLY; | ||||||
|  |     case ClimateMode::CLIMATE_MODE_HEAT: | ||||||
|  |       return MideaMode::MODE_HEAT; | ||||||
|  |     default: | ||||||
|  |       return MideaMode::MODE_OFF; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ClimateSwingMode Converters::to_climate_swing_mode(MideaSwingMode mode) { | ||||||
|  |   switch (mode) { | ||||||
|  |     case MideaSwingMode::SWING_VERTICAL: | ||||||
|  |       return ClimateSwingMode::CLIMATE_SWING_VERTICAL; | ||||||
|  |     case MideaSwingMode::SWING_HORIZONTAL: | ||||||
|  |       return ClimateSwingMode::CLIMATE_SWING_HORIZONTAL; | ||||||
|  |     case MideaSwingMode::SWING_BOTH: | ||||||
|  |       return ClimateSwingMode::CLIMATE_SWING_BOTH; | ||||||
|  |     default: | ||||||
|  |       return ClimateSwingMode::CLIMATE_SWING_OFF; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | MideaSwingMode Converters::to_midea_swing_mode(ClimateSwingMode mode) { | ||||||
|  |   switch (mode) { | ||||||
|  |     case ClimateSwingMode::CLIMATE_SWING_VERTICAL: | ||||||
|  |       return MideaSwingMode::SWING_VERTICAL; | ||||||
|  |     case ClimateSwingMode::CLIMATE_SWING_HORIZONTAL: | ||||||
|  |       return MideaSwingMode::SWING_HORIZONTAL; | ||||||
|  |     case ClimateSwingMode::CLIMATE_SWING_BOTH: | ||||||
|  |       return MideaSwingMode::SWING_BOTH; | ||||||
|  |     default: | ||||||
|  |       return MideaSwingMode::SWING_OFF; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | MideaFanMode Converters::to_midea_fan_mode(ClimateFanMode mode) { | ||||||
|  |   switch (mode) { | ||||||
|  |     case ClimateFanMode::CLIMATE_FAN_LOW: | ||||||
|  |       return MideaFanMode::FAN_LOW; | ||||||
|  |     case ClimateFanMode::CLIMATE_FAN_MEDIUM: | ||||||
|  |       return MideaFanMode::FAN_MEDIUM; | ||||||
|  |     case ClimateFanMode::CLIMATE_FAN_HIGH: | ||||||
|  |       return MideaFanMode::FAN_HIGH; | ||||||
|  |     default: | ||||||
|  |       return MideaFanMode::FAN_AUTO; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ClimateFanMode Converters::to_climate_fan_mode(MideaFanMode mode) { | ||||||
|  |   switch (mode) { | ||||||
|  |     case MideaFanMode::FAN_LOW: | ||||||
|  |       return ClimateFanMode::CLIMATE_FAN_LOW; | ||||||
|  |     case MideaFanMode::FAN_MEDIUM: | ||||||
|  |       return ClimateFanMode::CLIMATE_FAN_MEDIUM; | ||||||
|  |     case MideaFanMode::FAN_HIGH: | ||||||
|  |       return ClimateFanMode::CLIMATE_FAN_HIGH; | ||||||
|  |     default: | ||||||
|  |       return ClimateFanMode::CLIMATE_FAN_AUTO; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool Converters::is_custom_midea_fan_mode(MideaFanMode mode) { | ||||||
|  |   switch (mode) { | ||||||
|  |     case MideaFanMode::FAN_SILENT: | ||||||
|  |     case MideaFanMode::FAN_TURBO: | ||||||
|  |       return true; | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) { | ||||||
|  |   switch (mode) { | ||||||
|  |     case MideaFanMode::FAN_SILENT: | ||||||
|  |       return Constants::SILENT; | ||||||
|  |     default: | ||||||
|  |       return Constants::TURBO; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | MideaFanMode Converters::to_midea_fan_mode(const std::string &mode) { | ||||||
|  |   if (mode == Constants::SILENT) | ||||||
|  |     return MideaFanMode::FAN_SILENT; | ||||||
|  |   return MideaFanMode::FAN_TURBO; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | MideaPreset Converters::to_midea_preset(ClimatePreset preset) { | ||||||
|  |   switch (preset) { | ||||||
|  |     case ClimatePreset::CLIMATE_PRESET_SLEEP: | ||||||
|  |       return MideaPreset::PRESET_SLEEP; | ||||||
|  |     case ClimatePreset::CLIMATE_PRESET_ECO: | ||||||
|  |       return MideaPreset::PRESET_ECO; | ||||||
|  |     case ClimatePreset::CLIMATE_PRESET_BOOST: | ||||||
|  |       return MideaPreset::PRESET_TURBO; | ||||||
|  |     default: | ||||||
|  |       return MideaPreset::PRESET_NONE; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ClimatePreset Converters::to_climate_preset(MideaPreset preset) { | ||||||
|  |   switch (preset) { | ||||||
|  |     case MideaPreset::PRESET_SLEEP: | ||||||
|  |       return ClimatePreset::CLIMATE_PRESET_SLEEP; | ||||||
|  |     case MideaPreset::PRESET_ECO: | ||||||
|  |       return ClimatePreset::CLIMATE_PRESET_ECO; | ||||||
|  |     case MideaPreset::PRESET_TURBO: | ||||||
|  |       return ClimatePreset::CLIMATE_PRESET_BOOST; | ||||||
|  |     default: | ||||||
|  |       return ClimatePreset::CLIMATE_PRESET_NONE; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool Converters::is_custom_midea_preset(MideaPreset preset) { return preset == MideaPreset::PRESET_FREEZE_PROTECTION; } | ||||||
|  |  | ||||||
|  | const std::string &Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; } | ||||||
|  |  | ||||||
|  | MideaPreset Converters::to_midea_preset(const std::string &preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; } | ||||||
|  |  | ||||||
|  | void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities) { | ||||||
|  |   if (capabilities.supportAutoMode()) | ||||||
|  |     traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT_COOL); | ||||||
|  |   if (capabilities.supportCoolMode()) | ||||||
|  |     traits.add_supported_mode(ClimateMode::CLIMATE_MODE_COOL); | ||||||
|  |   if (capabilities.supportHeatMode()) | ||||||
|  |     traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT); | ||||||
|  |   if (capabilities.supportDryMode()) | ||||||
|  |     traits.add_supported_mode(ClimateMode::CLIMATE_MODE_DRY); | ||||||
|  |   if (capabilities.supportTurboPreset()) | ||||||
|  |     traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_BOOST); | ||||||
|  |   if (capabilities.supportEcoPreset()) | ||||||
|  |     traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO); | ||||||
|  |   if (capabilities.supportFrostProtectionPreset()) | ||||||
|  |     traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace midea | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										42
									
								
								esphome/components/midea/adapter.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								esphome/components/midea/adapter.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | #pragma once | ||||||
|  | #include <Appliance/AirConditioner/AirConditioner.h> | ||||||
|  | #include "esphome/components/climate/climate_traits.h" | ||||||
|  | #include "appliance_base.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace midea { | ||||||
|  |  | ||||||
|  | using MideaMode = dudanov::midea::ac::Mode; | ||||||
|  | using MideaSwingMode = dudanov::midea::ac::SwingMode; | ||||||
|  | using MideaFanMode = dudanov::midea::ac::FanMode; | ||||||
|  | using MideaPreset = dudanov::midea::ac::Preset; | ||||||
|  |  | ||||||
|  | class Constants { | ||||||
|  |  public: | ||||||
|  |   static const char *const TAG; | ||||||
|  |   static const std::string FREEZE_PROTECTION; | ||||||
|  |   static const std::string SILENT; | ||||||
|  |   static const std::string TURBO; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class Converters { | ||||||
|  |  public: | ||||||
|  |   static MideaMode to_midea_mode(ClimateMode mode); | ||||||
|  |   static ClimateMode to_climate_mode(MideaMode mode); | ||||||
|  |   static MideaSwingMode to_midea_swing_mode(ClimateSwingMode mode); | ||||||
|  |   static ClimateSwingMode to_climate_swing_mode(MideaSwingMode mode); | ||||||
|  |   static MideaPreset to_midea_preset(ClimatePreset preset); | ||||||
|  |   static MideaPreset to_midea_preset(const std::string &preset); | ||||||
|  |   static bool is_custom_midea_preset(MideaPreset preset); | ||||||
|  |   static ClimatePreset to_climate_preset(MideaPreset preset); | ||||||
|  |   static const std::string &to_custom_climate_preset(MideaPreset preset); | ||||||
|  |   static MideaFanMode to_midea_fan_mode(ClimateFanMode fan_mode); | ||||||
|  |   static MideaFanMode to_midea_fan_mode(const std::string &fan_mode); | ||||||
|  |   static bool is_custom_midea_fan_mode(MideaFanMode fan_mode); | ||||||
|  |   static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode); | ||||||
|  |   static const std::string &to_custom_climate_fan_mode(MideaFanMode fan_mode); | ||||||
|  |   static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace midea | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										152
									
								
								esphome/components/midea/air_conditioner.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								esphome/components/midea/air_conditioner.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | |||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "air_conditioner.h" | ||||||
|  | #include "adapter.h" | ||||||
|  | #ifdef USE_REMOTE_TRANSMITTER | ||||||
|  | #include "midea_ir.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace midea { | ||||||
|  |  | ||||||
|  | static void set_sensor(Sensor *sensor, float value) { | ||||||
|  |   if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value)) | ||||||
|  |     sensor->publish_state(value); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | template<typename T> void update_property(T &property, const T &value, bool &flag) { | ||||||
|  |   if (property != value) { | ||||||
|  |     property = value; | ||||||
|  |     flag = true; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AirConditioner::on_status_change() { | ||||||
|  |   bool need_publish = false; | ||||||
|  |   update_property(this->target_temperature, this->base_.getTargetTemp(), need_publish); | ||||||
|  |   update_property(this->current_temperature, this->base_.getIndoorTemp(), need_publish); | ||||||
|  |   auto mode = Converters::to_climate_mode(this->base_.getMode()); | ||||||
|  |   update_property(this->mode, mode, need_publish); | ||||||
|  |   auto swing_mode = Converters::to_climate_swing_mode(this->base_.getSwingMode()); | ||||||
|  |   update_property(this->swing_mode, swing_mode, need_publish); | ||||||
|  |   // Preset | ||||||
|  |   auto preset = this->base_.getPreset(); | ||||||
|  |   if (Converters::is_custom_midea_preset(preset)) { | ||||||
|  |     if (this->set_custom_preset_(Converters::to_custom_climate_preset(preset))) | ||||||
|  |       need_publish = true; | ||||||
|  |   } else if (this->set_preset_(Converters::to_climate_preset(preset))) { | ||||||
|  |     need_publish = true; | ||||||
|  |   } | ||||||
|  |   // Fan mode | ||||||
|  |   auto fan_mode = this->base_.getFanMode(); | ||||||
|  |   if (Converters::is_custom_midea_fan_mode(fan_mode)) { | ||||||
|  |     if (this->set_custom_fan_mode_(Converters::to_custom_climate_fan_mode(fan_mode))) | ||||||
|  |       need_publish = true; | ||||||
|  |   } else if (this->set_fan_mode_(Converters::to_climate_fan_mode(fan_mode))) { | ||||||
|  |     need_publish = true; | ||||||
|  |   } | ||||||
|  |   if (need_publish) | ||||||
|  |     this->publish_state(); | ||||||
|  |   set_sensor(this->outdoor_sensor_, this->base_.getOutdoorTemp()); | ||||||
|  |   set_sensor(this->power_sensor_, this->base_.getPowerUsage()); | ||||||
|  |   set_sensor(this->humidity_sensor_, this->base_.getIndoorHum()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AirConditioner::control(const ClimateCall &call) { | ||||||
|  |   dudanov::midea::ac::Control ctrl{}; | ||||||
|  |   if (call.get_target_temperature().has_value()) | ||||||
|  |     ctrl.targetTemp = call.get_target_temperature().value(); | ||||||
|  |   if (call.get_swing_mode().has_value()) | ||||||
|  |     ctrl.swingMode = Converters::to_midea_swing_mode(call.get_swing_mode().value()); | ||||||
|  |   if (call.get_mode().has_value()) | ||||||
|  |     ctrl.mode = Converters::to_midea_mode(call.get_mode().value()); | ||||||
|  |   if (call.get_preset().has_value()) | ||||||
|  |     ctrl.preset = Converters::to_midea_preset(call.get_preset().value()); | ||||||
|  |   else if (call.get_custom_preset().has_value()) | ||||||
|  |     ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().value()); | ||||||
|  |   if (call.get_fan_mode().has_value()) | ||||||
|  |     ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value()); | ||||||
|  |   else if (call.get_custom_fan_mode().has_value()) | ||||||
|  |     ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().value()); | ||||||
|  |   this->base_.control(ctrl); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ClimateTraits AirConditioner::traits() { | ||||||
|  |   auto traits = ClimateTraits(); | ||||||
|  |   traits.set_supports_current_temperature(true); | ||||||
|  |   traits.set_visual_min_temperature(17); | ||||||
|  |   traits.set_visual_max_temperature(30); | ||||||
|  |   traits.set_visual_temperature_step(0.5); | ||||||
|  |   traits.set_supported_modes(this->supported_modes_); | ||||||
|  |   traits.set_supported_swing_modes(this->supported_swing_modes_); | ||||||
|  |   traits.set_supported_presets(this->supported_presets_); | ||||||
|  |   traits.set_supported_custom_presets(this->supported_custom_presets_); | ||||||
|  |   traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); | ||||||
|  |   /* + MINIMAL SET OF CAPABILITIES */ | ||||||
|  |   traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF); | ||||||
|  |   traits.add_supported_mode(ClimateMode::CLIMATE_MODE_FAN_ONLY); | ||||||
|  |   traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO); | ||||||
|  |   traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW); | ||||||
|  |   traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM); | ||||||
|  |   traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH); | ||||||
|  |   traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF); | ||||||
|  |   traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_VERTICAL); | ||||||
|  |   traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE); | ||||||
|  |   traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_SLEEP); | ||||||
|  |   if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) | ||||||
|  |     Converters::to_climate_traits(traits, this->base_.getCapabilities()); | ||||||
|  |   return traits; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AirConditioner::dump_config() { | ||||||
|  |   ESP_LOGCONFIG(Constants::TAG, "MideaDongle:"); | ||||||
|  |   ESP_LOGCONFIG(Constants::TAG, "  [x] Period: %dms", this->base_.getPeriod()); | ||||||
|  |   ESP_LOGCONFIG(Constants::TAG, "  [x] Response timeout: %dms", this->base_.getTimeout()); | ||||||
|  |   ESP_LOGCONFIG(Constants::TAG, "  [x] Request attempts: %d", this->base_.getNumAttempts()); | ||||||
|  | #ifdef USE_REMOTE_TRANSMITTER | ||||||
|  |   ESP_LOGCONFIG(Constants::TAG, "  [x] Using RemoteTransmitter"); | ||||||
|  | #endif | ||||||
|  |   if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) { | ||||||
|  |     this->base_.getCapabilities().dump(); | ||||||
|  |   } else if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_ERROR) { | ||||||
|  |     ESP_LOGW(Constants::TAG, | ||||||
|  |              "Failed to get 0xB5 capabilities report. Suggest to disable it in config and manually set your " | ||||||
|  |              "appliance options."); | ||||||
|  |   } | ||||||
|  |   this->dump_traits_(Constants::TAG); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* ACTIONS */ | ||||||
|  |  | ||||||
|  | void AirConditioner::do_follow_me(float temperature, bool beeper) { | ||||||
|  | #ifdef USE_REMOTE_TRANSMITTER | ||||||
|  |   IrFollowMeData data(static_cast<uint8_t>(lroundf(temperature)), beeper); | ||||||
|  |   this->transmit_ir(data); | ||||||
|  | #else | ||||||
|  |   ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); | ||||||
|  | #endif | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AirConditioner::do_swing_step() { | ||||||
|  | #ifdef USE_REMOTE_TRANSMITTER | ||||||
|  |   IrSpecialData data(0x01); | ||||||
|  |   this->transmit_ir(data); | ||||||
|  | #else | ||||||
|  |   ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); | ||||||
|  | #endif | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AirConditioner::do_display_toggle() { | ||||||
|  |   if (this->base_.getCapabilities().supportLightControl()) { | ||||||
|  |     this->base_.displayToggle(); | ||||||
|  |   } else { | ||||||
|  | #ifdef USE_REMOTE_TRANSMITTER | ||||||
|  |     IrSpecialData data(0x08); | ||||||
|  |     this->transmit_ir(data); | ||||||
|  | #else | ||||||
|  |     ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); | ||||||
|  | #endif | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace midea | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										41
									
								
								esphome/components/midea/air_conditioner.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								esphome/components/midea/air_conditioner.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | #pragma once | ||||||
|  | #include <Appliance/AirConditioner/AirConditioner.h> | ||||||
|  | #include "appliance_base.h" | ||||||
|  | #include "esphome/components/sensor/sensor.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace midea { | ||||||
|  |  | ||||||
|  | using sensor::Sensor; | ||||||
|  | using climate::ClimateCall; | ||||||
|  |  | ||||||
|  | class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner> { | ||||||
|  |  public: | ||||||
|  |   void dump_config() override; | ||||||
|  |   void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; } | ||||||
|  |   void set_humidity_setpoint_sensor(Sensor *sensor) { this->humidity_sensor_ = sensor; } | ||||||
|  |   void set_power_sensor(Sensor *sensor) { this->power_sensor_ = sensor; } | ||||||
|  |   void on_status_change() override; | ||||||
|  |  | ||||||
|  |   /* ############### */ | ||||||
|  |   /* ### ACTIONS ### */ | ||||||
|  |   /* ############### */ | ||||||
|  |  | ||||||
|  |   void do_follow_me(float temperature, bool beeper = false); | ||||||
|  |   void do_display_toggle(); | ||||||
|  |   void do_swing_step(); | ||||||
|  |   void do_beeper_on() { this->set_beeper_feedback(true); } | ||||||
|  |   void do_beeper_off() { this->set_beeper_feedback(false); } | ||||||
|  |   void do_power_on() { this->base_.setPowerState(true); } | ||||||
|  |   void do_power_off() { this->base_.setPowerState(false); } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   void control(const ClimateCall &call) override; | ||||||
|  |   ClimateTraits traits() override; | ||||||
|  |   Sensor *outdoor_sensor_{nullptr}; | ||||||
|  |   Sensor *humidity_sensor_{nullptr}; | ||||||
|  |   Sensor *power_sensor_{nullptr}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace midea | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										76
									
								
								esphome/components/midea/appliance_base.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								esphome/components/midea/appliance_base.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | |||||||
|  | #pragma once | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/components/uart/uart.h" | ||||||
|  | #include "esphome/components/climate/climate.h" | ||||||
|  | #ifdef USE_REMOTE_TRANSMITTER | ||||||
|  | #include "esphome/components/remote_base/midea_protocol.h" | ||||||
|  | #include "esphome/components/remote_transmitter/remote_transmitter.h" | ||||||
|  | #endif | ||||||
|  | #include <Appliance/ApplianceBase.h> | ||||||
|  | #include <Helpers/Logger.h> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace midea { | ||||||
|  |  | ||||||
|  | using climate::ClimatePreset; | ||||||
|  | using climate::ClimateTraits; | ||||||
|  | using climate::ClimateMode; | ||||||
|  | using climate::ClimateSwingMode; | ||||||
|  | using climate::ClimateFanMode; | ||||||
|  |  | ||||||
|  | template<typename T> class ApplianceBase : public Component, public uart::UARTDevice, public climate::Climate { | ||||||
|  |   static_assert(std::is_base_of<dudanov::midea::ApplianceBase, T>::value, | ||||||
|  |                 "T must derive from dudanov::midea::ApplianceBase class"); | ||||||
|  |  | ||||||
|  |  public: | ||||||
|  |   ApplianceBase() { | ||||||
|  |     this->base_.setStream(this); | ||||||
|  |     this->base_.addOnStateCallback(std::bind(&ApplianceBase::on_status_change, this)); | ||||||
|  |     dudanov::midea::ApplianceBase::setLogger([](int level, const char *tag, int line, String format, va_list args) { | ||||||
|  |       esp_log_vprintf_(level, tag, line, format.c_str(), args); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |   bool can_proceed() override { | ||||||
|  |     return this->base_.getAutoconfStatus() != dudanov::midea::AutoconfStatus::AUTOCONF_PROGRESS; | ||||||
|  |   } | ||||||
|  |   float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; } | ||||||
|  |   void setup() override { this->base_.setup(); } | ||||||
|  |   void loop() override { this->base_.loop(); } | ||||||
|  |   void set_period(uint32_t ms) { this->base_.setPeriod(ms); } | ||||||
|  |   void set_response_timeout(uint32_t ms) { this->base_.setTimeout(ms); } | ||||||
|  |   void set_request_attempts(uint32_t attempts) { this->base_.setNumAttempts(attempts); } | ||||||
|  |   void set_beeper_feedback(bool state) { this->base_.setBeeper(state); } | ||||||
|  |   void set_autoconf(bool value) { this->base_.setAutoconf(value); } | ||||||
|  |   void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); } | ||||||
|  |   void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); } | ||||||
|  |   void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); } | ||||||
|  |   void set_custom_presets(std::set<std::string> presets) { this->supported_custom_presets_ = std::move(presets); } | ||||||
|  |   void set_custom_fan_modes(std::set<std::string> modes) { this->supported_custom_fan_modes_ = std::move(modes); } | ||||||
|  |   virtual void on_status_change() = 0; | ||||||
|  | #ifdef USE_REMOTE_TRANSMITTER | ||||||
|  |   void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { | ||||||
|  |     this->transmitter_ = transmitter; | ||||||
|  |   } | ||||||
|  |   void transmit_ir(remote_base::MideaData &data) { | ||||||
|  |     data.finalize(); | ||||||
|  |     auto transmit = this->transmitter_->transmit(); | ||||||
|  |     remote_base::MideaProtocol().encode(transmit.get_data(), data); | ||||||
|  |     transmit.perform(); | ||||||
|  |   } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   T base_; | ||||||
|  |   std::set<ClimateMode> supported_modes_{}; | ||||||
|  |   std::set<ClimateSwingMode> supported_swing_modes_{}; | ||||||
|  |   std::set<ClimatePreset> supported_presets_{}; | ||||||
|  |   std::set<std::string> supported_custom_presets_{}; | ||||||
|  |   std::set<std::string> supported_custom_fan_modes_{}; | ||||||
|  | #ifdef USE_REMOTE_TRANSMITTER | ||||||
|  |   remote_transmitter::RemoteTransmitterComponent *transmitter_{nullptr}; | ||||||
|  | #endif | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace midea | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										56
									
								
								esphome/components/midea/automations.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								esphome/components/midea/automations.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | #pragma once | ||||||
|  | #include "esphome/core/automation.h" | ||||||
|  | #include "air_conditioner.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace midea { | ||||||
|  |  | ||||||
|  | template<typename... Ts> class MideaActionBase : public Action<Ts...> { | ||||||
|  |  public: | ||||||
|  |   void set_parent(AirConditioner *parent) { this->parent_ = parent; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   AirConditioner *parent_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class FollowMeAction : public MideaActionBase<Ts...> { | ||||||
|  |   TEMPLATABLE_VALUE(float, temperature) | ||||||
|  |   TEMPLATABLE_VALUE(bool, beeper) | ||||||
|  |  | ||||||
|  |   void play(Ts... x) override { | ||||||
|  |     this->parent_->do_follow_me(this->temperature_.value(x...), this->beeper_.value(x...)); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class SwingStepAction : public MideaActionBase<Ts...> { | ||||||
|  |  public: | ||||||
|  |   void play(Ts... x) override { this->parent_->do_swing_step(); } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class DisplayToggleAction : public MideaActionBase<Ts...> { | ||||||
|  |  public: | ||||||
|  |   void play(Ts... x) override { this->parent_->do_display_toggle(); } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class BeeperOnAction : public MideaActionBase<Ts...> { | ||||||
|  |  public: | ||||||
|  |   void play(Ts... x) override { this->parent_->do_beeper_on(); } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class BeeperOffAction : public MideaActionBase<Ts...> { | ||||||
|  |  public: | ||||||
|  |   void play(Ts... x) override { this->parent_->do_beeper_off(); } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class PowerOnAction : public MideaActionBase<Ts...> { | ||||||
|  |  public: | ||||||
|  |   void play(Ts... x) override { this->parent_->do_power_on(); } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class PowerOffAction : public MideaActionBase<Ts...> { | ||||||
|  |  public: | ||||||
|  |   void play(Ts... x) override { this->parent_->do_power_off(); } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace midea | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										284
									
								
								esphome/components/midea/climate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								esphome/components/midea/climate.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,284 @@ | |||||||
|  | from esphome.core import coroutine | ||||||
|  | from esphome import automation | ||||||
|  | from esphome.components import climate, sensor, uart, remote_transmitter | ||||||
|  | from esphome.components.remote_base import CONF_TRANSMITTER_ID | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | import esphome.codegen as cg | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_AUTOCONF, | ||||||
|  |     CONF_BEEPER, | ||||||
|  |     CONF_CUSTOM_FAN_MODES, | ||||||
|  |     CONF_CUSTOM_PRESETS, | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_NUM_ATTEMPTS, | ||||||
|  |     CONF_PERIOD, | ||||||
|  |     CONF_SUPPORTED_MODES, | ||||||
|  |     CONF_SUPPORTED_PRESETS, | ||||||
|  |     CONF_SUPPORTED_SWING_MODES, | ||||||
|  |     CONF_TIMEOUT, | ||||||
|  |     CONF_TEMPERATURE, | ||||||
|  |     DEVICE_CLASS_POWER, | ||||||
|  |     DEVICE_CLASS_TEMPERATURE, | ||||||
|  |     DEVICE_CLASS_HUMIDITY, | ||||||
|  |     ICON_POWER, | ||||||
|  |     ICON_THERMOMETER, | ||||||
|  |     ICON_WATER_PERCENT, | ||||||
|  |     STATE_CLASS_MEASUREMENT, | ||||||
|  |     UNIT_CELSIUS, | ||||||
|  |     UNIT_PERCENT, | ||||||
|  |     UNIT_WATT, | ||||||
|  | ) | ||||||
|  | from esphome.components.climate import ( | ||||||
|  |     ClimateMode, | ||||||
|  |     ClimatePreset, | ||||||
|  |     ClimateSwingMode, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@dudanov"] | ||||||
|  | DEPENDENCIES = ["climate", "uart", "wifi"] | ||||||
|  | AUTO_LOAD = ["sensor"] | ||||||
|  | CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" | ||||||
|  | CONF_POWER_USAGE = "power_usage" | ||||||
|  | CONF_HUMIDITY_SETPOINT = "humidity_setpoint" | ||||||
|  | midea_ns = cg.esphome_ns.namespace("midea") | ||||||
|  | AirConditioner = midea_ns.class_("AirConditioner", climate.Climate, cg.Component) | ||||||
|  | Capabilities = midea_ns.namespace("Constants") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def templatize(value): | ||||||
|  |     if isinstance(value, cv.Schema): | ||||||
|  |         value = value.schema | ||||||
|  |     ret = {} | ||||||
|  |     for key, val in value.items(): | ||||||
|  |         ret[key] = cv.templatable(val) | ||||||
|  |     return cv.Schema(ret) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def register_action(name, type_, schema): | ||||||
|  |     validator = templatize(schema).extend(MIDEA_ACTION_BASE_SCHEMA) | ||||||
|  |     registerer = automation.register_action(f"midea_ac.{name}", type_, validator) | ||||||
|  |  | ||||||
|  |     def decorator(func): | ||||||
|  |         async def new_func(config, action_id, template_arg, args): | ||||||
|  |             ac_ = await cg.get_variable(config[CONF_ID]) | ||||||
|  |             var = cg.new_Pvariable(action_id, template_arg) | ||||||
|  |             cg.add(var.set_parent(ac_)) | ||||||
|  |             await coroutine(func)(var, config, args) | ||||||
|  |             return var | ||||||
|  |  | ||||||
|  |         return registerer(new_func) | ||||||
|  |  | ||||||
|  |     return decorator | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ALLOWED_CLIMATE_MODES = { | ||||||
|  |     "HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL, | ||||||
|  |     "COOL": ClimateMode.CLIMATE_MODE_COOL, | ||||||
|  |     "HEAT": ClimateMode.CLIMATE_MODE_HEAT, | ||||||
|  |     "DRY": ClimateMode.CLIMATE_MODE_DRY, | ||||||
|  |     "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ALLOWED_CLIMATE_PRESETS = { | ||||||
|  |     "ECO": ClimatePreset.CLIMATE_PRESET_ECO, | ||||||
|  |     "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, | ||||||
|  |     "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ALLOWED_CLIMATE_SWING_MODES = { | ||||||
|  |     "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, | ||||||
|  |     "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, | ||||||
|  |     "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | CUSTOM_FAN_MODES = { | ||||||
|  |     "SILENT": Capabilities.SILENT, | ||||||
|  |     "TURBO": Capabilities.TURBO, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | CUSTOM_PRESETS = { | ||||||
|  |     "FREEZE_PROTECTION": Capabilities.FREEZE_PROTECTION, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | validate_modes = cv.enum(ALLOWED_CLIMATE_MODES, upper=True) | ||||||
|  | validate_presets = cv.enum(ALLOWED_CLIMATE_PRESETS, upper=True) | ||||||
|  | validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True) | ||||||
|  | validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True) | ||||||
|  | validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.All( | ||||||
|  |     climate.CLIMATE_SCHEMA.extend( | ||||||
|  |         { | ||||||
|  |             cv.GenerateID(): cv.declare_id(AirConditioner), | ||||||
|  |             cv.Optional(CONF_PERIOD, default="1s"): cv.time_period, | ||||||
|  |             cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period, | ||||||
|  |             cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5), | ||||||
|  |             cv.Optional(CONF_TRANSMITTER_ID): cv.use_id( | ||||||
|  |                 remote_transmitter.RemoteTransmitterComponent | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_BEEPER, default=False): cv.boolean, | ||||||
|  |             cv.Optional(CONF_AUTOCONF, default=True): cv.boolean, | ||||||
|  |             cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(validate_modes), | ||||||
|  |             cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list( | ||||||
|  |                 validate_swing_modes | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_SUPPORTED_PRESETS): cv.ensure_list(validate_presets), | ||||||
|  |             cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(validate_custom_presets), | ||||||
|  |             cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list( | ||||||
|  |                 validate_custom_fan_modes | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_CELSIUS, | ||||||
|  |                 icon=ICON_THERMOMETER, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_WATT, | ||||||
|  |                 icon=ICON_POWER, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_POWER, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_PERCENT, | ||||||
|  |                 icon=ICON_WATER_PERCENT, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_HUMIDITY, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |     .extend(uart.UART_DEVICE_SCHEMA) | ||||||
|  |     .extend(cv.COMPONENT_SCHEMA) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # Actions | ||||||
|  | FollowMeAction = midea_ns.class_("FollowMeAction", automation.Action) | ||||||
|  | DisplayToggleAction = midea_ns.class_("DisplayToggleAction", automation.Action) | ||||||
|  | SwingStepAction = midea_ns.class_("SwingStepAction", automation.Action) | ||||||
|  | BeeperOnAction = midea_ns.class_("BeeperOnAction", automation.Action) | ||||||
|  | BeeperOffAction = midea_ns.class_("BeeperOffAction", automation.Action) | ||||||
|  | PowerOnAction = midea_ns.class_("PowerOnAction", automation.Action) | ||||||
|  | PowerOffAction = midea_ns.class_("PowerOffAction", automation.Action) | ||||||
|  |  | ||||||
|  | MIDEA_ACTION_BASE_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(CONF_ID): cv.use_id(AirConditioner), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # FollowMe action | ||||||
|  | MIDEA_FOLLOW_ME_MIN = 0 | ||||||
|  | MIDEA_FOLLOW_ME_MAX = 37 | ||||||
|  | MIDEA_FOLLOW_ME_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.Required(CONF_TEMPERATURE): cv.templatable(cv.temperature), | ||||||
|  |         cv.Optional(CONF_BEEPER, default=False): cv.templatable(cv.boolean), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @register_action("follow_me", FollowMeAction, MIDEA_FOLLOW_ME_SCHEMA) | ||||||
|  | async def follow_me_to_code(var, config, args): | ||||||
|  |     template_ = await cg.templatable(config[CONF_BEEPER], args, cg.bool_) | ||||||
|  |     cg.add(var.set_beeper(template_)) | ||||||
|  |     template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.float_) | ||||||
|  |     cg.add(var.set_temperature(template_)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Toggle Display action | ||||||
|  | @register_action( | ||||||
|  |     "display_toggle", | ||||||
|  |     DisplayToggleAction, | ||||||
|  |     cv.Schema({}), | ||||||
|  | ) | ||||||
|  | async def display_toggle_to_code(var, config, args): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Swing Step action | ||||||
|  | @register_action( | ||||||
|  |     "swing_step", | ||||||
|  |     SwingStepAction, | ||||||
|  |     cv.Schema({}), | ||||||
|  | ) | ||||||
|  | async def swing_step_to_code(var, config, args): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Beeper On action | ||||||
|  | @register_action( | ||||||
|  |     "beeper_on", | ||||||
|  |     BeeperOnAction, | ||||||
|  |     cv.Schema({}), | ||||||
|  | ) | ||||||
|  | async def beeper_on_to_code(var, config, args): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Beeper Off action | ||||||
|  | @register_action( | ||||||
|  |     "beeper_off", | ||||||
|  |     BeeperOffAction, | ||||||
|  |     cv.Schema({}), | ||||||
|  | ) | ||||||
|  | async def beeper_off_to_code(var, config, args): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Power On action | ||||||
|  | @register_action( | ||||||
|  |     "power_on", | ||||||
|  |     PowerOnAction, | ||||||
|  |     cv.Schema({}), | ||||||
|  | ) | ||||||
|  | async def power_on_to_code(var, config, args): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Power Off action | ||||||
|  | @register_action( | ||||||
|  |     "power_off", | ||||||
|  |     PowerOffAction, | ||||||
|  |     cv.Schema({}), | ||||||
|  | ) | ||||||
|  | async def power_off_to_code(var, config, args): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     await uart.register_uart_device(var, config) | ||||||
|  |     await climate.register_climate(var, config) | ||||||
|  |     cg.add(var.set_period(config[CONF_PERIOD].total_milliseconds)) | ||||||
|  |     cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds)) | ||||||
|  |     cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS])) | ||||||
|  |     if CONF_TRANSMITTER_ID in config: | ||||||
|  |         cg.add_define("USE_REMOTE_TRANSMITTER") | ||||||
|  |         transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID]) | ||||||
|  |         cg.add(var.set_transmitter(transmitter_)) | ||||||
|  |     cg.add(var.set_beeper_feedback(config[CONF_BEEPER])) | ||||||
|  |     cg.add(var.set_autoconf(config[CONF_AUTOCONF])) | ||||||
|  |     if CONF_SUPPORTED_MODES in config: | ||||||
|  |         cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) | ||||||
|  |     if CONF_SUPPORTED_SWING_MODES in config: | ||||||
|  |         cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES])) | ||||||
|  |     if CONF_SUPPORTED_PRESETS in config: | ||||||
|  |         cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) | ||||||
|  |     if CONF_CUSTOM_PRESETS in config: | ||||||
|  |         cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS])) | ||||||
|  |     if CONF_CUSTOM_FAN_MODES in config: | ||||||
|  |         cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES])) | ||||||
|  |     if CONF_OUTDOOR_TEMPERATURE in config: | ||||||
|  |         sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE]) | ||||||
|  |         cg.add(var.set_outdoor_temperature_sensor(sens)) | ||||||
|  |     if CONF_POWER_USAGE in config: | ||||||
|  |         sens = await sensor.new_sensor(config[CONF_POWER_USAGE]) | ||||||
|  |         cg.add(var.set_power_sensor(sens)) | ||||||
|  |     if CONF_HUMIDITY_SETPOINT in config: | ||||||
|  |         sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT]) | ||||||
|  |         cg.add(var.set_humidity_setpoint_sensor(sens)) | ||||||
|  |     cg.add_library("dudanov/MideaUART", "1.1.5") | ||||||
							
								
								
									
										42
									
								
								esphome/components/midea/midea_ir.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								esphome/components/midea/midea_ir.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | #pragma once | ||||||
|  | #ifdef USE_REMOTE_TRANSMITTER | ||||||
|  | #include "esphome/components/remote_base/midea_protocol.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace midea { | ||||||
|  |  | ||||||
|  | using IrData = remote_base::MideaData; | ||||||
|  |  | ||||||
|  | class IrFollowMeData : public IrData { | ||||||
|  |  public: | ||||||
|  |   // Default constructor (temp: 30C, beeper: off) | ||||||
|  |   IrFollowMeData() : IrData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {} | ||||||
|  |   // Copy from Base | ||||||
|  |   IrFollowMeData(const IrData &data) : IrData(data) {} | ||||||
|  |   // Direct from temperature and beeper values | ||||||
|  |   IrFollowMeData(uint8_t temp, bool beeper = false) : IrFollowMeData() { | ||||||
|  |     this->set_temp(temp); | ||||||
|  |     this->set_beeper(beeper); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /* TEMPERATURE */ | ||||||
|  |   uint8_t temp() const { return this->data_[4] - 1; } | ||||||
|  |   void set_temp(uint8_t val) { this->data_[4] = std::min(MAX_TEMP, val) + 1; } | ||||||
|  |  | ||||||
|  |   /* BEEPER */ | ||||||
|  |   bool beeper() const { return this->data_[3] & 128; } | ||||||
|  |   void set_beeper(bool val) { this->set_value_(3, 1, 7, val); } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   static const uint8_t MAX_TEMP = 37; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class IrSpecialData : public IrData { | ||||||
|  |  public: | ||||||
|  |   IrSpecialData(uint8_t code) : IrData({MIDEA_TYPE_SPECIAL, code, 0xFF, 0xFF, 0xFF}) {} | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace midea | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif | ||||||
| @@ -1,115 +1,3 @@ | |||||||
| from esphome.components import climate, sensor |  | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| import esphome.codegen as cg |  | ||||||
| from esphome.const import ( |  | ||||||
|     CONF_CUSTOM_FAN_MODES, |  | ||||||
|     CONF_CUSTOM_PRESETS, |  | ||||||
|     CONF_ID, |  | ||||||
|     CONF_PRESET_BOOST, |  | ||||||
|     CONF_PRESET_ECO, |  | ||||||
|     CONF_PRESET_SLEEP, |  | ||||||
|     STATE_CLASS_MEASUREMENT, |  | ||||||
|     UNIT_CELSIUS, |  | ||||||
|     UNIT_PERCENT, |  | ||||||
|     UNIT_WATT, |  | ||||||
|     ICON_THERMOMETER, |  | ||||||
|     ICON_POWER, |  | ||||||
|     DEVICE_CLASS_POWER, |  | ||||||
|     DEVICE_CLASS_TEMPERATURE, |  | ||||||
|     ICON_WATER_PERCENT, |  | ||||||
|     DEVICE_CLASS_HUMIDITY, |  | ||||||
| ) |  | ||||||
| from esphome.components.midea_dongle import CONF_MIDEA_DONGLE_ID, MideaDongle |  | ||||||
|  |  | ||||||
| AUTO_LOAD = ["climate", "sensor", "midea_dongle"] | CONFIG_SCHEMA = cv.invalid("This platform has been renamed to midea in 2021.9") | ||||||
| CODEOWNERS = ["@dudanov"] |  | ||||||
| CONF_BEEPER = "beeper" |  | ||||||
| CONF_SWING_HORIZONTAL = "swing_horizontal" |  | ||||||
| CONF_SWING_BOTH = "swing_both" |  | ||||||
| CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" |  | ||||||
| CONF_POWER_USAGE = "power_usage" |  | ||||||
| CONF_HUMIDITY_SETPOINT = "humidity_setpoint" |  | ||||||
| midea_ac_ns = cg.esphome_ns.namespace("midea_ac") |  | ||||||
| MideaAC = midea_ac_ns.class_("MideaAC", climate.Climate, cg.Component) |  | ||||||
|  |  | ||||||
| CLIMATE_CUSTOM_FAN_MODES = { |  | ||||||
|     "SILENT": "silent", |  | ||||||
|     "TURBO": "turbo", |  | ||||||
| } |  | ||||||
|  |  | ||||||
| validate_climate_custom_fan_mode = cv.enum(CLIMATE_CUSTOM_FAN_MODES, upper=True) |  | ||||||
|  |  | ||||||
| CLIMATE_CUSTOM_PRESETS = { |  | ||||||
|     "FREEZE_PROTECTION": "freeze protection", |  | ||||||
| } |  | ||||||
|  |  | ||||||
| validate_climate_custom_preset = cv.enum(CLIMATE_CUSTOM_PRESETS, upper=True) |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All( |  | ||||||
|     climate.CLIMATE_SCHEMA.extend( |  | ||||||
|         { |  | ||||||
|             cv.GenerateID(): cv.declare_id(MideaAC), |  | ||||||
|             cv.GenerateID(CONF_MIDEA_DONGLE_ID): cv.use_id(MideaDongle), |  | ||||||
|             cv.Optional(CONF_BEEPER, default=False): cv.boolean, |  | ||||||
|             cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list( |  | ||||||
|                 validate_climate_custom_fan_mode |  | ||||||
|             ), |  | ||||||
|             cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list( |  | ||||||
|                 validate_climate_custom_preset |  | ||||||
|             ), |  | ||||||
|             cv.Optional(CONF_SWING_HORIZONTAL, default=False): cv.boolean, |  | ||||||
|             cv.Optional(CONF_SWING_BOTH, default=False): cv.boolean, |  | ||||||
|             cv.Optional(CONF_PRESET_ECO, default=False): cv.boolean, |  | ||||||
|             cv.Optional(CONF_PRESET_SLEEP, default=False): cv.boolean, |  | ||||||
|             cv.Optional(CONF_PRESET_BOOST, default=False): cv.boolean, |  | ||||||
|             cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema( |  | ||||||
|                 unit_of_measurement=UNIT_CELSIUS, |  | ||||||
|                 icon=ICON_THERMOMETER, |  | ||||||
|                 accuracy_decimals=0, |  | ||||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, |  | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |  | ||||||
|             ), |  | ||||||
|             cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema( |  | ||||||
|                 unit_of_measurement=UNIT_WATT, |  | ||||||
|                 icon=ICON_POWER, |  | ||||||
|                 accuracy_decimals=0, |  | ||||||
|                 device_class=DEVICE_CLASS_POWER, |  | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |  | ||||||
|             ), |  | ||||||
|             cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema( |  | ||||||
|                 unit_of_measurement=UNIT_PERCENT, |  | ||||||
|                 icon=ICON_WATER_PERCENT, |  | ||||||
|                 accuracy_decimals=0, |  | ||||||
|                 device_class=DEVICE_CLASS_HUMIDITY, |  | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |  | ||||||
|             ), |  | ||||||
|         } |  | ||||||
|     ).extend(cv.COMPONENT_SCHEMA) |  | ||||||
| ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): |  | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |  | ||||||
|     await cg.register_component(var, config) |  | ||||||
|     await climate.register_climate(var, config) |  | ||||||
|     paren = await cg.get_variable(config[CONF_MIDEA_DONGLE_ID]) |  | ||||||
|     cg.add(var.set_midea_dongle_parent(paren)) |  | ||||||
|     cg.add(var.set_beeper_feedback(config[CONF_BEEPER])) |  | ||||||
|     if CONF_CUSTOM_FAN_MODES in config: |  | ||||||
|         cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES])) |  | ||||||
|     if CONF_CUSTOM_PRESETS in config: |  | ||||||
|         cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS])) |  | ||||||
|     cg.add(var.set_swing_horizontal(config[CONF_SWING_HORIZONTAL])) |  | ||||||
|     cg.add(var.set_swing_both(config[CONF_SWING_BOTH])) |  | ||||||
|     cg.add(var.set_preset_eco(config[CONF_PRESET_ECO])) |  | ||||||
|     cg.add(var.set_preset_sleep(config[CONF_PRESET_SLEEP])) |  | ||||||
|     cg.add(var.set_preset_boost(config[CONF_PRESET_BOOST])) |  | ||||||
|     if CONF_OUTDOOR_TEMPERATURE in config: |  | ||||||
|         sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE]) |  | ||||||
|         cg.add(var.set_outdoor_temperature_sensor(sens)) |  | ||||||
|     if CONF_POWER_USAGE in config: |  | ||||||
|         sens = await sensor.new_sensor(config[CONF_POWER_USAGE]) |  | ||||||
|         cg.add(var.set_power_sensor(sens)) |  | ||||||
|     if CONF_HUMIDITY_SETPOINT in config: |  | ||||||
|         sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT]) |  | ||||||
|         cg.add(var.set_humidity_setpoint_sensor(sens)) |  | ||||||
|   | |||||||
| @@ -1,208 +0,0 @@ | |||||||
| #include "esphome/core/log.h" |  | ||||||
| #include "midea_climate.h" |  | ||||||
|  |  | ||||||
| namespace esphome { |  | ||||||
| namespace midea_ac { |  | ||||||
|  |  | ||||||
| static const char *const TAG = "midea_ac"; |  | ||||||
|  |  | ||||||
| static void set_sensor(sensor::Sensor *sensor, float value) { |  | ||||||
|   if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value)) |  | ||||||
|     sensor->publish_state(value); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| template<typename T> void set_property(T &property, T value, bool &flag) { |  | ||||||
|   if (property != value) { |  | ||||||
|     property = value; |  | ||||||
|     flag = true; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MideaAC::on_frame(const midea_dongle::Frame &frame) { |  | ||||||
|   const auto p = frame.as<PropertiesFrame>(); |  | ||||||
|   if (p.has_power_info()) { |  | ||||||
|     set_sensor(this->power_sensor_, p.get_power_usage()); |  | ||||||
|     return; |  | ||||||
|   } else if (!p.has_properties()) { |  | ||||||
|     ESP_LOGW(TAG, "RX: frame has unknown type"); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   if (p.get_type() == midea_dongle::MideaMessageType::DEVICE_CONTROL) { |  | ||||||
|     ESP_LOGD(TAG, "RX: control frame"); |  | ||||||
|     this->ctrl_request_ = false; |  | ||||||
|   } else { |  | ||||||
|     ESP_LOGD(TAG, "RX: query frame"); |  | ||||||
|   } |  | ||||||
|   if (this->ctrl_request_) |  | ||||||
|     return; |  | ||||||
|   this->cmd_frame_.set_properties(p);  // copy properties from response |  | ||||||
|   bool need_publish = false; |  | ||||||
|   set_property(this->mode, p.get_mode(), need_publish); |  | ||||||
|   set_property(this->target_temperature, p.get_target_temp(), need_publish); |  | ||||||
|   set_property(this->current_temperature, p.get_indoor_temp(), need_publish); |  | ||||||
|   if (p.is_custom_fan_mode()) { |  | ||||||
|     this->fan_mode.reset(); |  | ||||||
|     optional<std::string> mode = p.get_custom_fan_mode(); |  | ||||||
|     set_property(this->custom_fan_mode, mode, need_publish); |  | ||||||
|   } else { |  | ||||||
|     this->custom_fan_mode.reset(); |  | ||||||
|     optional<climate::ClimateFanMode> mode = p.get_fan_mode(); |  | ||||||
|     set_property(this->fan_mode, mode, need_publish); |  | ||||||
|   } |  | ||||||
|   set_property(this->swing_mode, p.get_swing_mode(), need_publish); |  | ||||||
|   if (p.is_custom_preset()) { |  | ||||||
|     this->preset.reset(); |  | ||||||
|     optional<std::string> preset = p.get_custom_preset(); |  | ||||||
|     set_property(this->custom_preset, preset, need_publish); |  | ||||||
|   } else { |  | ||||||
|     this->custom_preset.reset(); |  | ||||||
|     set_property(this->preset, p.get_preset(), need_publish); |  | ||||||
|   } |  | ||||||
|   if (need_publish) |  | ||||||
|     this->publish_state(); |  | ||||||
|   set_sensor(this->outdoor_sensor_, p.get_outdoor_temp()); |  | ||||||
|   set_sensor(this->humidity_sensor_, p.get_humidity_setpoint()); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MideaAC::on_update() { |  | ||||||
|   if (this->ctrl_request_) { |  | ||||||
|     ESP_LOGD(TAG, "TX: control"); |  | ||||||
|     this->parent_->write_frame(this->cmd_frame_); |  | ||||||
|   } else { |  | ||||||
|     ESP_LOGD(TAG, "TX: query"); |  | ||||||
|     if (this->power_sensor_ == nullptr || this->request_num_++ % 32) |  | ||||||
|       this->parent_->write_frame(this->query_frame_); |  | ||||||
|     else |  | ||||||
|       this->parent_->write_frame(this->power_frame_); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool MideaAC::allow_preset(climate::ClimatePreset preset) const { |  | ||||||
|   switch (preset) { |  | ||||||
|     case climate::CLIMATE_PRESET_ECO: |  | ||||||
|       if (this->mode == climate::CLIMATE_MODE_COOL) { |  | ||||||
|         return true; |  | ||||||
|       } else { |  | ||||||
|         ESP_LOGD(TAG, "ECO preset is only available in COOL mode"); |  | ||||||
|       } |  | ||||||
|       break; |  | ||||||
|     case climate::CLIMATE_PRESET_SLEEP: |  | ||||||
|       if (this->mode == climate::CLIMATE_MODE_FAN_ONLY || this->mode == climate::CLIMATE_MODE_DRY) { |  | ||||||
|         ESP_LOGD(TAG, "SLEEP preset is not available in FAN_ONLY or DRY mode"); |  | ||||||
|       } else { |  | ||||||
|         return true; |  | ||||||
|       } |  | ||||||
|       break; |  | ||||||
|     case climate::CLIMATE_PRESET_BOOST: |  | ||||||
|       if (this->mode == climate::CLIMATE_MODE_HEAT || this->mode == climate::CLIMATE_MODE_COOL) { |  | ||||||
|         return true; |  | ||||||
|       } else { |  | ||||||
|         ESP_LOGD(TAG, "BOOST preset is only available in HEAT or COOL mode"); |  | ||||||
|       } |  | ||||||
|       break; |  | ||||||
|     case climate::CLIMATE_PRESET_NONE: |  | ||||||
|       return true; |  | ||||||
|     default: |  | ||||||
|       break; |  | ||||||
|   } |  | ||||||
|   return false; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool MideaAC::allow_custom_preset(const std::string &custom_preset) const { |  | ||||||
|   if (custom_preset == MIDEA_FREEZE_PROTECTION_PRESET) { |  | ||||||
|     if (this->mode == climate::CLIMATE_MODE_HEAT) { |  | ||||||
|       return true; |  | ||||||
|     } else { |  | ||||||
|       ESP_LOGD(TAG, "%s is only available in HEAT mode", MIDEA_FREEZE_PROTECTION_PRESET.c_str()); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   return false; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void MideaAC::control(const climate::ClimateCall &call) { |  | ||||||
|   if (call.get_mode().has_value() && call.get_mode().value() != this->mode) { |  | ||||||
|     this->cmd_frame_.set_mode(call.get_mode().value()); |  | ||||||
|     this->ctrl_request_ = true; |  | ||||||
|   } |  | ||||||
|   if (call.get_target_temperature().has_value() && call.get_target_temperature().value() != this->target_temperature) { |  | ||||||
|     this->cmd_frame_.set_target_temp(call.get_target_temperature().value()); |  | ||||||
|     this->ctrl_request_ = true; |  | ||||||
|   } |  | ||||||
|   if (call.get_fan_mode().has_value() && |  | ||||||
|       (!this->fan_mode.has_value() || this->fan_mode.value() != call.get_fan_mode().value())) { |  | ||||||
|     this->custom_fan_mode.reset(); |  | ||||||
|     this->cmd_frame_.set_fan_mode(call.get_fan_mode().value()); |  | ||||||
|     this->ctrl_request_ = true; |  | ||||||
|   } |  | ||||||
|   if (call.get_custom_fan_mode().has_value() && |  | ||||||
|       (!this->custom_fan_mode.has_value() || this->custom_fan_mode.value() != call.get_custom_fan_mode().value())) { |  | ||||||
|     this->fan_mode.reset(); |  | ||||||
|     this->cmd_frame_.set_custom_fan_mode(call.get_custom_fan_mode().value()); |  | ||||||
|     this->ctrl_request_ = true; |  | ||||||
|   } |  | ||||||
|   if (call.get_swing_mode().has_value() && call.get_swing_mode().value() != this->swing_mode) { |  | ||||||
|     this->cmd_frame_.set_swing_mode(call.get_swing_mode().value()); |  | ||||||
|     this->ctrl_request_ = true; |  | ||||||
|   } |  | ||||||
|   if (call.get_preset().has_value() && this->allow_preset(call.get_preset().value()) && |  | ||||||
|       (!this->preset.has_value() || this->preset.value() != call.get_preset().value())) { |  | ||||||
|     this->custom_preset.reset(); |  | ||||||
|     this->cmd_frame_.set_preset(call.get_preset().value()); |  | ||||||
|     this->ctrl_request_ = true; |  | ||||||
|   } |  | ||||||
|   if (call.get_custom_preset().has_value() && this->allow_custom_preset(call.get_custom_preset().value()) && |  | ||||||
|       (!this->custom_preset.has_value() || this->custom_preset.value() != call.get_custom_preset().value())) { |  | ||||||
|     this->preset.reset(); |  | ||||||
|     this->cmd_frame_.set_custom_preset(call.get_custom_preset().value()); |  | ||||||
|     this->ctrl_request_ = true; |  | ||||||
|   } |  | ||||||
|   if (this->ctrl_request_) { |  | ||||||
|     this->cmd_frame_.set_beeper_feedback(this->beeper_feedback_); |  | ||||||
|     this->cmd_frame_.finalize(); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| climate::ClimateTraits MideaAC::traits() { |  | ||||||
|   auto traits = climate::ClimateTraits(); |  | ||||||
|   traits.set_visual_min_temperature(17); |  | ||||||
|   traits.set_visual_max_temperature(30); |  | ||||||
|   traits.set_visual_temperature_step(0.5); |  | ||||||
|   traits.set_supported_modes({ |  | ||||||
|       climate::CLIMATE_MODE_OFF, |  | ||||||
|       climate::CLIMATE_MODE_HEAT_COOL, |  | ||||||
|       climate::CLIMATE_MODE_COOL, |  | ||||||
|       climate::CLIMATE_MODE_DRY, |  | ||||||
|       climate::CLIMATE_MODE_HEAT, |  | ||||||
|       climate::CLIMATE_MODE_FAN_ONLY, |  | ||||||
|   }); |  | ||||||
|   traits.set_supported_fan_modes({ |  | ||||||
|       climate::CLIMATE_FAN_AUTO, |  | ||||||
|       climate::CLIMATE_FAN_LOW, |  | ||||||
|       climate::CLIMATE_FAN_MEDIUM, |  | ||||||
|       climate::CLIMATE_FAN_HIGH, |  | ||||||
|   }); |  | ||||||
|   traits.set_supported_custom_fan_modes(this->traits_custom_fan_modes_); |  | ||||||
|   traits.set_supported_swing_modes({ |  | ||||||
|       climate::CLIMATE_SWING_OFF, |  | ||||||
|       climate::CLIMATE_SWING_VERTICAL, |  | ||||||
|   }); |  | ||||||
|   if (traits_swing_horizontal_) |  | ||||||
|     traits.add_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL); |  | ||||||
|   if (traits_swing_both_) |  | ||||||
|     traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH); |  | ||||||
|   traits.set_supported_presets({ |  | ||||||
|       climate::CLIMATE_PRESET_NONE, |  | ||||||
|   }); |  | ||||||
|   if (traits_preset_eco_) |  | ||||||
|     traits.add_supported_preset(climate::CLIMATE_PRESET_ECO); |  | ||||||
|   if (traits_preset_sleep_) |  | ||||||
|     traits.add_supported_preset(climate::CLIMATE_PRESET_SLEEP); |  | ||||||
|   if (traits_preset_boost_) |  | ||||||
|     traits.add_supported_preset(climate::CLIMATE_PRESET_BOOST); |  | ||||||
|   traits.set_supported_custom_presets(this->traits_custom_presets_); |  | ||||||
|   traits.set_supports_current_temperature(true); |  | ||||||
|   return traits; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| }  // namespace midea_ac |  | ||||||
| }  // namespace esphome |  | ||||||
| @@ -1,68 +0,0 @@ | |||||||
| #pragma once |  | ||||||
|  |  | ||||||
| #include <utility> |  | ||||||
|  |  | ||||||
| #include "esphome/core/component.h" |  | ||||||
| #include "esphome/components/sensor/sensor.h" |  | ||||||
| #include "esphome/components/midea_dongle/midea_dongle.h" |  | ||||||
| #include "esphome/components/climate/climate.h" |  | ||||||
| #include "esphome/components/midea_dongle/midea_dongle.h" |  | ||||||
| #include "esphome/components/sensor/sensor.h" |  | ||||||
| #include "esphome/core/component.h" |  | ||||||
| #include "midea_frame.h" |  | ||||||
|  |  | ||||||
| namespace esphome { |  | ||||||
| namespace midea_ac { |  | ||||||
|  |  | ||||||
| class MideaAC : public midea_dongle::MideaAppliance, public climate::Climate, public Component { |  | ||||||
|  public: |  | ||||||
|   float get_setup_priority() const override { return setup_priority::LATE; } |  | ||||||
|   void on_frame(const midea_dongle::Frame &frame) override; |  | ||||||
|   void on_update() override; |  | ||||||
|   void setup() override { this->parent_->set_appliance(this); } |  | ||||||
|   void set_midea_dongle_parent(midea_dongle::MideaDongle *parent) { this->parent_ = parent; } |  | ||||||
|   void set_outdoor_temperature_sensor(sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; } |  | ||||||
|   void set_humidity_setpoint_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; } |  | ||||||
|   void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; } |  | ||||||
|   void set_beeper_feedback(bool state) { this->beeper_feedback_ = state; } |  | ||||||
|   void set_swing_horizontal(bool state) { this->traits_swing_horizontal_ = state; } |  | ||||||
|   void set_swing_both(bool state) { this->traits_swing_both_ = state; } |  | ||||||
|   void set_preset_eco(bool state) { this->traits_preset_eco_ = state; } |  | ||||||
|   void set_preset_sleep(bool state) { this->traits_preset_sleep_ = state; } |  | ||||||
|   void set_preset_boost(bool state) { this->traits_preset_boost_ = state; } |  | ||||||
|   bool allow_preset(climate::ClimatePreset preset) const; |  | ||||||
|   void set_custom_fan_modes(std::set<std::string> custom_fan_modes) { |  | ||||||
|     this->traits_custom_fan_modes_ = std::move(custom_fan_modes); |  | ||||||
|   } |  | ||||||
|   void set_custom_presets(std::set<std::string> custom_presets) { |  | ||||||
|     this->traits_custom_presets_ = std::move(custom_presets); |  | ||||||
|   } |  | ||||||
|   bool allow_custom_preset(const std::string &custom_preset) const; |  | ||||||
|  |  | ||||||
|  protected: |  | ||||||
|   /// Override control to change settings of the climate device. |  | ||||||
|   void control(const climate::ClimateCall &call) override; |  | ||||||
|   /// Return the traits of this controller. |  | ||||||
|   climate::ClimateTraits traits() override; |  | ||||||
|  |  | ||||||
|   const QueryFrame query_frame_; |  | ||||||
|   const PowerQueryFrame power_frame_; |  | ||||||
|   CommandFrame cmd_frame_; |  | ||||||
|   midea_dongle::MideaDongle *parent_{nullptr}; |  | ||||||
|   sensor::Sensor *outdoor_sensor_{nullptr}; |  | ||||||
|   sensor::Sensor *humidity_sensor_{nullptr}; |  | ||||||
|   sensor::Sensor *power_sensor_{nullptr}; |  | ||||||
|   uint8_t request_num_{0}; |  | ||||||
|   bool ctrl_request_{false}; |  | ||||||
|   bool beeper_feedback_{false}; |  | ||||||
|   bool traits_swing_horizontal_{false}; |  | ||||||
|   bool traits_swing_both_{false}; |  | ||||||
|   bool traits_preset_eco_{false}; |  | ||||||
|   bool traits_preset_sleep_{false}; |  | ||||||
|   bool traits_preset_boost_{false}; |  | ||||||
|   std::set<std::string> traits_custom_fan_modes_{{}}; |  | ||||||
|   std::set<std::string> traits_custom_presets_{{}}; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| }  // namespace midea_ac |  | ||||||
| }  // namespace esphome |  | ||||||
| @@ -1,238 +0,0 @@ | |||||||
| #include "midea_frame.h" |  | ||||||
|  |  | ||||||
| namespace esphome { |  | ||||||
| namespace midea_ac { |  | ||||||
|  |  | ||||||
| static const char *const TAG = "midea_ac"; |  | ||||||
| const std::string MIDEA_SILENT_FAN_MODE = "silent"; |  | ||||||
| const std::string MIDEA_TURBO_FAN_MODE = "turbo"; |  | ||||||
| const std::string MIDEA_FREEZE_PROTECTION_PRESET = "freeze protection"; |  | ||||||
|  |  | ||||||
| const uint8_t QueryFrame::INIT[] = {0xAA, 0x21, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x81, |  | ||||||
|                                     0x00, 0xFF, 0x03, 0xFF, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |  | ||||||
|                                     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x37, 0x31}; |  | ||||||
|  |  | ||||||
| const uint8_t PowerQueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x21, |  | ||||||
|                                          0x01, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |  | ||||||
|                                          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x17, 0x6A}; |  | ||||||
|  |  | ||||||
| const uint8_t CommandFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x40, 0x00, |  | ||||||
|                                       0x00, 0x00, 0x7F, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |  | ||||||
|                                       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; |  | ||||||
|  |  | ||||||
| float PropertiesFrame::get_target_temp() const { |  | ||||||
|   float temp = static_cast<float>((this->pbuf_[12] & 0x0F) + 16); |  | ||||||
|   if (this->pbuf_[12] & 0x10) |  | ||||||
|     temp += 0.5; |  | ||||||
|   return temp; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PropertiesFrame::set_target_temp(float temp) { |  | ||||||
|   uint8_t tmp = static_cast<uint8_t>(temp * 16.0) + 4; |  | ||||||
|   tmp = ((tmp & 8) << 1) | (tmp >> 4); |  | ||||||
|   this->pbuf_[12] &= ~0x1F; |  | ||||||
|   this->pbuf_[12] |= tmp; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| static float i16tof(int16_t in) { return static_cast<float>(in - 50) / 2.0; } |  | ||||||
| float PropertiesFrame::get_indoor_temp() const { return i16tof(this->pbuf_[21]); } |  | ||||||
| float PropertiesFrame::get_outdoor_temp() const { return i16tof(this->pbuf_[22]); } |  | ||||||
| float PropertiesFrame::get_humidity_setpoint() const { return static_cast<float>(this->pbuf_[29] & 0x7F); } |  | ||||||
|  |  | ||||||
| climate::ClimateMode PropertiesFrame::get_mode() const { |  | ||||||
|   if (!this->get_power_()) |  | ||||||
|     return climate::CLIMATE_MODE_OFF; |  | ||||||
|   switch (this->pbuf_[12] >> 5) { |  | ||||||
|     case MIDEA_MODE_AUTO: |  | ||||||
|       return climate::CLIMATE_MODE_HEAT_COOL; |  | ||||||
|     case MIDEA_MODE_COOL: |  | ||||||
|       return climate::CLIMATE_MODE_COOL; |  | ||||||
|     case MIDEA_MODE_DRY: |  | ||||||
|       return climate::CLIMATE_MODE_DRY; |  | ||||||
|     case MIDEA_MODE_HEAT: |  | ||||||
|       return climate::CLIMATE_MODE_HEAT; |  | ||||||
|     case MIDEA_MODE_FAN_ONLY: |  | ||||||
|       return climate::CLIMATE_MODE_FAN_ONLY; |  | ||||||
|     default: |  | ||||||
|       return climate::CLIMATE_MODE_OFF; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PropertiesFrame::set_mode(climate::ClimateMode mode) { |  | ||||||
|   uint8_t m; |  | ||||||
|   switch (mode) { |  | ||||||
|     case climate::CLIMATE_MODE_HEAT_COOL: |  | ||||||
|       m = MIDEA_MODE_AUTO; |  | ||||||
|       break; |  | ||||||
|     case climate::CLIMATE_MODE_COOL: |  | ||||||
|       m = MIDEA_MODE_COOL; |  | ||||||
|       break; |  | ||||||
|     case climate::CLIMATE_MODE_DRY: |  | ||||||
|       m = MIDEA_MODE_DRY; |  | ||||||
|       break; |  | ||||||
|     case climate::CLIMATE_MODE_HEAT: |  | ||||||
|       m = MIDEA_MODE_HEAT; |  | ||||||
|       break; |  | ||||||
|     case climate::CLIMATE_MODE_FAN_ONLY: |  | ||||||
|       m = MIDEA_MODE_FAN_ONLY; |  | ||||||
|       break; |  | ||||||
|     default: |  | ||||||
|       this->set_power_(false); |  | ||||||
|       return; |  | ||||||
|   } |  | ||||||
|   this->set_power_(true); |  | ||||||
|   this->pbuf_[12] &= ~0xE0; |  | ||||||
|   this->pbuf_[12] |= m << 5; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| optional<climate::ClimatePreset> PropertiesFrame::get_preset() const { |  | ||||||
|   if (this->get_eco_mode()) |  | ||||||
|     return climate::CLIMATE_PRESET_ECO; |  | ||||||
|   if (this->get_sleep_mode()) |  | ||||||
|     return climate::CLIMATE_PRESET_SLEEP; |  | ||||||
|   if (this->get_turbo_mode()) |  | ||||||
|     return climate::CLIMATE_PRESET_BOOST; |  | ||||||
|   return climate::CLIMATE_PRESET_NONE; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PropertiesFrame::set_preset(climate::ClimatePreset preset) { |  | ||||||
|   this->clear_presets(); |  | ||||||
|   switch (preset) { |  | ||||||
|     case climate::CLIMATE_PRESET_ECO: |  | ||||||
|       this->set_eco_mode(true); |  | ||||||
|       break; |  | ||||||
|     case climate::CLIMATE_PRESET_SLEEP: |  | ||||||
|       this->set_sleep_mode(true); |  | ||||||
|       break; |  | ||||||
|     case climate::CLIMATE_PRESET_BOOST: |  | ||||||
|       this->set_turbo_mode(true); |  | ||||||
|       break; |  | ||||||
|     default: |  | ||||||
|       break; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PropertiesFrame::clear_presets() { |  | ||||||
|   this->set_eco_mode(false); |  | ||||||
|   this->set_sleep_mode(false); |  | ||||||
|   this->set_turbo_mode(false); |  | ||||||
|   this->set_freeze_protection_mode(false); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool PropertiesFrame::is_custom_preset() const { return this->get_freeze_protection_mode(); } |  | ||||||
|  |  | ||||||
| const std::string &PropertiesFrame::get_custom_preset() const { return midea_ac::MIDEA_FREEZE_PROTECTION_PRESET; }; |  | ||||||
|  |  | ||||||
| void PropertiesFrame::set_custom_preset(const std::string &preset) { |  | ||||||
|   this->clear_presets(); |  | ||||||
|   if (preset == MIDEA_FREEZE_PROTECTION_PRESET) |  | ||||||
|     this->set_freeze_protection_mode(true); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool PropertiesFrame::is_custom_fan_mode() const { |  | ||||||
|   switch (this->pbuf_[13]) { |  | ||||||
|     case MIDEA_FAN_SILENT: |  | ||||||
|     case MIDEA_FAN_TURBO: |  | ||||||
|       return true; |  | ||||||
|     default: |  | ||||||
|       return false; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| climate::ClimateFanMode PropertiesFrame::get_fan_mode() const { |  | ||||||
|   switch (this->pbuf_[13]) { |  | ||||||
|     case MIDEA_FAN_LOW: |  | ||||||
|       return climate::CLIMATE_FAN_LOW; |  | ||||||
|     case MIDEA_FAN_MEDIUM: |  | ||||||
|       return climate::CLIMATE_FAN_MEDIUM; |  | ||||||
|     case MIDEA_FAN_HIGH: |  | ||||||
|       return climate::CLIMATE_FAN_HIGH; |  | ||||||
|     default: |  | ||||||
|       return climate::CLIMATE_FAN_AUTO; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PropertiesFrame::set_fan_mode(climate::ClimateFanMode mode) { |  | ||||||
|   uint8_t m; |  | ||||||
|   switch (mode) { |  | ||||||
|     case climate::CLIMATE_FAN_LOW: |  | ||||||
|       m = MIDEA_FAN_LOW; |  | ||||||
|       break; |  | ||||||
|     case climate::CLIMATE_FAN_MEDIUM: |  | ||||||
|       m = MIDEA_FAN_MEDIUM; |  | ||||||
|       break; |  | ||||||
|     case climate::CLIMATE_FAN_HIGH: |  | ||||||
|       m = MIDEA_FAN_HIGH; |  | ||||||
|       break; |  | ||||||
|     default: |  | ||||||
|       m = MIDEA_FAN_AUTO; |  | ||||||
|       break; |  | ||||||
|   } |  | ||||||
|   this->pbuf_[13] = m; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const std::string &PropertiesFrame::get_custom_fan_mode() const { |  | ||||||
|   switch (this->pbuf_[13]) { |  | ||||||
|     case MIDEA_FAN_SILENT: |  | ||||||
|       return MIDEA_SILENT_FAN_MODE; |  | ||||||
|     default: |  | ||||||
|       return MIDEA_TURBO_FAN_MODE; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PropertiesFrame::set_custom_fan_mode(const std::string &mode) { |  | ||||||
|   uint8_t m; |  | ||||||
|   if (mode == MIDEA_SILENT_FAN_MODE) { |  | ||||||
|     m = MIDEA_FAN_SILENT; |  | ||||||
|   } else { |  | ||||||
|     m = MIDEA_FAN_TURBO; |  | ||||||
|   } |  | ||||||
|   this->pbuf_[13] = m; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| climate::ClimateSwingMode PropertiesFrame::get_swing_mode() const { |  | ||||||
|   switch (this->pbuf_[17] & 0x0F) { |  | ||||||
|     case MIDEA_SWING_VERTICAL: |  | ||||||
|       return climate::CLIMATE_SWING_VERTICAL; |  | ||||||
|     case MIDEA_SWING_HORIZONTAL: |  | ||||||
|       return climate::CLIMATE_SWING_HORIZONTAL; |  | ||||||
|     case MIDEA_SWING_BOTH: |  | ||||||
|       return climate::CLIMATE_SWING_BOTH; |  | ||||||
|     default: |  | ||||||
|       return climate::CLIMATE_SWING_OFF; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void PropertiesFrame::set_swing_mode(climate::ClimateSwingMode mode) { |  | ||||||
|   uint8_t m; |  | ||||||
|   switch (mode) { |  | ||||||
|     case climate::CLIMATE_SWING_VERTICAL: |  | ||||||
|       m = MIDEA_SWING_VERTICAL; |  | ||||||
|       break; |  | ||||||
|     case climate::CLIMATE_SWING_HORIZONTAL: |  | ||||||
|       m = MIDEA_SWING_HORIZONTAL; |  | ||||||
|       break; |  | ||||||
|     case climate::CLIMATE_SWING_BOTH: |  | ||||||
|       m = MIDEA_SWING_BOTH; |  | ||||||
|       break; |  | ||||||
|     default: |  | ||||||
|       m = MIDEA_SWING_OFF; |  | ||||||
|       break; |  | ||||||
|   } |  | ||||||
|   this->pbuf_[17] = 0x30 | m; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| float PropertiesFrame::get_power_usage() const { |  | ||||||
|   uint32_t power = 0; |  | ||||||
|   const uint8_t *ptr = this->pbuf_ + 28; |  | ||||||
|   for (uint32_t weight = 1;; weight *= 10, ptr--) { |  | ||||||
|     power += (*ptr % 16) * weight; |  | ||||||
|     weight *= 10; |  | ||||||
|     power += (*ptr / 16) * weight; |  | ||||||
|     if (weight == 100000) |  | ||||||
|       return static_cast<float>(power) * 0.1; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| }  // namespace midea_ac |  | ||||||
| }  // namespace esphome |  | ||||||
| @@ -1,165 +0,0 @@ | |||||||
| #pragma once |  | ||||||
| #include "esphome/components/climate/climate.h" |  | ||||||
| #include "esphome/components/midea_dongle/midea_frame.h" |  | ||||||
|  |  | ||||||
| namespace esphome { |  | ||||||
| namespace midea_ac { |  | ||||||
|  |  | ||||||
| extern const std::string MIDEA_SILENT_FAN_MODE; |  | ||||||
| extern const std::string MIDEA_TURBO_FAN_MODE; |  | ||||||
| extern const std::string MIDEA_FREEZE_PROTECTION_PRESET; |  | ||||||
|  |  | ||||||
| /// Enum for all modes a Midea device can be in. |  | ||||||
| enum MideaMode : uint8_t { |  | ||||||
|   /// The Midea device is set to automatically change the heating/cooling cycle |  | ||||||
|   MIDEA_MODE_AUTO = 1, |  | ||||||
|   /// The Midea device is manually set to cool mode (not in auto mode!) |  | ||||||
|   MIDEA_MODE_COOL = 2, |  | ||||||
|   /// The Midea device is manually set to dry mode |  | ||||||
|   MIDEA_MODE_DRY = 3, |  | ||||||
|   /// The Midea device is manually set to heat mode (not in auto mode!) |  | ||||||
|   MIDEA_MODE_HEAT = 4, |  | ||||||
|   /// The Midea device is manually set to fan only mode |  | ||||||
|   MIDEA_MODE_FAN_ONLY = 5, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| /// Enum for all modes a Midea fan can be in |  | ||||||
| enum MideaFanMode : uint8_t { |  | ||||||
|   /// The fan mode is set to Auto |  | ||||||
|   MIDEA_FAN_AUTO = 102, |  | ||||||
|   /// The fan mode is set to Silent |  | ||||||
|   MIDEA_FAN_SILENT = 20, |  | ||||||
|   /// The fan mode is set to Low |  | ||||||
|   MIDEA_FAN_LOW = 40, |  | ||||||
|   /// The fan mode is set to Medium |  | ||||||
|   MIDEA_FAN_MEDIUM = 60, |  | ||||||
|   /// The fan mode is set to High |  | ||||||
|   MIDEA_FAN_HIGH = 80, |  | ||||||
|   /// The fan mode is set to Turbo |  | ||||||
|   MIDEA_FAN_TURBO = 100, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| /// Enum for all modes a Midea swing can be in |  | ||||||
| enum MideaSwingMode : uint8_t { |  | ||||||
|   /// The sing mode is set to Off |  | ||||||
|   MIDEA_SWING_OFF = 0b0000, |  | ||||||
|   /// The fan mode is set to Both |  | ||||||
|   MIDEA_SWING_BOTH = 0b1111, |  | ||||||
|   /// The fan mode is set to Vertical |  | ||||||
|   MIDEA_SWING_VERTICAL = 0b1100, |  | ||||||
|   /// The fan mode is set to Horizontal |  | ||||||
|   MIDEA_SWING_HORIZONTAL = 0b0011, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| class PropertiesFrame : public midea_dongle::BaseFrame { |  | ||||||
|  public: |  | ||||||
|   PropertiesFrame() = delete; |  | ||||||
|   PropertiesFrame(uint8_t *data) : BaseFrame(data) {} |  | ||||||
|   PropertiesFrame(const Frame &frame) : BaseFrame(frame) {} |  | ||||||
|  |  | ||||||
|   bool has_properties() const { |  | ||||||
|     return this->has_response_type(0xC0) && (this->has_type(0x03) || this->has_type(0x02)); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   bool has_power_info() const { return this->has_response_type(0xC1); } |  | ||||||
|  |  | ||||||
|   /* TARGET TEMPERATURE */ |  | ||||||
|  |  | ||||||
|   float get_target_temp() const; |  | ||||||
|   void set_target_temp(float temp); |  | ||||||
|  |  | ||||||
|   /* MODE */ |  | ||||||
|   climate::ClimateMode get_mode() const; |  | ||||||
|   void set_mode(climate::ClimateMode mode); |  | ||||||
|  |  | ||||||
|   /* FAN SPEED */ |  | ||||||
|   bool is_custom_fan_mode() const; |  | ||||||
|   climate::ClimateFanMode get_fan_mode() const; |  | ||||||
|   void set_fan_mode(climate::ClimateFanMode mode); |  | ||||||
|  |  | ||||||
|   const std::string &get_custom_fan_mode() const; |  | ||||||
|   void set_custom_fan_mode(const std::string &mode); |  | ||||||
|  |  | ||||||
|   /* SWING MODE */ |  | ||||||
|   climate::ClimateSwingMode get_swing_mode() const; |  | ||||||
|   void set_swing_mode(climate::ClimateSwingMode mode); |  | ||||||
|  |  | ||||||
|   /* INDOOR TEMPERATURE */ |  | ||||||
|   float get_indoor_temp() const; |  | ||||||
|  |  | ||||||
|   /* OUTDOOR TEMPERATURE */ |  | ||||||
|   float get_outdoor_temp() const; |  | ||||||
|  |  | ||||||
|   /* HUMIDITY SETPOINT */ |  | ||||||
|   float get_humidity_setpoint() const; |  | ||||||
|  |  | ||||||
|   /* ECO MODE */ |  | ||||||
|   bool get_eco_mode() const { return this->pbuf_[19] & 0x10; } |  | ||||||
|   void set_eco_mode(bool state) { this->set_bytemask_(19, 0x80, state); } |  | ||||||
|  |  | ||||||
|   /* SLEEP MODE */ |  | ||||||
|   bool get_sleep_mode() const { return this->pbuf_[20] & 0x01; } |  | ||||||
|   void set_sleep_mode(bool state) { this->set_bytemask_(20, 0x01, state); } |  | ||||||
|  |  | ||||||
|   /* TURBO MODE */ |  | ||||||
|   bool get_turbo_mode() const { return this->pbuf_[18] & 0x20 || this->pbuf_[20] & 0x02; } |  | ||||||
|   void set_turbo_mode(bool state) { |  | ||||||
|     this->set_bytemask_(18, 0x20, state); |  | ||||||
|     this->set_bytemask_(20, 0x02, state); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /* FREEZE PROTECTION */ |  | ||||||
|   bool get_freeze_protection_mode() const { return this->pbuf_[31] & 0x80; } |  | ||||||
|   void set_freeze_protection_mode(bool state) { this->set_bytemask_(31, 0x80, state); } |  | ||||||
|  |  | ||||||
|   /* PRESET */ |  | ||||||
|   optional<climate::ClimatePreset> get_preset() const; |  | ||||||
|   void set_preset(climate::ClimatePreset preset); |  | ||||||
|   void clear_presets(); |  | ||||||
|  |  | ||||||
|   bool is_custom_preset() const; |  | ||||||
|   const std::string &get_custom_preset() const; |  | ||||||
|   void set_custom_preset(const std::string &preset); |  | ||||||
|  |  | ||||||
|   /* POWER USAGE */ |  | ||||||
|   float get_power_usage() const; |  | ||||||
|  |  | ||||||
|   /// Set properties from another frame |  | ||||||
|   void set_properties(const PropertiesFrame &p) { memcpy(this->pbuf_ + 11, p.data() + 11, 10); } |  | ||||||
|  |  | ||||||
|  protected: |  | ||||||
|   /* POWER */ |  | ||||||
|   bool get_power_() const { return this->pbuf_[11] & 0x01; } |  | ||||||
|   void set_power_(bool state) { this->set_bytemask_(11, 0x01, state); } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| // Query state frame (read-only) |  | ||||||
| class QueryFrame : public midea_dongle::StaticFrame<midea_dongle::Frame> { |  | ||||||
|  public: |  | ||||||
|   QueryFrame() : StaticFrame(FPSTR(this->INIT)) {} |  | ||||||
|  |  | ||||||
|  private: |  | ||||||
|   static const uint8_t PROGMEM INIT[]; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| // Power query state frame (read-only) |  | ||||||
| class PowerQueryFrame : public midea_dongle::StaticFrame<midea_dongle::Frame> { |  | ||||||
|  public: |  | ||||||
|   PowerQueryFrame() : StaticFrame(FPSTR(this->INIT)) {} |  | ||||||
|  |  | ||||||
|  private: |  | ||||||
|   static const uint8_t PROGMEM INIT[]; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| // Command frame |  | ||||||
| class CommandFrame : public midea_dongle::StaticFrame<PropertiesFrame> { |  | ||||||
|  public: |  | ||||||
|   CommandFrame() : StaticFrame(FPSTR(this->INIT)) {} |  | ||||||
|   void set_beeper_feedback(bool state) { this->set_bytemask_(11, 0x40, state); } |  | ||||||
|  |  | ||||||
|  private: |  | ||||||
|   static const uint8_t PROGMEM INIT[]; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| }  // namespace midea_ac |  | ||||||
| }  // namespace esphome |  | ||||||
| @@ -1,30 +0,0 @@ | |||||||
| import esphome.codegen as cg |  | ||||||
| import esphome.config_validation as cv |  | ||||||
| from esphome.components import uart |  | ||||||
| from esphome.const import CONF_ID |  | ||||||
|  |  | ||||||
| DEPENDENCIES = ["wifi", "uart"] |  | ||||||
| CODEOWNERS = ["@dudanov"] |  | ||||||
|  |  | ||||||
| midea_dongle_ns = cg.esphome_ns.namespace("midea_dongle") |  | ||||||
| MideaDongle = midea_dongle_ns.class_("MideaDongle", cg.Component, uart.UARTDevice) |  | ||||||
|  |  | ||||||
| CONF_MIDEA_DONGLE_ID = "midea_dongle_id" |  | ||||||
| CONF_STRENGTH_ICON = "strength_icon" |  | ||||||
| CONFIG_SCHEMA = ( |  | ||||||
|     cv.Schema( |  | ||||||
|         { |  | ||||||
|             cv.GenerateID(): cv.declare_id(MideaDongle), |  | ||||||
|             cv.Optional(CONF_STRENGTH_ICON, default=False): cv.boolean, |  | ||||||
|         } |  | ||||||
|     ) |  | ||||||
|     .extend(cv.COMPONENT_SCHEMA) |  | ||||||
|     .extend(uart.UART_DEVICE_SCHEMA) |  | ||||||
| ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): |  | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |  | ||||||
|     await cg.register_component(var, config) |  | ||||||
|     await uart.register_uart_device(var, config) |  | ||||||
|     cg.add(var.use_strength_icon(config[CONF_STRENGTH_ICON])) |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user